Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ listed-plugin portable core, host adapters, and hermetic evals.
This repository is not public and does not publish packages. Pull-request CI runs
workflow gates, but `main` has no protected required checks. Production Gina MCP
remains at `https://askgina.ai/ai/gina/mcp`. Callers supply a bearer token. The
client exposes only the 29 catalog read tools.
client exposes only the 30 catalog read tools.

## Packages and runtimes

Expand Down
5 changes: 3 additions & 2 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion docs/programmatic-client.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,5 +34,5 @@ ask-gina list
ask-gina call gina.listScheduledPrompts '{}'
```

Only the 29 catalog read-tool names are callable. Unknown names are rejected
Only the 30 catalog read-tool names are callable. Unknown names are rejected
before transport. There is no login, DCR, or write/execute catalog.
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,8 @@
},
"overrides": {
"@effect/platform-node-shared": "4.0.0-rc.111",
"oxlint": "1.78.0"
"oxlint": "1.78.0",
"qs": "6.16.0"
},
"engines": {
"bun": "1.4.x"
Expand Down
95 changes: 47 additions & 48 deletions packages/contracts/__tests__/contracts.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,18 @@ import { Effect, Schema } from "effect";
import {
ASK_GINA_SKILL_DEFINITIONS,
EXECUTE_SCOPE,
EXECUTION_HANDOFF_ORIGIN,
EXECUTION_HANDOFF_PATHNAME,
GINA_PREDICTION_RENDER_TOOL_NAMES,
GINA_READ_TOOL_CATALOG,
GinaReadToolCatalogJsonSchema,
PRODUCTION_MCP_URL,
READ_SCOPE,
RELEASE_VERSION,
SOURCE_COMMIT,
buildExecutionHandoffUrl,
catalogSha,
getGinaReadToolAnnotations,
getGinaReadToolFamily,
isGinaMcpAppBoundReadTool,
isGinaPredictionRenderToolName,
isGinaReadToolName,
listCatalogToolNames,
} from "@askgina/contracts";
Expand Down Expand Up @@ -47,6 +46,7 @@ const EXPECTED_TOOL_NAMES = [
"predictions.getExpiringMarkets",
"predictions.getPredictionOrderbook",
"predictions.getSeriesMarket",
"predictions.getPredictionMarketDetails",
"predictions.fetchPolymarketData",
"predictions.fetchPolymarketHistory",
"predictions.getPolymarketPositions",
Expand All @@ -67,17 +67,29 @@ const EXPECTED_MCP_APP_BOUND_TOOLS: readonly string[] = [
"predictions.getPolymarketPositions",
];

const EXPECTED_PREDICTION_SKILL_TOOLS = [
"predictions.searchPredictionMarkets",
"predictions.getPredictionOrderbook",
"predictions.fetchPolymarketData",
"predictions.fetchPolymarketHistory",
"predictions.getPolymarketPositions",
"predictions.getPolymarketOrderHistory",
"predictions.renderPredictionPodium",
"predictions.renderPredictionBinaryMarket",
"predictions.renderPredictionCollection",
] as const;

const familyFromName = (name: (typeof EXPECTED_TOOL_NAMES)[number]) =>
name.startsWith("gina.") ? "portfolio" : name.split(".", 1)[0];

describe("@askgina/contracts", () => {
it.effect("publishes the exact 29-name catalog projection", () =>
it.effect("publishes the exact 30-name catalog projection", () =>
Effect.sync(() => {
assert.strictEqual(GINA_READ_TOOL_CATALOG.length, 29);
assert.strictEqual(GINA_READ_TOOL_CATALOG.length, 30);
assert.deepStrictEqual(listCatalogToolNames(), EXPECTED_TOOL_NAMES);
assert.deepStrictEqual(
GINA_READ_TOOL_CATALOG.map((tool) => Object.keys(tool)),
Array.from({ length: 29 }, () => [
Array.from({ length: 30 }, () => [
"name",
"family",
"readOnlyHint",
Expand All @@ -88,6 +100,7 @@ describe("@askgina/contracts", () => {
);
assert.isTrue(EXPECTED_TOOL_NAMES.every((name) => isGinaReadToolName(name)));
assert.isFalse(isGinaReadToolName("perps.placeOrder"));
assert.isFalse(isGinaReadToolName("predictions.renderPredictionPodium"));
assert.isFalse(isGinaReadToolName(undefined));
}),
);
Expand Down Expand Up @@ -119,47 +132,45 @@ describe("@askgina/contracts", () => {
}),
);

it.effect("publishes the exact skill ownership and handoff definitions", () =>
it.effect("publishes prediction renderer names and the public prediction skill tools", () =>
Effect.sync(() => {
assert.deepStrictEqual(GINA_PREDICTION_RENDER_TOOL_NAMES, [
"predictions.renderPredictionPodium",
"predictions.renderPredictionBinaryMarket",
"predictions.renderPredictionCollection",
]);
assert.isTrue(
GINA_PREDICTION_RENDER_TOOL_NAMES.every((name) => isGinaPredictionRenderToolName(name)),
);
assert.isFalse(isGinaPredictionRenderToolName("predictions.searchPredictionMarkets"));
assert.isFalse(isGinaPredictionRenderToolName(undefined));
}),
);

it.effect("publishes the exact skill ownership definitions", () =>
Effect.sync(() => {
assert.deepStrictEqual(
ASK_GINA_SKILL_DEFINITIONS.map(({ name, handoffAgent, handoffExamplePrompt }) => ({
name,
handoffAgent,
handoffExamplePrompt,
})),
ASK_GINA_SKILL_DEFINITIONS.map(({ name }) => name),
[
{
name: "review-gina-account",
handoffAgent: "gina",
handoffExamplePrompt: "Create a daily 9 AM portfolio summary.",
},
{
name: "research-spot-tokens",
handoffAgent: "gina",
handoffExamplePrompt: "Swap 0.5 ETH for USDC.",
},
{
name: "research-hyperliquid",
handoffAgent: "perps",
handoffExamplePrompt: "Place a 1 ETH long with a 2500 USDC stop.",
},
{
name: "research-prediction-markets",
handoffAgent: "predictions",
handoffExamplePrompt: "Buy 25 USDC of Yes on market 123.",
},
"review-gina-account",
"research-spot-tokens",
"research-hyperliquid",
"research-prediction-markets",
],
);

for (const skill of ASK_GINA_SKILL_DEFINITIONS) {
if (skill.name === "research-prediction-markets") {
assert.deepStrictEqual(skill.tools, EXPECTED_PREDICTION_SKILL_TOOLS);
continue;
}

const family =
skill.name === "review-gina-account"
? "portfolio"
: skill.name === "research-spot-tokens"
? "spot"
: skill.name === "research-hyperliquid"
? "perps"
: "predictions";
: "perps";
assert.deepStrictEqual(
skill.tools,
GINA_READ_TOOL_CATALOG.filter((tool) => tool.family === family).map((tool) => tool.name),
Expand All @@ -168,26 +179,14 @@ describe("@askgina/contracts", () => {
}),
);

it.effect("pins endpoints, scopes, handoff encoding, and source compatibility", () =>
it.effect("pins endpoints, scopes, and source compatibility", () =>
Effect.gen(function* () {
assert.strictEqual(PRODUCTION_MCP_URL, "https://askgina.ai/ai/gina/mcp");
assert.strictEqual(EXECUTION_HANDOFF_ORIGIN, "https://askgina.ai");
assert.strictEqual(EXECUTION_HANDOFF_PATHNAME, "/new");
assert.strictEqual(READ_SCOPE, "tools:read");
assert.strictEqual(EXECUTE_SCOPE, "tools:execute");
assert.strictEqual(RELEASE_VERSION, "0.1.0");
assert.strictEqual(SOURCE_COMMIT, "908af9015f1e87cf1ba4893226d149905e74df4a");

const handoffUrl = buildExecutionHandoffUrl("perps", "long ETH + set stop? 50%");
assert.strictEqual(
handoffUrl,
"https://askgina.ai/new?agent=perps&prompt=long%20ETH%20%2B%20set%20stop%3F%2050%25",
);
const parsedHandoffUrl = new URL(handoffUrl);
assert.deepStrictEqual([...parsedHandoffUrl.searchParams.keys()], ["agent", "prompt"]);
assert.strictEqual(parsedHandoffUrl.searchParams.get("agent"), "perps");
assert.strictEqual(parsedHandoffUrl.searchParams.get("prompt"), "long ETH + set stop? 50%");

const computedCatalogSha = createHash("sha256")
.update(yield* Schema.encodeEffect(GinaReadToolCatalogJsonSchema)(GINA_READ_TOOL_CATALOG))
.digest("hex");
Expand Down
70 changes: 42 additions & 28 deletions packages/contracts/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,13 @@
import { Function, Schema } from "effect";
import { Schema } from "effect";

export const PRODUCTION_MCP_URL = "https://askgina.ai/ai/gina/mcp";
export const EXECUTION_HANDOFF_ORIGIN = "https://askgina.ai";
export const EXECUTION_HANDOFF_PATHNAME = "/new";

export const READ_SCOPE = "tools:read";
export const EXECUTE_SCOPE = "tools:execute";

export const GINA_MCP_APP_FAMILY_VALUES = ["spot", "perps", "predictions", "portfolio"] as const;
export type GinaMcpAppFamily = (typeof GINA_MCP_APP_FAMILY_VALUES)[number];

export type ExecutionHandoffAgent = "gina" | "perps" | "predictions";

export type GinaReadToolAnnotations = Readonly<{
readOnlyHint: true;
destructiveHint: false;
Expand Down Expand Up @@ -226,6 +222,14 @@ export const GINA_READ_TOOL_CATALOG = [
openWorldHint: true,
mcpAppBound: false,
},
{
name: "predictions.getPredictionMarketDetails",
family: "predictions",
readOnlyHint: true,
destructiveHint: false,
openWorldHint: true,
mcpAppBound: false,
},
{
name: "predictions.fetchPolymarketData",
family: "predictions",
Expand Down Expand Up @@ -262,6 +266,23 @@ export const GINA_READ_TOOL_CATALOG = [

export type GinaReadToolName = (typeof GINA_READ_TOOL_CATALOG)[number]["name"];

export const GINA_PREDICTION_RENDER_TOOL_NAMES = [
"predictions.renderPredictionPodium",
"predictions.renderPredictionBinaryMarket",
"predictions.renderPredictionCollection",
] as const;

export type GinaPredictionRenderToolName = (typeof GINA_PREDICTION_RENDER_TOOL_NAMES)[number];

export const isGinaPredictionRenderToolName = (
name: unknown,
): name is GinaPredictionRenderToolName =>
name === "predictions.renderPredictionPodium" ||
name === "predictions.renderPredictionBinaryMarket" ||
name === "predictions.renderPredictionCollection";

export type AskGinaSkillToolName = GinaReadToolName | GinaPredictionRenderToolName;

export const GINA_CLOSED_WORLD_READ_TOOL_NAMES = [
"gina.getAccountAddresses",
"gina.listScheduledPrompts",
Expand Down Expand Up @@ -290,7 +311,7 @@ export const GinaReadToolCatalogEntrySchema = Schema.Struct({
});

export const GinaReadToolCatalogSchema = Schema.Array(GinaReadToolCatalogEntrySchema).check(
Schema.isLengthBetween(29, 29),
Schema.isLengthBetween(30, 30),
);

export const GinaReadToolCatalogJsonSchema = Schema.fromJsonString(GinaReadToolCatalogSchema);
Expand All @@ -316,15 +337,6 @@ export const isGinaMcpAppBoundReadTool = (name: GinaReadToolName): boolean =>
name as (typeof GINA_MCP_APP_BOUND_READ_TOOL_NAMES)[number],
);

export const buildExecutionHandoffUrl = Function.dual<
(prompt: string) => (agent: ExecutionHandoffAgent) => string,
(agent: ExecutionHandoffAgent, prompt: string) => string
>(
2,
(agent, prompt) =>
`${EXECUTION_HANDOFF_ORIGIN}${EXECUTION_HANDOFF_PATHNAME}?agent=${agent}&prompt=${encodeURIComponent(prompt)}`,
);

const sharedReadTools = GINA_READ_TOOL_CATALOG.filter((tool) => tool.family === "portfolio").map(
(tool) => tool.name,
);
Expand All @@ -342,38 +354,40 @@ export type SkillName = (typeof SKILL_NAMES)[number];

export type AskGinaSkillDefinition = Readonly<{
name: SkillName;
handoffAgent: ExecutionHandoffAgent;
handoffExamplePrompt: string;
tools: readonly GinaReadToolName[];
tools: readonly AskGinaSkillToolName[];
}>;

const PREDICTION_SKILL_TOOLS = [
"predictions.searchPredictionMarkets",
"predictions.getPredictionOrderbook",
"predictions.fetchPolymarketData",
"predictions.fetchPolymarketHistory",
"predictions.getPolymarketPositions",
"predictions.getPolymarketOrderHistory",
"predictions.renderPredictionPodium",
"predictions.renderPredictionBinaryMarket",
"predictions.renderPredictionCollection",
] as const satisfies readonly AskGinaSkillToolName[];

export const ASK_GINA_SKILL_DEFINITIONS = [
{
name: "review-gina-account",
handoffAgent: "gina",
handoffExamplePrompt: "Create a daily 9 AM portfolio summary.",
tools: sharedReadTools,
},
{
name: "research-spot-tokens",
handoffAgent: "gina",
handoffExamplePrompt: "Swap 0.5 ETH for USDC.",
tools: familyTools("spot"),
},
{
name: "research-hyperliquid",
handoffAgent: "perps",
handoffExamplePrompt: "Place a 1 ETH long with a 2500 USDC stop.",
tools: familyTools("perps"),
},
{
name: "research-prediction-markets",
handoffAgent: "predictions",
handoffExamplePrompt: "Buy 25 USDC of Yes on market 123.",
tools: familyTools("predictions"),
tools: PREDICTION_SKILL_TOOLS,
},
] as const satisfies readonly AskGinaSkillDefinition[];

export const SOURCE_COMMIT = "908af9015f1e87cf1ba4893226d149905e74df4a";
export const RELEASE_VERSION = "0.1.0";
export const catalogSha = "06a3c7ca4f56617e7aebdcc840b96f4fcfbffeafb7d5b359d1d4c90eb4aeefda";
export const catalogSha = "92fb5788ef462b85710f03c4fa6b8b67b8beaf3e20a478e6128daf839fb1bbee";
2 changes: 1 addition & 1 deletion packages/evals/__tests__/responses-api.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ describe("Responses API trial adapter", () => {
assert.strictEqual(request.url, "https://api.openai.com/v1/responses");
assert.strictEqual(request.headers.authorization, "Bearer synthetic-fixture");
assert.strictEqual(request.headers.accept, "application/json");
assert.strictEqual(allowedTools.length, 29);
assert.strictEqual(allowedTools.length, 30);

const body = readRequestBody(request);
assert.strictEqual(body.model, "chat-latest");
Expand Down
2 changes: 1 addition & 1 deletion packages/evals/__tests__/sanitize.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ const aggregate: SanitizedEvalAggregate = {
suiteId: "synthetic-model-smoke-v1",
suiteVersion: 1,
fixtureVersion: 1,
catalogSha: "06a3c7ca4f56617e7aebdcc840b96f4fcfbffeafb7d5b359d1d4c90eb4aeefda",
catalogSha: "92fb5788ef462b85710f03c4fa6b8b67b8beaf3e20a478e6128daf839fb1bbee",
overall: { passed: 2, total: 3 },
dimensions: {
routing: { passed: 2, failed: 1 },
Expand Down
8 changes: 0 additions & 8 deletions packages/evals/src/corpus/positive-execution-handoff.yaml

This file was deleted.

2 changes: 1 addition & 1 deletion packages/evals/src/fixtures/sanitized-aggregate.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"suiteId": "synthetic-model-smoke-v1",
"suiteVersion": 1,
"fixtureVersion": 1,
"catalogSha": "06a3c7ca4f56617e7aebdcc840b96f4fcfbffeafb7d5b359d1d4c90eb4aeefda",
"catalogSha": "92fb5788ef462b85710f03c4fa6b8b67b8beaf3e20a478e6128daf839fb1bbee",
"overall": {
"passed": 2,
"total": 3
Expand Down
Loading
Loading