From 1f61269377083c91f4c5faf221d6decf3ee9aa77 Mon Sep 17 00:00:00 2001 From: Rhys Sullivan <39114868+RhysSullivan@users.noreply.github.com> Date: Wed, 26 Aug 2026 16:15:18 -0700 Subject: [PATCH 1/2] Serve runtime-observed output shapes for schemaless tools --- e2e/scenarios/shape-memory.test.ts | 178 ++++++++++++++ packages/core/execution/src/skills.ts | 1 + .../core/execution/src/tool-invoker.test.ts | 26 +++ packages/core/execution/src/tool-invoker.ts | 9 + packages/core/sdk/src/executor.test.ts | 57 +++++ packages/core/sdk/src/executor.ts | 51 +++- packages/core/sdk/src/shape-inference.test.ts | 136 +++++++++++ packages/core/sdk/src/shape-inference.ts | 219 ++++++++++++++++++ packages/core/sdk/src/shape-memory.ts | 90 +++++++ packages/core/sdk/src/types.ts | 4 + 10 files changed, 767 insertions(+), 4 deletions(-) create mode 100644 e2e/scenarios/shape-memory.test.ts create mode 100644 packages/core/sdk/src/shape-inference.test.ts create mode 100644 packages/core/sdk/src/shape-inference.ts create mode 100644 packages/core/sdk/src/shape-memory.ts diff --git a/e2e/scenarios/shape-memory.test.ts b/e2e/scenarios/shape-memory.test.ts new file mode 100644 index 0000000000..a1d2f5e123 --- /dev/null +++ b/e2e/scenarios/shape-memory.test.ts @@ -0,0 +1,178 @@ +// Cross-target: muscle memory — runtime-observed output shapes. Most OpenAPI +// operations declare no response schema, so `tools.describe.tool()` used to +// render `data: unknown` forever and the model had to guess response shapes. +// This journey proves the warm path end to end through public surfaces only: +// a schemaless tool describes as `unknown`, one real invocation against a live +// upstream teaches the shape, and the very next describe serves a real +// TypeScript type marked as observed. +import { randomBytes } from "node:crypto"; +import { createServer } from "node:http"; + +import { expect } from "@effect/vitest"; +import { Effect } from "effect"; +import { composePluginApi } from "@executor-js/api/server"; +import { openApiHttpPlugin } from "@executor-js/plugin-openapi/api"; +import { AuthTemplateSlug, ConnectionName, IntegrationSlug } from "@executor-js/sdk/shared"; + +import { scenario } from "../src/scenario"; +import { Api, Target } from "../src/services"; + +const api = composePluginApi([openApiHttpPlugin()] as const); + +/** One GET operation whose 200 declares no response schema — the shape the + * model would otherwise have to guess. */ +const issuesSpec = JSON.stringify({ + openapi: "3.0.3", + info: { title: "Issues API", version: "1.0.0" }, + paths: { + "/issues": { + get: { + operationId: "listIssues", + summary: "List issues", + responses: { "200": { description: "issues" } }, + }, + }, + }, +}); + +/** A live upstream for the single invocation that teaches the shape. */ +const serveIssuesFixture = Effect.acquireRelease( + Effect.callback<{ readonly url: string; readonly close: () => void }>((resume) => { + const server = createServer((_request, response) => { + response.writeHead(200, { "content-type": "application/json" }); + response.end( + JSON.stringify({ + issues: [ + { id: 1, title: "first", open: true }, + { id: 2, title: "second", open: false }, + ], + total: 2, + }), + ); + }); + server.listen(0, "127.0.0.1", () => { + const addressInfo = server.address(); + const port = typeof addressInfo === "object" && addressInfo !== null ? addressInfo.port : 0; + resume( + Effect.succeed({ + url: `http://127.0.0.1:${port}`, + close: () => { + server.close(); + server.closeAllConnections(); + }, + }), + ); + }); + }), + (fixture) => Effect.sync(fixture.close), +); + +const describeCode = (slug: string) => ` +const details = await tools.describe.tool({ path: "${slug}.org.main.issues.listIssues" }); +return { + outputTypeScript: details.outputTypeScript ?? null, + note: details.outputTypeScriptNote ?? null, + error: details.error ?? null, +}; +`; + +type DescribeOutcome = { + readonly outputTypeScript: string | null; + readonly note: string | null; + readonly error: unknown; +}; + +scenario( + "Muscle memory · a schemaless tool's observed output shape reaches describe", + {}, + Effect.scoped( + Effect.gen(function* () { + const target = yield* Target; + const { client: makeApiClient } = yield* Api; + const identity = yield* target.newIdentity(); + const client = yield* makeApiClient(api, identity); + const slug = IntegrationSlug.make(`shape_memory_${randomBytes(4).toString("hex")}`); + const upstream = yield* serveIssuesFixture; + + yield* Effect.ensuring( + Effect.gen(function* () { + yield* client.openapi.addSpec({ + payload: { + spec: { kind: "blob", value: issuesSpec }, + slug, + baseUrl: upstream.url, + authenticationTemplate: [ + { + slug: "apiKey", + type: "apiKey", + headers: { authorization: ["Bearer ", { type: "variable", name: "token" }] }, + }, + ], + }, + }); + yield* client.connections.create({ + payload: { + owner: "org", + name: ConnectionName.make("main"), + integration: slug, + template: AuthTemplateSlug.make("apiKey"), + value: `key_${randomBytes(8).toString("hex")}`, + }, + }); + + const describe = Effect.gen(function* () { + const executed = yield* client.executions.execute({ + payload: { code: describeCode(String(slug)), autoApprove: true }, + }); + expect(executed.status, executed.text).toBe("completed"); + return JSON.parse(executed.text) as DescribeOutcome; + }); + + // Cold: no declared response schema — the model sees unknown. + const cold = yield* describe; + expect(cold.error, "the tool resolves").toBeNull(); + expect(cold.outputTypeScript, "cold describe has no shape").toContain("data: unknown;"); + expect(cold.note, "cold describe carries no provenance note").toBeNull(); + + // One real call against the live upstream teaches the shape. + const invoked = yield* client.executions.execute({ + payload: { + code: ` +const result = await tools.${slug}.org.main.issues.listIssues({}); +return { ok: result.ok }; +`, + autoApprove: true, + }, + }); + expect(invoked.status, invoked.text).toBe("completed"); + expect(JSON.parse(invoked.text), "the teaching call succeeded").toEqual({ ok: true }); + + // Warm: the observed shape is served, marked as observed. + const warm = yield* describe; + expect(warm.outputTypeScript, "warm describe serves the observed shape").toContain( + "issues", + ); + expect(warm.outputTypeScript, "field types come from the live payload").toContain( + "total", + ); + expect(warm.outputTypeScript, "the shape no longer collapses").not.toContain( + "data: unknown;", + ); + expect(warm.note, "provenance is explicit").toContain("observed from 1 live response"); + }), + Effect.gen(function* () { + yield* client.connections + .remove({ + params: { + owner: "org", + integration: slug, + name: ConnectionName.make("main"), + }, + }) + .pipe(Effect.ignore); + yield* client.openapi.removeSpec({ params: { slug } }).pipe(Effect.ignore); + }), + ); + }), + ), +); diff --git a/packages/core/execution/src/skills.ts b/packages/core/execution/src/skills.ts index f503b26983..deeb31cb3b 100644 --- a/packages/core/execution/src/skills.ts +++ b/packages/core/execution/src/skills.ts @@ -58,6 +58,7 @@ const EXECUTE_SKILL_BODY = [ "- The `tools` object is a lazy proxy — enumerating it (`Object.keys(tools)`, spread, `for...in`) throws. Use `tools.search()` or `tools.executor.coreTools.connections.list({})` instead.", '- Pass an object to system tools, e.g. `tools.search({ query: "..." })`, `tools.executor.coreTools.connections.list({})`, and `tools.describe.tool({ path })`.', '- `tools.describe.tool()` returns compact TypeScript shapes. Use `inputTypeScript`, `outputTypeScript`, and `typeScriptDefinitions`. If the path doesn\'t resolve, the result carries `error: { code: "tool_not_found", suggestions }` — use a suggestion instead of retrying the same path.', + "- When `outputTypeScriptNote` is present, the `data` type was observed from live responses rather than declared by the provider: the listed fields are reliable, but the shape may be incomplete — prefer optional access for anything not listed.", "- For tools that return large collections (e.g. `getStates`, `getAll`), filter results in code rather than calling per-item tools.", "- Do not use `fetch` — all API calls go through `tools.*`.", "- If execution pauses for interaction, resume it with the returned `resumePayload`.", diff --git a/packages/core/execution/src/tool-invoker.test.ts b/packages/core/execution/src/tool-invoker.test.ts index 4d7d6681aa..505c0f0f91 100644 --- a/packages/core/execution/src/tool-invoker.test.ts +++ b/packages/core/execution/src/tool-invoker.test.ts @@ -877,6 +877,32 @@ describe("tool discovery", () => { }), ); + it.effect("serves an observed shape with a provenance note once a schemaless tool runs", () => + Effect.gen(function* () { + const executor = yield* makeSearchExecutor(); + const invoker = makeExecutorToolInvoker(executor, { + invokeOptions: { onElicitation: acceptAll }, + }); + + // Cold: no declared output schema — data renders as unknown, no note. + const cold = yield* describeTool(executor, "github.org.main.listRepositoryIssues"); + expect(cold.outputTypeScript).toContain("data: unknown;"); + expect(cold.outputTypeScriptNote).toBeUndefined(); + + yield* invoker.invoke({ + path: "github.org.main.listRepositoryIssues", + args: { owner: "executor", repo: "executor" }, + }); + + // Warm: the live `[]` payload becomes the served type, marked observed. + const warm = yield* describeTool(executor, "github.org.main.listRepositoryIssues"); + expect(warm.outputTypeScript).toBe( + "{ ok: true; data: unknown[]; http?: ToolHttpMeta } | { ok: false; error: ToolError }", + ); + expect(warm.outputTypeScriptNote).toContain("observed from 1 live response"); + }), + ); + it.effect("describes a return type that accepts the sandbox invocation result", () => Effect.gen(function* () { const executor = yield* makeSearchExecutor(); diff --git a/packages/core/execution/src/tool-invoker.ts b/packages/core/execution/src/tool-invoker.ts index 4da9251767..f47235401b 100644 --- a/packages/core/execution/src/tool-invoker.ts +++ b/packages/core/execution/src/tool-invoker.ts @@ -76,6 +76,7 @@ type DescribedTool = { readonly description?: string; readonly inputTypeScript?: string; readonly outputTypeScript?: string; + readonly outputTypeScriptNote?: string; readonly typeScriptDefinitions?: Record; /** Set when the path resolves to no tool — mirrors invoke's tool_not_found. */ readonly error?: { @@ -866,6 +867,14 @@ export const describeTool = Effect.fn("executor.tools.describe")(function* ( description: schema.description, inputTypeScript: schema.inputTypeScript, outputTypeScript: wrapOutputTypeScript(schema.outputTypeScript), + // The compact TS render drops the schema's provenance description, so an + // observed (runtime-inferred) shape gets an explicit note: the model + // should treat the fields as reliable but not exhaustive. + ...(schema.outputSchemaSource === "observed" + ? { + outputTypeScriptNote: `data type observed from ${schema.outputSchemaObservations ?? 1} live response(s), not declared by the provider; fields may be incomplete.`, + } + : {}), typeScriptDefinitions: withToolResultDefinitions(schema.typeScriptDefinitions), }; return described; diff --git a/packages/core/sdk/src/executor.test.ts b/packages/core/sdk/src/executor.test.ts index 00b878fd64..19319061a5 100644 --- a/packages/core/sdk/src/executor.test.ts +++ b/packages/core/sdk/src/executor.test.ts @@ -673,3 +673,60 @@ describe("createExecutor", () => { }), ); }); + +describe("muscle memory (observed output shapes)", () => { + const provisioned = Effect.fn(function* () { + const executor = yield* makeTestExecutor({ + plugins: [demoPlugin] as const, + coreTools: { webBaseUrl: "http://localhost:3000" }, + }); + yield* executor.demo.seed(); + yield* executor.execute(ToolAddress.make("executor.coreTools.connections.create"), { + owner: "org", + name: String(CONN), + integration: String(INTEG), + template: String(TEMPLATE), + identityLabel: "Demo", + from: { provider: "memory", id: "secret-token" }, + }); + return executor; + }); + + it.effect("serves an observed output shape once a schemaless tool has run", () => + Effect.gen(function* () { + const executor = yield* provisioned(); + + // Cold: `run` declares no output schema, nothing observed yet. + const cold = yield* executor.tools.schema(addr("run")); + expect(cold?.outputSchema).toBeUndefined(); + expect(cold?.outputTypeScript).toBeUndefined(); + + yield* executor.execute(addr("run"), {}); + + // Warm: the live payload `{ ran: "run" }` becomes the served shape, + // with provenance marked on the schema. + const warm = yield* executor.tools.schema(addr("run")); + expect(warm?.outputSchema).toMatchObject({ + type: "object", + properties: { ran: { type: "string" } }, + required: ["ran"], + description: "Observed from 1 live response; fields may be incomplete.", + }); + expect(warm?.outputTypeScript).toContain("ran"); + expect(warm?.outputTypeScript).not.toBe("unknown"); + }), + ); + + it.effect("never overrides a declared output schema with observations", () => + Effect.gen(function* () { + const executor = yield* provisioned(); + + // `inspect` declares `outputSchema: { $ref: "#/$defs/Owner" }`; running + // it observes `{ ran: "inspect" }`, which must not displace the + // declared schema. + yield* executor.execute(addr("inspect"), { pet: { lives: 9 } }); + const schema = yield* executor.tools.schema(addr("inspect")); + expect(schema?.outputSchema).toEqual({ $ref: "#/$defs/Owner" }); + }), + ); +}); diff --git a/packages/core/sdk/src/executor.ts b/packages/core/sdk/src/executor.ts index f1b9443477..2ce9a39227 100644 --- a/packages/core/sdk/src/executor.ts +++ b/packages/core/sdk/src/executor.ts @@ -176,7 +176,8 @@ import { type EnterpriseManagedRollout, } from "./oauth-ema"; import { connectionIdentifier } from "./connection-name-identifier"; -import { annotateToolResultOutcome } from "./tool-result"; +import { annotateToolResultOutcome, isToolResult } from "./tool-result"; +import { makeShapeMemory, observedShapeToJsonSchema, SHAPE_MEMORY_PLUGIN_ID } from "./shape-memory"; import { isUnauthorizedToolFailure } from "./auth-tool-failure"; const PLUGIN_STORAGE_DELETE_KEY_BATCH_SIZE = 90; @@ -1664,6 +1665,13 @@ export const createExecutor = (effect: Effect.Effect) => fuma.transaction(effect); + // Runtime-observed output shapes ("muscle memory"): learned on the + // execute success path, served by tools.schema when a tool declares no + // output schema. Backed by plugin_storage under a reserved system id. + const shapeMemory = makeShapeMemory( + makePluginStorageFacade({ core, pluginId: SHAPE_MEMORY_PLUGIN_ID, owner: ownerBinding }), + ); + // Populated once, never mutated after startup. const staticTools = new Map(); const runtimes = new Map(); @@ -4058,6 +4066,21 @@ export const createExecutor = b.and( @@ -4069,12 +4092,12 @@ export const createExecutor = (); for (const def of definitionRows) defs.set(def.name, decodeJsonColumn(def.schema)); - const referenced = collectReferencedDefinitions([inputSchema, outputSchema], defs); + const referenced = collectReferencedDefinitions([inputSchema, effectiveOutputSchema], defs); const preview = yield* Effect.tryPromise({ try: () => buildToolTypeScriptPreview({ inputSchema, - outputSchema, + outputSchema: effectiveOutputSchema, defs, }), catch: (cause) => @@ -4087,7 +4110,13 @@ export const createExecutor = 0 ? (referenced as Record) @@ -4762,6 +4791,20 @@ export const createExecutor = { + if (staticTools.has(String(address))) return Effect.void; + const parsed = parseToolAddress(String(address)); + if (!parsed) return Effect.void; + const data = isToolResult(result) ? (result.ok ? result.data : undefined) : result; + if (data === undefined) return Effect.void; + return shapeMemory.observe(String(address), parsed.owner, data); + }), Effect.withSpan("executor.tool.execute", { attributes: { "mcp.tool.name": String(address), diff --git a/packages/core/sdk/src/shape-inference.test.ts b/packages/core/sdk/src/shape-inference.test.ts new file mode 100644 index 0000000000..784005e082 --- /dev/null +++ b/packages/core/sdk/src/shape-inference.test.ts @@ -0,0 +1,136 @@ +import { describe, expect, it } from "@effect/vitest"; + +import { inferShape, mergeShapes, observeShape, type InferredShape } from "./shape-inference"; + +describe("inferShape", () => { + it("infers primitives without recording values", () => { + expect(inferShape("secret token")).toEqual({ type: "string" }); + expect(inferShape(42)).toEqual({ type: "number" }); + expect(inferShape(true)).toEqual({ type: "boolean" }); + expect(inferShape(null)).toEqual({ type: "null" }); + expect(inferShape(undefined)).toEqual({ type: "null" }); + }); + + it("infers object structure with all keys required", () => { + expect(inferShape({ id: "abc", count: 3 })).toEqual({ + type: "object", + properties: { id: { type: "string" }, count: { type: "number" } }, + required: ["count", "id"], + }); + }); + + it("merges sampled array elements into one item shape", () => { + expect(inferShape([{ id: 1 }, { id: 2, label: "x" }])).toEqual({ + type: "array", + items: { + type: "object", + properties: { id: { type: "number" }, label: { type: "string" } }, + required: ["id"], + }, + }); + }); + + it("keeps an empty array itemless", () => { + expect(inferShape([])).toEqual({ type: "array" }); + }); + + it("collapses wide objects to a map so data-bearing keys never persist", () => { + const byEmail = Object.fromEntries( + Array.from({ length: 40 }, (_, i) => [`user${i}@example.com`, { active: true }]), + ); + const shape = inferShape(byEmail); + expect(shape.properties).toBeUndefined(); + expect(shape.additionalProperties).toEqual({ + type: "object", + properties: { active: { type: "boolean" } }, + required: ["active"], + }); + }); + + it("degrades to unknown past the depth bound", () => { + let value: unknown = "leaf"; + for (let i = 0; i < 10; i++) value = { child: value }; + const json = JSON.stringify(inferShape(value)); + expect(json).toContain("{}"); + }); +}); + +describe("mergeShapes", () => { + it("makes fields missing from one observation optional", () => { + const merged = mergeShapes(inferShape({ id: "a", email: "x@y.z" }), inferShape({ id: "b" })); + expect(merged).toEqual({ + type: "object", + properties: { id: { type: "string" }, email: { type: "string" } }, + required: ["id"], + }); + }); + + it("unions differing primitive types", () => { + expect(mergeShapes({ type: "string" }, { type: "number" })).toEqual({ + anyOf: [{ type: "string" }, { type: "number" }], + }); + }); + + it("merges same-typed union branches instead of duplicating them", () => { + const union = mergeShapes({ type: "string" }, { type: "null" }); + const widened = mergeShapes(union, inferShape({ id: 1 })); + const again = mergeShapes(widened, inferShape({ id: 2, extra: true })); + expect(again.anyOf).toHaveLength(3); + const objectBranch = again.anyOf?.find((branch) => branch.type === "object"); + expect(objectBranch?.required).toEqual(["id"]); + }); + + it("degrades to unknown when the union grows past its cap", () => { + const wide = [ + { type: "string" } as const, + { type: "number" } as const, + { type: "boolean" } as const, + { type: "null" } as const, + { type: "array" } as const, + ].reduce((left, right) => mergeShapes(left, right), { type: "string" }); + expect(wide).toEqual({}); + }); + + it("merges array item shapes across calls", () => { + const merged = mergeShapes(inferShape([{ id: 1 }]), inferShape([{ id: 2, done: false }])); + expect(merged).toEqual({ + type: "array", + items: { + type: "object", + properties: { id: { type: "number" }, done: { type: "boolean" } }, + required: ["id"], + }, + }); + }); +}); + +describe("observeShape", () => { + it("starts a record from the first observation", () => { + const record = observeShape(null, { ok: true }, 1000); + expect(record.observations).toBe(1); + expect(record.updatedAt).toBe(1000); + expect(record.schema.type).toBe("object"); + }); + + it("accumulates observations by merging", () => { + const first = observeShape(null, { id: "a", email: "x@y.z" }, 1000); + const second = observeShape(first, { id: "b" }, 2000); + expect(second.observations).toBe(2); + expect(second.updatedAt).toBe(2000); + expect(second.schema.required).toEqual(["id"]); + expect(Object.keys(second.schema.properties ?? {}).sort()).toEqual(["email", "id"]); + }); + + it("stays under the serialized size bound for adversarial payloads", () => { + const wide = Object.fromEntries( + Array.from({ length: 24 }, (_, i) => [ + `field_with_a_rather_long_name_${i}`, + Object.fromEntries( + Array.from({ length: 24 }, (_, j) => [`nested_property_name_${j}`, { deep: { x: 1 } }]), + ), + ]), + ); + const record = observeShape(null, wide, 1000); + expect(JSON.stringify(record.schema).length).toBeLessThanOrEqual(16_000); + }); +}); diff --git a/packages/core/sdk/src/shape-inference.ts b/packages/core/sdk/src/shape-inference.ts new file mode 100644 index 0000000000..3cd0e5d33d --- /dev/null +++ b/packages/core/sdk/src/shape-inference.ts @@ -0,0 +1,219 @@ +/** + * Runtime output-shape inference — the "muscle memory" half of code mode. + * + * Most tools declare no output schema, so `tools.describe.tool()` renders + * `data: unknown` and the model guesses response shapes. This module infers a + * lightweight shape from real tool results at dispatch time: field names and + * broad types only, never values. Shapes merge across calls, so unions and + * optional fields converge instead of thrashing. + * + * The output is a small JSON Schema subset (`type`, `properties`, `required`, + * `items`, `additionalProperties`, `anyOf`) so it can be stored as plain JSON + * and rendered through the same schema → TypeScript compiler that declared + * schemas use. + * + * Every dimension is bounded — depth, object width, array sampling, union + * width, serialized size — because inference runs on the hot dispatch path + * against arbitrary upstream payloads. + */ + +export type InferredShape = { + readonly type?: "null" | "boolean" | "number" | "string" | "object" | "array"; + readonly properties?: Readonly>; + readonly required?: readonly string[]; + readonly items?: InferredShape; + readonly additionalProperties?: InferredShape; + readonly anyOf?: readonly InferredShape[]; +}; + +/** `{}` — matches anything; the "we know nothing beyond this point" shape. */ +const UNKNOWN: InferredShape = {}; + +const MAX_DEPTH = 6; +/** Array elements sampled per call; later calls keep widening the merge. */ +const MAX_ARRAY_SAMPLE = 5; +/** + * An object with more own keys than this is treated as a map keyed by data + * (ids, emails, dates) rather than a struct. Collapsing to + * `additionalProperties` keeps data-bearing keys out of the shape — field + * names of a struct are API surface, but map keys are values. + */ +const MAX_OBJECT_KEYS = 24; +/** Union width cap; beyond this the shape degrades to unknown. */ +const MAX_ANYOF = 4; + +const isUnknown = (shape: InferredShape): boolean => + shape.type === undefined && shape.anyOf === undefined; + +/** Infer the shape of one observed value. Reads structure only, never values. */ +export const inferShape = (value: unknown, depth = 0): InferredShape => { + if (value === null || value === undefined) return { type: "null" }; + if (typeof value === "boolean") return { type: "boolean" }; + if (typeof value === "number") return { type: "number" }; + if (typeof value === "string") return { type: "string" }; + if (depth >= MAX_DEPTH) return UNKNOWN; + + if (Array.isArray(value)) { + if (value.length === 0) return { type: "array" }; + const sampled = value + .slice(0, MAX_ARRAY_SAMPLE) + .map((item) => inferShape(item, depth + 1)) + .reduce((left, right) => mergeShapes(left, right)); + return { type: "array", items: sampled }; + } + + if (typeof value === "object") { + const entries = Object.entries(value as Record); + if (entries.length > MAX_OBJECT_KEYS) { + const merged = entries + .slice(0, MAX_ARRAY_SAMPLE) + .map(([, item]) => inferShape(item, depth + 1)) + .reduce((left, right) => mergeShapes(left, right)); + return { type: "object", additionalProperties: merged }; + } + const properties: Record = {}; + for (const [key, item] of entries) { + properties[key] = inferShape(item, depth + 1); + } + return { type: "object", properties, required: entries.map(([key]) => key).sort() }; + } + + // function / symbol / bigint — nothing useful to say structurally. + return UNKNOWN; +}; + +const mergeObjectShapes = (left: InferredShape, right: InferredShape): InferredShape => { + // A map-shaped observation absorbs struct-shaped ones: once keys look like + // data, later struct keys are data too. + if (left.additionalProperties !== undefined || right.additionalProperties !== undefined) { + const values = [ + left.additionalProperties, + right.additionalProperties, + ...Object.values(left.properties ?? {}), + ...Object.values(right.properties ?? {}), + ].filter((shape): shape is InferredShape => shape !== undefined); + return { + type: "object", + additionalProperties: + values.length === 0 ? UNKNOWN : values.reduce((a, b) => mergeShapes(a, b)), + }; + } + + const leftProps = left.properties ?? {}; + const rightProps = right.properties ?? {}; + const keys = [...new Set([...Object.keys(leftProps), ...Object.keys(rightProps)])].sort(); + if (keys.length > MAX_OBJECT_KEYS) { + const values = keys + .slice(0, MAX_ARRAY_SAMPLE) + .map((key) => leftProps[key] ?? rightProps[key]) + .filter((shape): shape is InferredShape => shape !== undefined); + return { + type: "object", + additionalProperties: + values.length === 0 ? UNKNOWN : values.reduce((a, b) => mergeShapes(a, b)), + }; + } + + const properties: Record = {}; + for (const key of keys) { + const a = leftProps[key]; + const b = rightProps[key]; + properties[key] = a !== undefined && b !== undefined ? mergeShapes(a, b) : (a ?? b ?? UNKNOWN); + } + const leftRequired = new Set(left.required ?? []); + const required = (right.required ?? []).filter((key) => leftRequired.has(key)).sort(); + return { type: "object", properties, required }; +}; + +/** + * Merge two observed shapes into the narrowest shape matching both. + * Same-typed shapes merge structurally; differently-typed shapes union into + * `anyOf`, degrading to unknown past `MAX_ANYOF` branches. + */ +export const mergeShapes = (left: InferredShape, right: InferredShape): InferredShape => { + if (isUnknown(left) || isUnknown(right)) return UNKNOWN; + + if (left.anyOf !== undefined || right.anyOf !== undefined) { + const branches = [...(left.anyOf ?? [left]), ...(right.anyOf ?? [right])]; + return branches.reduce((merged, branch) => addUnionBranch(merged, branch), { + anyOf: [], + } as InferredShape); + } + + if (left.type !== right.type) return addUnionBranch({ anyOf: [left] }, right); + + if (left.type === "object") return mergeObjectShapes(left, right); + if (left.type === "array") { + if (left.items === undefined) return right; + if (right.items === undefined) return left; + return { type: "array", items: mergeShapes(left.items, right.items) }; + } + return left; +}; + +const addUnionBranch = (union: InferredShape, branch: InferredShape): InferredShape => { + if (isUnknown(union) || isUnknown(branch)) return UNKNOWN; + const branches = [...(union.anyOf ?? [union])]; + const index = branches.findIndex((existing) => existing.type === branch.type); + const next = + index === -1 + ? [...branches, branch] + : branches.map((existing, i) => (i === index ? mergeShapes(existing, branch) : existing)); + if (next.length > MAX_ANYOF) return UNKNOWN; + return next.length === 1 ? (next[0] ?? UNKNOWN) : { anyOf: next }; +}; + +/** + * One tool's accumulated muscle memory: the merged shape plus enough + * bookkeeping to judge freshness. Stored as plain JSON. + */ +export type ObservedShape = { + readonly schema: InferredShape; + readonly observations: number; + readonly updatedAt: number; +}; + +/** Serialized-size ceiling per tool; a shape past this degrades to unknown + * children rather than growing without bound. */ +const MAX_SHAPE_JSON_CHARS = 16_000; + +const shrink = (shape: InferredShape, depth: number): InferredShape => { + if (depth <= 0) return UNKNOWN; + if (shape.anyOf) return { anyOf: shape.anyOf.map((branch) => shrink(branch, depth - 1)) }; + if (shape.type === "array" && shape.items) { + return { type: "array", items: shrink(shape.items, depth - 1) }; + } + if (shape.type === "object" && shape.additionalProperties) { + return { type: "object", additionalProperties: shrink(shape.additionalProperties, depth - 1) }; + } + if (shape.type === "object" && shape.properties) { + const properties: Record = {}; + for (const [key, child] of Object.entries(shape.properties)) { + properties[key] = shrink(child, depth - 1); + } + return { ...shape, properties }; + } + return shape; +}; + +/** Fold a new observation into an existing record, keeping the result bounded. */ +export const observeShape = ( + previous: ObservedShape | null, + value: unknown, + now: number, +): ObservedShape => { + const observed = inferShape(value); + let schema = previous === null ? observed : mergeShapes(previous.schema, observed); + for ( + let depth = MAX_DEPTH; + depth > 0 && JSON.stringify(schema).length > MAX_SHAPE_JSON_CHARS; + depth-- + ) { + schema = shrink(schema, depth); + } + return { + schema, + observations: (previous?.observations ?? 0) + 1, + updatedAt: now, + }; +}; diff --git a/packages/core/sdk/src/shape-memory.ts b/packages/core/sdk/src/shape-memory.ts new file mode 100644 index 0000000000..d5ab9f1d22 --- /dev/null +++ b/packages/core/sdk/src/shape-memory.ts @@ -0,0 +1,90 @@ +/** + * Muscle memory for tool outputs — the persistence half of runtime shape + * inference (see shape-inference.ts for the algorithm). + * + * Observed shapes live in the already-migrated `plugin_storage` table under a + * reserved system plugin id: owner-scoped, tenant-partitioned, and untouched + * by tool-catalog refresh (which deletes and recreates `tool` rows, so the + * tool row itself is not a viable home). An in-memory read-through cache + * keeps the hot path off the database: within one executor instance a tool's + * shape is loaded at most once, and a write happens only when a new + * observation actually changes the merged shape — after a few calls a stable + * API stops producing writes entirely. + * + * `observe` never fails and is intended to be forked off the dispatch path; + * `recall` degrades to "no memory" on any storage failure. + */ + +import { Clock, Effect } from "effect"; + +import type { Owner } from "./ids"; +import type { PluginStorageFacade } from "./plugin-storage"; +import { observeShape, type ObservedShape } from "./shape-inference"; + +/** Reserved system namespace inside `plugin_storage`; not a real plugin. */ +export const SHAPE_MEMORY_PLUGIN_ID = "executor.shape-memory"; +const COLLECTION = "observed-output-shapes"; + +export type ShapeMemory = { + /** + * Fold one successful tool payload into the tool's remembered shape. + * Structure only — values never leave this call. Never fails. + */ + readonly observe: (address: string, owner: Owner, value: unknown) => Effect.Effect; + /** The remembered shape for an address, or null when nothing is known. */ + readonly recall: (address: string, owner: Owner) => Effect.Effect; +}; + +export const makeShapeMemory = (storage: PluginStorageFacade): ShapeMemory => { + const cache = new Map(); + const persisted = new Map(); + + const cacheKey = (owner: Owner, address: string) => `${owner}:${address}`; + + const load = (address: string, owner: Owner): Effect.Effect => + Effect.gen(function* () { + const key = cacheKey(owner, address); + const hit = cache.get(key); + if (hit !== undefined) return hit; + const entry = yield* storage + .getForOwner({ owner, collection: COLLECTION, key: address }) + .pipe(Effect.catch(() => Effect.succeed(null))); + const record = entry?.data ?? null; + cache.set(key, record); + if (record !== null) persisted.set(key, JSON.stringify(record.schema)); + return record; + }); + + const observe = (address: string, owner: Owner, value: unknown): Effect.Effect => + Effect.gen(function* () { + const key = cacheKey(owner, address); + const prior = yield* load(address, owner); + const now = yield* Clock.currentTimeMillis; + const next = observeShape(prior, value, now); + cache.set(key, next); + // Write only when the merged shape actually changed — observation + // counters alone are bookkeeping, not worth a row write per call. + const schemaJson = JSON.stringify(next.schema); + if (persisted.get(key) === schemaJson) return; + yield* storage + .put({ owner, collection: COLLECTION, key: address, data: next }) + .pipe(Effect.catch(() => Effect.succeed(null))); + persisted.set(key, schemaJson); + }).pipe(Effect.catchCause(() => Effect.void)); + + return { + observe, + recall: (address, owner) => + load(address, owner).pipe(Effect.catchCause(() => Effect.succeed(null))), + }; +}; + +/** + * Render a remembered shape as the JSON Schema served in place of a missing + * declared output schema. The description marks provenance so a reader (and + * the schema view) can tell an observed shape from an author-declared one. + */ +export const observedShapeToJsonSchema = (record: ObservedShape): unknown => ({ + ...record.schema, + description: `Observed from ${record.observations} live response${record.observations === 1 ? "" : "s"}; fields may be incomplete.`, +}); diff --git a/packages/core/sdk/src/types.ts b/packages/core/sdk/src/types.ts index 852d409dbd..ad5d2bb0d4 100644 --- a/packages/core/sdk/src/types.ts +++ b/packages/core/sdk/src/types.ts @@ -21,6 +21,10 @@ export const ToolSchemaView = Schema.Struct({ description: Schema.optional(Schema.String), inputSchema: Schema.optional(Schema.Unknown), outputSchema: Schema.optional(Schema.Unknown), + // "observed" = runtime-inferred from live responses (muscle memory), served + // because the plugin declared no output schema. Absent = declared as-is. + outputSchemaSource: Schema.optional(Schema.Literals(["observed"])), + outputSchemaObservations: Schema.optional(Schema.Number), schemaDefinitions: Schema.optional(Schema.Record(Schema.String, Schema.Unknown)), inputTypeScript: Schema.optional(Schema.String), outputTypeScript: Schema.optional(Schema.String), From 9f8f47762d8849969f6f0828aa2bc0ba3edb9df7 Mon Sep 17 00:00:00 2001 From: Rhys Sullivan <39114868+RhysSullivan@users.noreply.github.com> Date: Wed, 26 Aug 2026 17:25:21 -0700 Subject: [PATCH 2/2] Mark observed types inline in the rendered signature --- apps/desktop/build/entitlements.mac.plist | 21 ------------------ apps/desktop/build/icon.png | Bin 64565 -> 0 bytes .../core/execution/src/tool-invoker.test.ts | 5 +++-- packages/core/execution/src/tool-invoker.ts | 13 ++++++++--- 4 files changed, 13 insertions(+), 26 deletions(-) delete mode 100644 apps/desktop/build/entitlements.mac.plist delete mode 100644 apps/desktop/build/icon.png diff --git a/apps/desktop/build/entitlements.mac.plist b/apps/desktop/build/entitlements.mac.plist deleted file mode 100644 index 043ecf496e..0000000000 --- a/apps/desktop/build/entitlements.mac.plist +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - com.apple.security.cs.allow-jit - - com.apple.security.cs.allow-unsigned-executable-memory - - - com.apple.security.cs.allow-dyld-environment-variables - - - com.apple.security.cs.disable-library-validation - - - diff --git a/apps/desktop/build/icon.png b/apps/desktop/build/icon.png deleted file mode 100644 index 48dadbfc58c84ce613e5463107abc69f4430e587..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 64565 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 { args: { owner: "executor", repo: "executor" }, }); - // Warm: the live `[]` payload becomes the served type, marked observed. + // Warm: the live `[]` payload becomes the served type, marked observed + // both inline and via the note. const warm = yield* describeTool(executor, "github.org.main.listRepositoryIssues"); expect(warm.outputTypeScript).toBe( - "{ ok: true; data: unknown[]; http?: ToolHttpMeta } | { ok: false; error: ToolError }", + "{ ok: true; data: unknown[] /* observed; may be incomplete */; http?: ToolHttpMeta } | { ok: false; error: ToolError }", ); expect(warm.outputTypeScriptNote).toContain("observed from 1 live response"); }), diff --git a/packages/core/execution/src/tool-invoker.ts b/packages/core/execution/src/tool-invoker.ts index f47235401b..2df47644ed 100644 --- a/packages/core/execution/src/tool-invoker.ts +++ b/packages/core/execution/src/tool-invoker.ts @@ -31,8 +31,12 @@ const TOOL_HTTP_META_TYPESCRIPT = "{ status: number; headers: { [k: string]: str const TOOL_FILE_TYPESCRIPT = '{ _tag: "ToolFile"; name?: string; mimeType: string; encoding: "base64"; data: string; byteLength: number; }'; -const wrapOutputTypeScript = (outputTypeScript?: string): string => - `{ ok: true; data: ${outputTypeScript ?? "unknown"}; http?: ToolHttpMeta } | { ok: false; error: ToolError }`; +const wrapOutputTypeScript = (outputTypeScript?: string, marker?: string): string => + `{ ok: true; data: ${outputTypeScript ?? "unknown"}${marker ?? ""}; http?: ToolHttpMeta } | { ok: false; error: ToolError }`; + +/** Inline provenance for observed types — a model that copies only the type + * string still sees the hint, since the compact render drops descriptions. */ +const OBSERVED_TYPE_MARKER = " /* observed; may be incomplete */"; const withToolResultDefinitions = ( definitions?: Record, @@ -866,7 +870,10 @@ export const describeTool = Effect.fn("executor.tools.describe")(function* ( name: schema.name ?? path, description: schema.description, inputTypeScript: schema.inputTypeScript, - outputTypeScript: wrapOutputTypeScript(schema.outputTypeScript), + outputTypeScript: wrapOutputTypeScript( + schema.outputTypeScript, + schema.outputSchemaSource === "observed" ? OBSERVED_TYPE_MARKER : undefined, + ), // The compact TS render drops the schema's provenance description, so an // observed (runtime-inferred) shape gets an explicit note: the model // should treat the fields as reliable but not exhaustive.