Skip to content
Closed
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
105 changes: 104 additions & 1 deletion e2e/scenarios/shape-memory.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,15 @@ import { createServer } from "node:http";
import { expect } from "@effect/vitest";
import { Effect } from "effect";
import { composePluginApi } from "@executor-js/api/server";
import { mcpHttpPlugin } from "@executor-js/plugin-mcp/api";
import { makeUndeclaredStructuredMcpServer, serveMcpServer } from "@executor-js/plugin-mcp/testing";
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);
const api = composePluginApi([openApiHttpPlugin(), mcpHttpPlugin()] as const);

/** One GET operation whose 200 declares no response schema — the shape the
* model would otherwise have to guess. */
Expand Down Expand Up @@ -176,3 +178,104 @@ return { ok: result.ok };
}),
),
);

// ---------------------------------------------------------------------------
// The MCP variant — the Blacksmith post's exact case. MCP tools always carry
// a declared CallToolResult envelope, but when the server declares no output
// schema its `structuredContent` slot is an untyped placeholder. One live
// call splices the observed payload shape into exactly that slot.
// ---------------------------------------------------------------------------

const mcpDescribeCode = (slug: string) => `
const details = await tools.describe.tool({ path: "${slug}.org.main.undeclared_structured_echo" });
return {
outputTypeScript: details.outputTypeScript ?? null,
note: details.outputTypeScriptNote ?? null,
error: details.error ?? null,
};
`;

scenario(
"Muscle memory · an MCP server's undeclared structuredContent 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_mcp_${randomBytes(4).toString("hex")}`);

const server = yield* serveMcpServer(makeUndeclaredStructuredMcpServer);

yield* client.mcp.addServer({
payload: {
transport: "remote",
name: "Undeclared structured MCP",
endpoint: server.url,
slug,
},
});

yield* Effect.ensuring(
Effect.gen(function* () {
yield* client.connections.create({
payload: {
owner: "org",
name: ConnectionName.make("main"),
integration: slug,
template: AuthTemplateSlug.make("none"),
value: "",
},
});

const describe = Effect.gen(function* () {
const executed = yield* client.executions.execute({
payload: { code: mcpDescribeCode(String(slug)), autoApprove: true },
});
expect(executed.status, executed.text).toBe("completed");
return JSON.parse(executed.text) as DescribeOutcome;
});

// Cold: the declared envelope is served, but its structuredContent
// slot carries no payload type and nothing claims to be observed.
const cold = yield* describe;
expect(cold.error, "the tool resolves").toBeNull();
expect(cold.outputTypeScript, "the envelope is declared").toContain("structuredContent");
expect(cold.outputTypeScript, "the payload is untyped cold").not.toContain(
"length: number",
);
expect(cold.note, "cold describe carries no provenance note").toBeNull();

// One real call teaches the payload shape.
const invoked = yield* client.executions.execute({
payload: {
code: `
const result = await tools.${slug}.org.main.undeclared_structured_echo({ value: "hi" });
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 fills exactly the structuredContent slot.
const warm = yield* describe;
expect(warm.outputTypeScript, "the payload type is served").toContain("length: number");
expect(warm.outputTypeScript, "the payload type is served").toContain("value: string");
expect(warm.outputTypeScript, "the declared envelope survives").toContain("content");
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.mcp.removeServer({ params: { slug } }).pipe(Effect.ignore);
}),
);
}),
),
);
40 changes: 27 additions & 13 deletions packages/core/sdk/src/executor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,13 @@ import {
} from "./oauth-ema";
import { connectionIdentifier } from "./connection-name-identifier";
import { annotateToolResultOutcome, isToolResult } from "./tool-result";
import { makeShapeMemory, observedShapeToJsonSchema, SHAPE_MEMORY_PLUGIN_ID } from "./shape-memory";
import {
hasShapeSlots,
makeShapeMemory,
observedShapeToJsonSchema,
SHAPE_MEMORY_PLUGIN_ID,
spliceObservedSlots,
} from "./shape-memory";
import { isUnauthorizedToolFailure } from "./auth-tool-failure";

const PLUGIN_STORAGE_DELETE_KEY_BATCH_SIZE = 90;
Expand Down Expand Up @@ -4066,20 +4072,28 @@ export const createExecutor = <const TPlugins extends readonly AnyPlugin[] = rea
? projected.outputSchema
: tool.outputSchema;

// Muscle memory: when neither the catalog row nor the plugin's
// projection declares an output schema, serve the shape observed from
// live responses instead of letting the type collapse to `unknown`.
// The schema's description marks it as observed.
// Muscle memory serve paths: a wholly-undeclared output schema is
// replaced by the observed shape; a declared schema carrying marked
// placeholder slots (e.g. the MCP result envelope's synthesized
// `structuredContent`) gets the observed counterpart spliced into
// exactly those slots, keeping the declared structure around them.
const slotted = outputSchema !== undefined && hasShapeSlots(outputSchema);
const observed =
outputSchema === undefined
outputSchema === undefined || slotted
? yield* shapeMemory.recall(String(address), parsed.owner)
: null;
const effectiveOutputSchema =
outputSchema !== undefined
? outputSchema
: observed !== null
? observedShapeToJsonSchema(observed)
: undefined;
let effectiveOutputSchema = outputSchema;
let observedServed = false;
if (outputSchema === undefined) {
if (observed !== null) {
effectiveOutputSchema = observedShapeToJsonSchema(observed);
observedServed = true;
}
} else if (slotted) {
const spliced = spliceObservedSlots(outputSchema, observed?.schema ?? null);
effectiveOutputSchema = spliced.schema;
observedServed = spliced.filled > 0;
}

const definitionRows = yield* core.findMany("definition", {
where: (b: AnyCb) =>
Expand Down Expand Up @@ -4111,7 +4125,7 @@ export const createExecutor = <const TPlugins extends readonly AnyPlugin[] = rea
description: tool.description,
inputSchema,
outputSchema: effectiveOutputSchema,
...(observed !== null
...(observedServed && observed !== null
? {
outputSchemaSource: "observed" as const,
outputSchemaObservations: observed.observations,
Expand Down
1 change: 1 addition & 0 deletions packages/core/sdk/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -433,6 +433,7 @@ export {

// The one TS-preview generator plugins assert against.
export { buildToolTypeScriptPreview } from "./schema-types";
export { SHAPE_SLOT_KEY } from "./shape-memory";

// Wire-level HTTP error schemas usable by plugin HttpApiGroup definitions.
export { InternalError } from "./api-errors";
Expand Down
85 changes: 85 additions & 0 deletions packages/core/sdk/src/shape-memory.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import { describe, expect, it } from "@effect/vitest";

import { inferShape } from "./shape-inference";
import { hasShapeSlots, SHAPE_SLOT_KEY, spliceObservedSlots } from "./shape-memory";

// The MCP CallToolResult envelope shape: declared structure around one
// synthesized placeholder slot.
const envelope = {
type: "object",
properties: {
content: { type: "array", items: { type: "object" } },
structuredContent: { type: "object", [SHAPE_SLOT_KEY]: true },
isError: { const: false },
},
required: ["content"],
};

describe("hasShapeSlots", () => {
it("finds a marked slot nested in properties", () => {
expect(hasShapeSlots(envelope)).toBe(true);
});

it("reports false for unmarked schemas", () => {
expect(hasShapeSlots({ type: "object", properties: { a: { type: "string" } } })).toBe(false);
expect(hasShapeSlots(undefined)).toBe(false);
});
});

describe("spliceObservedSlots", () => {
it("fills the marked slot from the observed counterpart and strips the marker", () => {
const observed = inferShape({
content: [{ type: "text", text: "x" }],
structuredContent: { value: "x", length: 1, ok: true },
});
const { schema, filled } = spliceObservedSlots(envelope, observed);
expect(filled).toBe(1);
const out = schema as { properties: Record<string, Record<string, unknown>> };
expect(out.properties["structuredContent"]?.[SHAPE_SLOT_KEY]).toBeUndefined();
expect(out.properties["structuredContent"]).toMatchObject({
type: "object",
properties: {
value: { type: "string" },
length: { type: "number" },
ok: { type: "boolean" },
},
});
// The declared structure around the slot is untouched.
expect(out.properties["content"]).toEqual(envelope.properties.content);
expect(out.properties["isError"]).toEqual(envelope.properties.isError);
});

it("keeps the placeholder and reports zero when the observation lacks the slot", () => {
const observed = inferShape({ content: [{ type: "text", text: "x" }] });
const { schema, filled } = spliceObservedSlots(envelope, observed);
expect(filled).toBe(0);
const out = schema as { properties: Record<string, Record<string, unknown>> };
expect(out.properties["structuredContent"]?.[SHAPE_SLOT_KEY]).toBeUndefined();
expect(out.properties["structuredContent"]?.["type"]).toBe("object");
});

it("strips markers even with no observation at all", () => {
const { schema, filled } = spliceObservedSlots(envelope, null);
expect(filled).toBe(0);
expect(JSON.stringify(schema)).not.toContain(SHAPE_SLOT_KEY);
});

it("descends through items to reach nested slots", () => {
const declared = {
type: "object",
properties: {
rows: { type: "array", items: { type: "object", [SHAPE_SLOT_KEY]: true } },
},
};
const observed = inferShape({ rows: [{ id: 7 }] });
const { schema, filled } = spliceObservedSlots(declared, observed);
expect(filled).toBe(1);
const out = schema as {
properties: { rows: { items: Record<string, unknown> } };
};
expect(out.properties.rows.items).toMatchObject({
type: "object",
properties: { id: { type: "number" } },
});
});
});
91 changes: 90 additions & 1 deletion packages/core/sdk/src/shape-memory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import { Clock, Effect } from "effect";

import type { Owner } from "./ids";
import type { PluginStorageFacade } from "./plugin-storage";
import { observeShape, type ObservedShape } from "./shape-inference";
import { observeShape, type InferredShape, type ObservedShape } from "./shape-inference";

/** Reserved system namespace inside `plugin_storage`; not a real plugin. */
export const SHAPE_MEMORY_PLUGIN_ID = "executor.shape-memory";
Expand Down Expand Up @@ -88,3 +88,92 @@ export const observedShapeToJsonSchema = (record: ObservedShape): unknown => ({
...record.schema,
description: `Observed from ${record.observations} live response${record.observations === 1 ? "" : "s"}; fields may be incomplete.`,
});

// ---------------------------------------------------------------------------
// Placeholder slots — partial serving inside a DECLARED schema.
//
// Some plugins must declare an output schema even when the upstream said
// nothing about the payload: the MCP plugin's CallToolResult envelope is
// genuinely declared (content blocks, isError), but its `structuredContent`
// slot is a synthesized "some object" placeholder whenever the server
// declared no output schema. A plugin marks such a slot with
// `SHAPE_SLOT_KEY: true` (typically at projection time), and serving splices
// the observed shape's counterpart into exactly that slot, keeping the
// declared structure around it.
// ---------------------------------------------------------------------------

/** Vendor-extension marker a plugin puts on a placeholder subschema. */
export const SHAPE_SLOT_KEY = "x-executor-shape-slot";

const OBSERVED_SLOT_DESCRIPTION = "Observed from live responses; fields may be incomplete.";

/** Slot scanning/splicing depth bound — marked slots live near the root. */
const MAX_SLOT_DEPTH = 8;

type SchemaNode = Record<string, unknown>;

const isSchemaNode = (value: unknown): value is SchemaNode =>
typeof value === "object" && value !== null && !Array.isArray(value);

export const hasShapeSlots = (schema: unknown, depth = 0): boolean => {
if (!isSchemaNode(schema) || depth >= MAX_SLOT_DEPTH) return false;
if (schema[SHAPE_SLOT_KEY] === true) return true;
const properties = schema["properties"];
if (
isSchemaNode(properties) &&
Object.values(properties).some((child) => hasShapeSlots(child, depth + 1))
) {
return true;
}
return hasShapeSlots(schema["items"], depth + 1);
};

const isInformativeShape = (shape: InferredShape): boolean =>
shape.type !== undefined || shape.anyOf !== undefined;

/**
* Replace marked placeholder slots in a declared schema with the observed
* shape's counterpart at the same path (descending `properties` by name and
* `items`). Slots with no informative observed counterpart keep their
* declared placeholder; markers are stripped either way so they never reach
* schema consumers. `filled` reports how many slots actually got a shape.
*/
export const spliceObservedSlots = (
declared: unknown,
observed: InferredShape | null,
): { readonly schema: unknown; readonly filled: number } => {
let filled = 0;
const walk = (node: unknown, shape: InferredShape | null, depth: number): unknown => {
if (!isSchemaNode(node) || depth >= MAX_SLOT_DEPTH) return node;
if (node[SHAPE_SLOT_KEY] === true) {
const { [SHAPE_SLOT_KEY]: _slot, ...placeholder } = node;
if (shape !== null && isInformativeShape(shape)) {
filled += 1;
return { ...shape, description: OBSERVED_SLOT_DESCRIPTION };
}
return placeholder;
}
let next: SchemaNode = node;
const properties = node["properties"];
if (isSchemaNode(properties)) {
const walkedProperties: Record<string, unknown> = {};
let changed = false;
for (const [key, child] of Object.entries(properties)) {
const counterpart = shape?.type === "object" ? (shape.properties?.[key] ?? null) : null;
const walked = walk(child, counterpart, depth + 1);
walkedProperties[key] = walked;
if (walked !== child) changed = true;
}
if (changed) next = { ...next, properties: walkedProperties };
}
const items = node["items"];
if (items !== undefined) {
const counterpart = shape?.type === "array" ? (shape.items ?? null) : null;
const walked = walk(items, counterpart, depth + 1);
if (walked !== items) next = { ...next, items: walked };
}
return next;
};
const schema = walk(declared, observed, 0);
return { schema, filled };
};
Loading
Loading