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
5 changes: 5 additions & 0 deletions .changeset/remove-remaining-route-shapes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"agent-bundle": minor
---

Make `CompiledAgentRoute.resultSchemaState` required on every compiled route; require `{ kind: 'resource' }` for `agent-bundle/test` App and resource contract fixtures (bare `{}` rejected); stop reporting AB4840 for removed `before`/`preflight` event exports. (#840)
6 changes: 3 additions & 3 deletions docs/diagnostics.md
Original file line number Diff line number Diff line change
Expand Up @@ -1153,8 +1153,8 @@ compile has no correct partial output, so every finding is an error
An event route uses a `.ts` lightweight handler or a `.tsx` rendered handler. A `.ts`
handler can return `ctx.render('./name.view.js', data)` to load its separate `.view.tsx`
sibling. Data must be strict JSON. The compiler uses explicit module boundaries without
extracting closures. Removed `before`/`preflight` exports, invalid view modules, and event
helper/path mismatches report `AB4840`. Definition helpers require a direct default call
extracting closures. Invalid view modules and event helper/path mismatches report
`AB4840`. Definition helpers require a direct default call
with an inline object literal; computed options and wrappers report `AB4810`.

Providers load on first `context.provider('<key>')` access. Concurrent readers share the
Expand Down Expand Up @@ -1203,7 +1203,7 @@ projections; no static provider subset declaration is required.
| `AB4837` | error | A compiled executable imports a compiler-carrying framework entry (`agent-bundle`, `/api`, `/config`, `/eval`, `/rstest`, `/test`, or `/test/browser`). The build checks requests after transformation and resolved package export identities, including aliases, and names the importing module. Type-only imports erased by the configured transform are legal. Source-only `inspect` and `validate` do not prove this dependency boundary. Keep compiler calls in a host process, or use `import type` for framework types. |
| `AB4838` | none | Retired. Schema imports are resolved by the bundler, not an inspection-time interpreter. |
| `AB4839` | none | Retired. Inspection does not follow schema alias chains. |
| `AB4840` | error | An event exports removed `before` or `preflight` bindings, its helper disagrees with the conventional path, or its `.view.tsx` sibling is not a valid rendered event module. Use a `.ts` handler and `ctx.render('./name.view.js', data)` for an explicit rendered view. |
| `AB4840` | error | An event helper disagrees with the conventional path, its `.view.tsx` sibling exports `config`, or its `.view.tsx` sibling is not a valid rendered event module. Use a `.ts` handler and `ctx.render('./name.view.js', data)` for an explicit rendered view. |
| `AB4843` | error | A `.cli.{ts,tsx}` module under `src/mcp/<server>/tools/` has no sibling tool route `<stem>.{ts,tsx}` (orphan), a `.cli.{ts,tsx}` module sits under `resources/`, `prompts/`, or `apps/`, or a second projection module (`<stem>.cli.ts` beside `<stem>.cli.tsx`) names the same tool, the first in path order wins and the second is reported. The suffix is reserved under `src/mcp/**` only. The message is `CLI projection <module> for tool:<server>/<tool>: <detail>.` (`has no sibling tool route …`, `<other module> already projects this tool …`); a misplaced module names no tool, so its message is `CLI projection <module>: sits under resources/, prompts/, or apps/ …`. `sourcePath` is the projection module's absolute path. Recovery: rename the file to match the sibling tool, or prefix `_` to park it, then inspect again. It is an error because a projection that cannot compile has no correct partial output. |
| `AB4844` | error | A CLI projection config does not satisfy its closed metadata contract, combines JSON input mode with flag mapping, or relaxes a required key without declaring `mapInput`. The generated runtime checks that a loaded `mapInput` is callable, awaits it, and validates the result through the original input schema. |
| `AB4845` | error | A CLI projection's grammar does not bind to the tool's contract: `flags`/`positionals` name a key absent from the tool's `RouteContract.input`; a `name`/alias is not kebab-case, is reserved (`help`, `json`, `ndjson`, `version`, and `yes` when confirm), or collides with another option's spelling or alias; `flags.<key>.name` or `flags.<key>.aliases` is declared on a key `positionals` consumes as a bare argument (`description`, `default`, and `required: false` still apply there); the tool's contract has a key `yes` while the command confirms, the shell keys parsed values by canonical key and strips `yes` as the confirmation, so no `name` override reaches the tool (`set confirm: false or rename the key`); or a `command` segment is not a safe identity segment. The message is `CLI projection <module> for tool:<server>/<tool>: <detail>.` and `sourcePath` is the projection module's absolute path. Recovery names the offending key or spelling and the accepted form, then says to inspect again. It is an error because a projection that cannot compile has no correct partial output. |
Expand Down
2 changes: 1 addition & 1 deletion packages/agent-bundle/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -912,7 +912,7 @@ registers app resources, `packed-stdio`, `packed-deleted-source`,
compiled `ui://` URI in `listResources` and `sweep` reads that resource. With
the default `apps: 'auto'` an app route needs no fixture entry: `coverage`
passes with a reason naming the auto-covered sweep. An explicit
`{ kind: 'resource' }` (or legacy `{}`) entry is always accepted, and
`{ kind: 'resource' }` entry is always accepted, and
Comment thread
ScriptedAlchemy marked this conversation as resolved.
`apps: 'explicit'` makes a missing app entry a `coverage` failure again. At
`mcp-in-memory` apps are never registered, so `apps` has no effect there.

Expand Down
1 change: 1 addition & 0 deletions packages/agent-bundle/src/build/entries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,7 @@ export const planScriptsSurface = async (
id: rendered.routeId,
kind: 'script',
provenance: { kind: 'conventional', relativePath: `scripts/${name}` },
resultSchemaState: 'unknown',
source,
}],
...(options.noticeRetention === undefined ? {} : { noticeRetention: options.noticeRetention }),
Expand Down
8 changes: 6 additions & 2 deletions packages/agent-bundle/src/dev/routes/application-tree.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ export interface ApplicationLeaf {
readonly label: string;
readonly handler?: string;
readonly ref: ApplicationNodeRef;
readonly resultSchemaState?: RouteManifestResultSchemaState;
readonly resultSchemaState: RouteManifestResultSchemaState;
readonly routeId?: string;
readonly source?: string;
}
Expand Down Expand Up @@ -179,7 +179,7 @@ const leafForRoute = (
label: routeLabel(ref),
...(route.execution?.handler === undefined ? {} : { handler: route.execution.handler }),
ref,
...(route.resultSchemaState === undefined ? {} : { resultSchemaState: route.resultSchemaState }),
resultSchemaState: route.resultSchemaState,
routeId: route.id,
source: route.source,
});
Expand Down Expand Up @@ -269,6 +269,7 @@ const configuredHookLeaves = (
key,
label: hook.event,
ref,
resultSchemaState: 'absent',
source: hook.path,
}));
}
Expand All @@ -291,6 +292,7 @@ const configuredScriptLeaves = (
key,
label: script.name,
ref,
resultSchemaState: 'absent',
...(script.file === undefined ? {} : { source: script.file.path }),
}));
}
Expand All @@ -306,6 +308,7 @@ const skillLeaves = (skills: readonly ApplicationTreeSkill[]): readonly Applicat
key: applicationNodeKey(ref),
label: skill.label,
ref,
resultSchemaState: 'absent' as const,
...(skill.source === undefined ? {} : { source: skill.source }),
});
}));
Expand All @@ -321,6 +324,7 @@ const staticDocumentLeaves = (
key: applicationNodeKey(ref),
label: document.name,
ref,
resultSchemaState: 'absent' as const,
source: document.provenance.sourcePath,
});
}));
Expand Down
6 changes: 3 additions & 3 deletions packages/agent-bundle/src/dev/routes/route-manifest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,8 +65,8 @@ export type RouteManifestContract = ArtifactManifestRouteContract;
*/
export interface RouteManifestRoute extends ArtifactManifestRoute {
readonly config: readonly RouteManifestConfigEntry[];
/** Static declaration/projection evidence; absent only on manifests from older dev servers. */
readonly resultSchemaState?: RouteManifestResultSchemaState;
/** Static declaration/projection evidence from the compiler pass. */
readonly resultSchemaState: RouteManifestResultSchemaState;
}

/** Result schemas execute as authored, so a declaration is known without inventing a static schema projection. */
Expand Down Expand Up @@ -164,7 +164,7 @@ const configSummary = (config: Readonly<Record<string, unknown>>): readonly Rout
const manifestRoute = (route: CompiledAgentRoute): RouteManifestRoute => ({
...artifactRouteFor(route),
config: configSummary(route.config),
...(route.resultSchemaState === undefined ? {} : { resultSchemaState: route.resultSchemaState }),
resultSchemaState: route.resultSchemaState,
});

const manifestServer = (server: CompiledServerSurface): RouteManifestServer => ({
Expand Down
7 changes: 2 additions & 5 deletions packages/agent-bundle/src/routes/event-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,21 +9,18 @@ export const eventHandlerEntry = (
relativePath: string,
sourcePath: string,
): CompiledEventHandler | undefined => {
const { named, definition } = scanRouteModuleExports(text, relativePath);
const { definition } = scanRouteModuleExports(text, relativePath);
if (definition?.event !== undefined) {
const expected = relativePath.match(/(?:^|\/)src\/events\/(.+)\.tsx?$/u)?.[1];
if (expected !== definition.event) throw new TypeError(`Event definition ${definition.event} disagrees with conventional path ${relativePath}.`);
}
if (named.has('preflight') || named.has('before')) {
throw new TypeError(`Event ${relativePath} must use a .ts handler and ctx.render('./${relativePath.split('/').at(-1)!.replace(/\.tsx?$/u, '.view.js')}', data); before/preflight exports are no longer supported.`);
}
if (!relativePath.endsWith('.ts')) return undefined;
const view = sourcePath.replace(/\.ts$/u, '.view.tsx');
const hasView = existsSync(view);
if (hasView) {
const viewText = readFileSync(view, 'utf8');
const viewExports = scanRouteModuleExports(viewText, view).named;
if (viewExports.has('config') || viewExports.has('preflight') || viewExports.has('before')) {
if (viewExports.has('config')) {
throw new TypeError(`Keep event configuration and control flow in ${relativePath}; ${view} only renders JSX.`);
}
const diagnostics = validateEventRouteModuleContract(viewText, relativePath.replace(/\.ts$/u, '.view.tsx'), view);
Expand Down
4 changes: 2 additions & 2 deletions packages/agent-bundle/src/routes/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -145,8 +145,8 @@ export interface CompiledAgentRoute {
/** Static cheap gate; present only on event routes that declare a valid relative default re-export. */
readonly handler?: CompiledEventHandler;
readonly provenance: RouteProvenance;
/** Omitted only by legacy or manually assembled graphs, where consumers must treat the declaration as unknown. */
readonly resultSchemaState?: RouteResultSchemaState;
/** Static declaration evidence: absent, unknown (module text unavailable), or unprojectable (named `resultSchema`). */
readonly resultSchemaState: RouteResultSchemaState;
/** The owning MCP server id (`mcp:<name>`); MCP route kinds only. */
readonly serverId?: string;
/** Absolute route module path. */
Expand Down
11 changes: 8 additions & 3 deletions packages/agent-bundle/src/test/contract.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
* `ui://` URI in `listResources`, and `sweep` reads that resource. With the
* default `apps: 'auto'` an app route needs no fixture entry — `coverage`
* passes with a reason naming the auto-covered sweep. An explicit
* `{ kind: 'resource' }` (or legacy `{}`) fixture is always accepted;
* `{ kind: 'resource' }` fixture is always accepted;
* `apps: 'explicit'` restores the requirement that every app route be listed.
*
* Stateful lifecycle fixtures replay over one open client at every boundary.
Expand Down Expand Up @@ -161,8 +161,7 @@ export interface ContractResourceFixture {
export interface ContractRouteFixture<Input = unknown> {
/**
* `'resource'` marks a resource/MCP App fixture (see `ContractResourceFixture`).
* Omit it for tool and prompt fixtures; a legacy `{}` still covers a
* resource or app route.
* Required for resource and app routes; omit it for tool and prompt fixtures.
*/
readonly kind?: ContractResourceFixture['kind'];
/** Valid input for the sweep invocation (tools/prompts; resources need none). */
Expand Down Expand Up @@ -1578,6 +1577,12 @@ const resolveRouteFixture = (
fixture: undefined,
};
}
if ((descriptor.kind === 'resource' || descriptor.kind === 'app') && fixture.kind !== 'resource') {
return {
coverage: failed(`resource/app route fixture must be { kind: "resource" }`),
fixture: undefined,
};
}
return { coverage: passed(), fixture };
};

Expand Down
1 change: 1 addition & 0 deletions packages/agent-bundle/tests/application-tree.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ const route = (
id,
kind,
provenance: { kind: 'conventional' },
resultSchemaState: kind === 'event-route' || kind === 'app' ? 'absent' : 'unprojectable',
source,
...extra,
});
Expand Down
1 change: 1 addition & 0 deletions packages/agent-bundle/tests/cli-projection.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -388,6 +388,7 @@ describe('MCP tool CLI surface projections', () => {
id: 'tool:demo/submit',
kind: 'tool',
provenance: { kind: 'conventional', relativePath: toolPath },
resultSchemaState: 'unprojectable' as const,
serverId: 'mcp:demo',
source: '/project/src/mcp/demo/tools/submit.tsx',
};
Expand Down
1 change: 1 addition & 0 deletions packages/agent-bundle/tests/cli-routes.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -620,6 +620,7 @@ describe('generated bin normalization', () => {
id: 'cli:inspect',
kind: 'cli',
provenance: { kind: 'conventional', relativePath: 'src/cli/inspect.ts' },
resultSchemaState: 'unprojectable' as const,
source: '/project/src/cli/inspect.ts',
}],
...overrides,
Expand Down
Loading
Loading