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
7 changes: 7 additions & 0 deletions .changeset/748-752-route-caller-input-types.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"agent-bundle": minor
"@agent-bundle/runtime": minor
"create-agent-bundle": patch
---

Type route callers by schema input and route components by schema output in the generated `.agent-bundle/routes.d.ts`: `createAppClient().call`, `onToolInput`, `renderRoute`, `invokeMcpTool`, and the contract matrix accept what a caller sends (a `.default()`ed field is optional, a `.transform()`ed field is spelled as the wire carries it), while `ToolRouteProps` keeps the parsed output. The registration carries both sides: `RegisteredRouteInput<Id>` and the new `RegisteredRouteParsedInput<Id>` (`@agent-bundle/runtime`), the latter typing `loadRouteModule(id).inputSchema.parse`; `agent-bundle/test` exports `RouteTargetParsedInput`. `getMcpPrompt` and `McpInvocationOptions` type their `input` as the caller's side too. A structural schema declaring only `_output` uses it for both. `renderRoute` now parses its input through the route's own `inputSchema` — for MCP and `cli:` routes alike — before the component runs and fails with an `invalid-input` harness error on rejected input. `agent-bundle validate` reports `AB4834` once per TypeScript program that imports `agent-bundle/app`, `agent-bundle/test`, `agent-bundle/eval`, or `@agent-bundle/runtime` and omits the generated declaration — following `references` transitively — instead of accepting any one referenced program that includes it. The `mcp-server` and `cli-tool` starters run `agent-bundle validate` inside `npm run typecheck`, so a clean checkout type-checks against current route declarations. (#757)
22 changes: 14 additions & 8 deletions docs/diagnostics.md
Original file line number Diff line number Diff line change
Expand Up @@ -1072,13 +1072,19 @@ it: `create-agent-bundle` templates and the `examples/*` projects list
because `**/*` never descends into dot-directories), while the file itself
stays gitignored. After publishing the declaration, `agent-bundle validate`
resolves the root `tsconfig.json` program the way `tsc -p` does — `extends`,
`files`, `include`, `exclude`, and one level of `references` for a
solution-style root — and reports `AB4834` (a **warning**, surfaced by
`validate` only) when the published file is not among its root files. A
project with no root `tsconfig.json`, no published declaration (route-free
and provider-free), or a `tsconfig.json` TypeScript cannot parse gets no
diagnostic: there is no program to be missing from, or `tsc` already
reports the parse failure itself.
`files`, `include`, `exclude`, then the modules those roots import, so a
narrow `files: ["src/index.ts"]` still reaches the consumer it imports — and
every program it `references`, transitively, and reports `AB4834` (a
**warning**, surfaced by `validate` only) once per program that *consumes*
the registration but does not compile the published file. A program consumes
it when one of the project's files in it imports `agent-bundle/app`, `agent-bundle/test`, `agent-bundle/eval`, or
`@agent-bundle/runtime`; a build-only project that imports none of them is
left alone, and a solution whose server project includes the file cannot
hide a browser project that omits it. A project with no root
`tsconfig.json`, no published declaration (route-free and provider-free), or
a `tsconfig.json` TypeScript cannot parse gets no diagnostic: there is no
program to be missing from, or `tsc` already reports the parse failure
itself.

Conventional `src/scripts/` routes ship through the same pipeline as
explicit `scripts` entries (#102 stage 1): a plain module directly under
Expand Down Expand Up @@ -1312,7 +1318,7 @@ resolving a provider set the author did not write.
| `AB4831` | error | Two layout modules declare one layout scope (for example `src/layout.ts` beside `src/layout.tsx`). Keep exactly one module per scope. |
| `AB4832` | error | A server layout (`src/mcp/<server>/layout.*`) names an MCP server that declares no tool, resource, or prompt route modules — the server directory is missing or holds only `apps/` routes, which never take a layout. Add routes under that server directory, move the layout, or rename it `_layout.*` to opt out. A server pinned to `custom`, `command`, or `remote` via `routes.servers.<server>` is skipped entirely: its layout is neither validated (`AB4830`) nor retained, because no generated worker composes it. |
| `AB4833` | error | `notices.retention` is malformed: `notices` or `retention` is not an object, carries an unknown key, `terminalTtl` is not a positive integer of milliseconds or a duration such as `"7d"`, `"12h"`, `"30m"`, or `"90s"`, `maxTerminal` / `maxJournalBytes` is not a positive integer — or the policy is declared by a project without a conventional `src/state.ts`, which has no co-mounted notice ledger to retain. Omit a field to keep the runtime default (`7d`, `500`, `16777216`). |
| `AB4834` | warning | `agent-bundle validate` published `.agent-bundle/routes.d.ts` (the project compiles routes or providers) but the root `tsconfig.json` program — resolved like `tsc -p`, including `extends` and one level of project `references` — does not compile it, so `renderRoute` / `renderRouteEvents` type-check route ids as `string` and `input` / `result` as `unknown`. Reported on `tsconfig.json`; never for a project without one. Add `".agent-bundle/routes.d.ts"` to `tsconfig.json` `include` (not `files`: an `include` entry is inert until the first build publishes the file, while a missing `files` entry is a `tsc` error); `build`, `dev`, and `validate` keep the file current and it stays gitignored. |
| `AB4834` | warning | `agent-bundle validate` published `.agent-bundle/routes.d.ts` (the project compiles routes or providers) but a TypeScript program that consumes the registration — the root `tsconfig.json` or any project it `references`, transitively, resolved like `tsc -p` with `extends`, whose source imports `agent-bundle/app`, `agent-bundle/test`, `agent-bundle/eval`, or `@agent-bundle/runtime` — does not compile it, so that program type-checks route ids as `string` and `input` / `result` / provider values as `unknown`. Reported once per such program, on its tsconfig; never for a program that imports none of those modules, nor for a project without a root `tsconfig.json`. Add the file to that tsconfig's `include`, spelled relative to that tsconfig (not `files`: an `include` entry is inert until the first `validate` publishes the file, while a missing `files` entry is a `tsc` error); a tsconfig without its own `include` array must declare one that also lists the patterns it inherits or the `**/*` default, since an `include` array replaces them; `validate`, `build`, and `dev` keep the file current and it stays gitignored. |
| `AB4835` | error | A route's static `config.render` (the render budget of one call, #454) is malformed: `render` is not an object, carries a key other than `maxElapsedMs`, `maxElapsedMs` is not a positive integer of milliseconds, or it exceeds the framework ceiling of `86400000` (24 hours) — or a plain `.ts` CLI command declares one, although it executes without a render session. Reported once per route: on an MCP tool, resource, or prompt route with its server (the tool's projected CLI command inherits the value), or on a `src/cli/**` command route; a route with a rejected budget compiles no command. Omit `render` to keep the runtime default (`60000`). Declare `config.render = { maxElapsedMs: <positive integer ≤ 86400000> }` on a rendered route, or remove it. The budget bounds the framework's render session only: Codex's `tool_timeout_sec` (60 s by default) and any per-server host timeout must be raised by the operator separately, while Claude Code's default per-call wall clock is about 28 hours and its idle timer is kept alive by the `notifications/progress` the projector forwards. |
| `AB4836` | error | A route's static `config.execution` (MCP task support, #369) is malformed: `execution` is not an object, carries a key other than `taskSupport`, or `taskSupport` is not one of `forbidden`, `optional`, `required` — or a resource or prompt route declares it, although the `2025-11-25` Tasks utility augments `tools/call` only. Reported once per route with its server. Omit `execution` to keep the wire default (`forbidden`: every call is an ordinary request), or declare `config.execution = { taskSupport: 'optional' }` so a task-aware client may receive a `CreateTaskResult` and poll `tasks/get` / `tasks/result` while the render continues, or `'required'` to refuse ordinary calls with JSON-RPC `-32601`. The generated server advertises the value in `tools/list` and declares the `tasks` capability only when at least one tool opted in. |
| `AB4837` | error | A route module of any kind except an App — a `src/cli/**` command, a `src/scripts/**` script, a tool, resource, or prompt route of a generated server, an event route — a layout, or a provider, or a module one of them reaches through relative value imports, imports `agent-bundle`, `agent-bundle/api`, `agent-bundle/config`, `agent-bundle/eval`, `agent-bundle/rstest`, `agent-bundle/test`, or `agent-bundle/test/browser` as a value (a static import whose binding is read at run time, `import 'agent-bundle/api'`, `import('agent-bundle/api')` with a literal specifier, or a non-type re-export). Those entries carry the compiler, and the generated executable is self-contained (#387): the bundler would inline the compiler and fail on the framework's runtime-relative module references (`Module not found: Can't resolve '../events'`), or the artifact validator would reject the inlined compiler's non-literal dynamic imports with `AB6005` — either way naming a generated file instead of the route (#558). Judged statically when the route graph compiles, so `inspect`, `validate`, `build`, and `dev` all report it, once per module, naming the route and the helper the import lives in. `import type`, `type`-qualified specifiers, and imports used only in type positions are elided by the bundler and never reported; routes of a server that is not generated (`custom`/`command`/`remote`, or an `AB4800` conflict) or of a CLI that is not generated (`conventional`, or an `AB4801` conflict) are never bundled, so they are not judged; likewise a layout that no bundled rendered route composes through (a worker imports only the layouts its routes reach: the tool, resource, and prompt routes of a generated server, the rendered `.tsx` commands of a generated CLI, and rendered `.tsx` scripts), and a provider in a project whose only executables are plain `.ts` scripts, which are bundled from their own source and mount none. Keep framework calls in a host process: expose an MCP App with `web.apps` and open it from the installed artifact with `<plugin> web`; keep other framework calls in host processes (`package.json` scripts, a hand-written `.mjs` run from the checkout). The bundle-safe entries stay allowed: `agent-bundle/app` (the browser MCP App client, a leaf with no Zod, Node, or compiler import), `agent-bundle/routes`, `agent-bundle/launch-env`, `agent-bundle/meta`, `agent-bundle/mcp-apps`, `agent-bundle/mcp-entry`, `agent-bundle/cli-entry`, `agent-bundle/terminal-capability`, and `agent-bundle/web-host`. |
Expand Down
8 changes: 6 additions & 2 deletions docs/entry-conventions.md
Original file line number Diff line number Diff line change
Expand Up @@ -1814,8 +1814,12 @@ injected ports.
`generateRouteTypes` (`src/routes/typegen.ts`) emits, for a graph with at
least one tool route, `AppToolRouteId` (the `tool:<server>/<name>` subset of
`RouteId`), `AgentBundleAppRouteContracts` (one `{ input: RouteInput<Id>;
result: RouteResult<Id> }` per tool from the module's own `inputSchema` and
`resultSchema` output), and exactly one augmentation:
result: RouteResult<Id> }` per tool — `input` from the module's own
`inputSchema` *input* type, what a caller sends before the server parses it,
so a defaulted field is optional and a transformed field is spelled as the
wire carries it; `result` from the `resultSchema` output; a structural schema
declaring only `_output` uses it for both, #752), and exactly one
augmentation:

```ts
declare module 'agent-bundle/app' {
Expand Down
6 changes: 3 additions & 3 deletions examples/host-test/tests/route-unit/routes.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,15 +91,15 @@ it('compiles every canonical event family plus the MCP and CLI surfaces', () =>
});

it('holds the slow probe open for the requested time, reporting one progress tick per tickMs, and records the call', async () => {
const slow = await render('tool:host-test/slow', { holdMs: 120, tickMs: 40 });
const slow = await render('tool:host-test/slow', { holdMs: 150, tickMs: 50 });
expect(slow.document.value).toMatchObject({ ticks: 3 });
expect((slow.document.value as { heldMs: number }).heldMs).toBeGreaterThanOrEqual(100);
expect((slow.document.value as { heldMs: number }).heldMs).toBeGreaterThanOrEqual(130);
expect(slow.progress.map((update) => update.completed)).toEqual([1, 2, 3]);
// The probe is recorded like every other MCP call; the dump that reads it records itself too.
const dumped = await render('tool:host-test/dump', {});
expect(dumped.document.value).toMatchObject({
records: [
expect.objectContaining({ event: 'mcp:slow', kind: 'mcp', observed: { holdMs: 120, tickMs: 40, tool: 'slow' } }),
expect.objectContaining({ event: 'mcp:slow', kind: 'mcp', observed: { holdMs: 150, tickMs: 50, tool: 'slow' } }),
expect.objectContaining({ event: 'mcp:dump', kind: 'mcp' }),
],
});
Expand Down
2 changes: 2 additions & 0 deletions packages/agent-bundle/src/app/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,10 +62,12 @@ export type AppRouteId = unknown extends AppRoutes
? `tool:${string}/${string}`
: Extract<keyof AppRoutes, `tool:${string}/${string}`> & string;

/** What a caller sends for one route: the registered `inputSchema` input type (defaults optional, transforms as the wire carries them), before the server parses it. */
export type AppRouteInput<Id extends string> = Id extends keyof AppRoutes
? AppRoutes[Id] extends AppRouteContract ? AppRoutes[Id]['input'] : unknown
: unknown;

/** The structured result one route resolves with: the registered `resultSchema` output. */
export type AppRouteResult<Id extends string> = Id extends keyof AppRoutes
? AppRoutes[Id] extends AppRouteContract ? AppRoutes[Id]['result'] : unknown
: unknown;
Expand Down
Loading
Loading