From 2493cade86a2902f5458f4f1fe49a771fe28698b Mon Sep 17 00:00:00 2001 From: ScriptedAlchemy Date: Thu, 3 Sep 2026 07:14:19 +0000 Subject: [PATCH 1/3] fix(runtime,test,inspect): require provider fixtures where providers do not run; project only capability contract fields (#95, #100 review follow-ups) --- .changeset/required-provider-fixtures.md | 6 +++ docs/entry-conventions.md | 6 ++- docs/framework-mode.md | 6 ++- packages/agent-bundle/src/api.ts | 42 ++++++++++++++---- packages/agent-bundle/src/test/cli.ts | 15 ++++--- packages/agent-bundle/src/test/index.ts | 6 ++- packages/agent-bundle/src/test/mcp.ts | 26 +++++++---- packages/agent-bundle/src/test/render.ts | 31 +++++++++++-- packages/agent-bundle/tests/api.test.ts | 44 +++++++++++++++++++ .../tests/provider-typegen.test.ts | 37 ++++++++++++++++ packages/rsc-runtime/src/agent-request.ts | 18 +++++++- packages/rsc-runtime/src/plugin.ts | 2 + 12 files changed, 208 insertions(+), 31 deletions(-) create mode 100644 .changeset/required-provider-fixtures.md diff --git a/.changeset/required-provider-fixtures.md b/.changeset/required-provider-fixtures.md new file mode 100644 index 000000000..738df2668 --- /dev/null +++ b/.changeset/required-provider-fixtures.md @@ -0,0 +1,6 @@ +--- +"@agent-bundle/runtime": patch +"agent-bundle": patch +--- + +Require provider fixtures where providers are not executed. Once a project's generated `.agent-bundle/routes.d.ts` augmentation declares provider keys, `runAgentRequest`'s `providers`, the test harness `options` argument, and its `context.providers` become required (`AgentRequestProvidersInit`, `RenderRouteContextInit`, `HarnessOptionsArguments`), so a handler typed against `(await agent()).providers.` can never observe an unchecked `undefined` from a custom scope or a route-unit test. Provider-free projects are unchanged. `inspect` now projects only the four-state contract fields of adapter-owned capability rows into component accounting, so extension fields on JavaScript or third-party adapters cannot shadow the canonical capability name or break `inspect --json`. diff --git a/docs/entry-conventions.md b/docs/entry-conventions.md index 28f4d3c8a..eec831d0c 100644 --- a/docs/entry-conventions.md +++ b/docs/entry-conventions.md @@ -177,7 +177,11 @@ counter, so provider filenames must not derive that key. Route-unit and CLI-dispatch tests inject provider values through the same `context` seam as identity axes (`renderRoute(id, { context: { providers: { library: fixture } } })`); the harness never executes conventional provider -modules, so a test chooses exactly the values a component observes. +modules, so a test chooses exactly the values a component observes. Once the +generated `.agent-bundle/routes.d.ts` augmentation declares provider keys, the +harness `options` and its `context.providers` become required (as does +`providers` on a direct `runAgentRequest`), so omitting a fixture the route's +types promise is a compile error rather than a runtime `undefined`. ### Handler request context diff --git a/docs/framework-mode.md b/docs/framework-mode.md index 3f8792246..be63977e3 100644 --- a/docs/framework-mode.md +++ b/docs/framework-mode.md @@ -107,7 +107,11 @@ the file is part of the project's TypeScript program (add `".agent-bundle/routes.d.ts"` to `tsconfig.json` `include`). Undeclared keys stay `unknown`. Route-unit and CLI-dispatch tests inject fixture values through `renderRoute(id, { context: { providers: { library } } })`; the harness never -executes provider modules on a test's behalf. +executes provider modules on a test's behalf. Because the augmentation makes +declared keys required, the same program also requires `context.providers` +(and the harness `options` argument) on every `renderRoute`, `invokeCli`, and +in-memory MCP call, and `providers` on a direct `runAgentRequest`: a handler +typed against `providers.library` can never observe an unchecked `undefined`. ### What reaches the MCP wire diff --git a/packages/agent-bundle/src/api.ts b/packages/agent-bundle/src/api.ts index 1fb425646..b04dc82c7 100644 --- a/packages/agent-bundle/src/api.ts +++ b/packages/agent-bundle/src/api.ts @@ -13,7 +13,7 @@ import { packOutputFromJson, type PackOutput, } from './build/pack-inventory.ts'; -import type { CapabilityState } from './core/capabilities.ts'; +import type { CapabilityEvidence, CapabilityState } from './core/capabilities.ts'; import { isInsideOrEqual } from './core/paths.ts'; import { stateDefinitionProjection, @@ -616,15 +616,41 @@ const componentCapabilityFor = ( capabilities: Readonly>, ): InspectionComponentCapability | undefined => { if (component.capability === undefined) return undefined; - const state = capabilities[component.capability]; - return Object.freeze({ - name: component.capability, - ...(state ?? unavailableCapability( - `The ${target} adapter publishes no ${component.capability} capability row.`, - )), - }); + const state = capabilities[component.capability] ?? unavailableCapability( + `The ${target} adapter publishes no ${component.capability} capability row.`, + ); + return Object.freeze({ ...capabilityContract(state), name: component.capability }); }; +/** + * Projects only the four-state contract fields of an adapter-owned capability + * row. `isCapabilityState` admits extension fields on JavaScript and third-party + * adapters, and copying them would let one named `name` shadow the canonical + * capability name or a cyclic one break `inspect --json`. + */ +const capabilityContract = (state: CapabilityState): CapabilityState => { + switch (state.state) { + case 'supported': + return { evidence: capabilityEvidenceContract(state.evidence), state: state.state }; + case 'degraded': + return { + ...(state.evidence === undefined ? {} : { evidence: capabilityEvidenceContract(state.evidence) }), + reason: state.reason, + state: state.state, + }; + case 'unavailable': + case 'prohibited': + return { reason: state.reason, state: state.state }; + default: { + const exhaustive: never = state; + throw new Error(`Unhandled capability state ${JSON.stringify(exhaustive)}`); + } + } +}; + +const capabilityEvidenceContract = (evidence: CapabilityEvidence): CapabilityEvidence => + Object.freeze({ observedVersion: evidence.observedVersion, target: evidence.target }); + interface AccountedComponents { readonly selected: readonly InspectionSelectedComponent[]; readonly skipped: readonly InspectionSkippedComponent[]; diff --git a/packages/agent-bundle/src/test/cli.ts b/packages/agent-bundle/src/test/cli.ts index 0706d1b88..7cc616086 100644 --- a/packages/agent-bundle/src/test/cli.ts +++ b/packages/agent-bundle/src/test/cli.ts @@ -25,14 +25,12 @@ import type { CompiledCliCommand } from '../routes/types.ts'; import { AgentTestError, captured } from './errors.ts'; import { CLI_DISPATCH_PROOF_LEVEL, type AgentBundleTestManifest } from './manifest.ts'; import { registeredRouteLoader, testManifest } from './registry.ts'; -import { prepareCliRenderHost, type RenderRouteContext } from './render.ts'; +import { prepareCliRenderHost, type HarnessOptionsArguments, type RenderRouteContextInit } from './render.ts'; import type { AgentRouteModule, RenderedRouteProvenance } from './types.ts'; export type { CliRenderedEvent }; -export interface InvokeCliOptions { - /** Request-scope overrides for the dispatched command, over the runtime's request contract. */ - readonly context?: RenderRouteContext; +export interface InvokeCliOptionsBase { readonly manifest?: AgentBundleTestManifest; readonly signal?: AbortSignal; /** @@ -42,6 +40,13 @@ export interface InvokeCliOptions { readonly tty?: boolean; } +/** + * Dispatch options; `context` carries the request-scope overrides for the + * dispatched command over the runtime's request contract and is required once + * the project declares providers (see {@link RenderRouteContextInit}). + */ +export type InvokeCliOptions = InvokeCliOptionsBase & RenderRouteContextInit; + export interface CliInvocation { /** The argv vector as dispatched, including the command path segments. */ readonly argv: readonly string[]; @@ -150,7 +155,7 @@ const moduleFor = async ( */ export const invokeCli = async ( argv: readonly string[], - options: InvokeCliOptions = {}, + ...[options = {}]: HarnessOptionsArguments ): Promise => { const manifest = options.manifest ?? testManifest(); if (manifest.cliCommands.length === 0) throw noCommands(manifest); diff --git a/packages/agent-bundle/src/test/index.ts b/packages/agent-bundle/src/test/index.ts index 6489d69bb..0209d9b26 100644 --- a/packages/agent-bundle/src/test/index.ts +++ b/packages/agent-bundle/src/test/index.ts @@ -51,8 +51,11 @@ export { AgentTestError } from './errors.ts'; export type { AgentTestErrorCode } from './errors.ts'; export { renderRoute, renderRouteEvents } from './render.ts'; export type { + HarnessOptionsArguments, RenderRouteContext, + RenderRouteContextInit, RenderRouteOptions, + RenderRouteOptionsBase, RenderRouteTarget, RenderedRoute, RenderedRouteEvents, @@ -119,6 +122,7 @@ export type { export type { InMemoryMcpSession, InMemoryMcpSessionOptions, + InMemoryMcpSessionOptionsBase, McpContentBlock, McpInvocationOptions, McpProjectionProvenance, @@ -128,7 +132,7 @@ export type { McpToolInvocation, } from './mcp.ts'; export { cliJson, cliNdjson, invokeCli } from './cli.ts'; -export type { CliDispatchProvenance, CliInvocation, CliRenderedEvent, InvokeCliOptions } from './cli.ts'; +export type { CliDispatchProvenance, CliInvocation, CliRenderedEvent, InvokeCliOptions, InvokeCliOptionsBase } from './cli.ts'; export { openPackedMcpServer, removeProjectSource } from './packed.ts'; export type { DeletedSourceReceipt, diff --git a/packages/agent-bundle/src/test/mcp.ts b/packages/agent-bundle/src/test/mcp.ts index ece401ff4..af03c154b 100644 --- a/packages/agent-bundle/src/test/mcp.ts +++ b/packages/agent-bundle/src/test/mcp.ts @@ -25,7 +25,7 @@ import type { createGeneratedRuntimeState } from '@agent-bundle/runtime/mount'; import { AgentTestError, captured } from './errors.ts'; import { MCP_IN_MEMORY_PROOF_LEVEL, type AgentBundleTestManifest } from './manifest.ts'; import { registeredRouteLoader, testManifest } from './registry.ts'; -import type { RenderRouteContext } from './render.ts'; +import type { HarnessOptionsArguments, RenderRouteContextInit } from './render.ts'; import type { RenderedRouteProvenance, TestableRouteDescriptor } from './types.ts'; /** Where an in-memory projection result came from and what it proves. */ @@ -57,12 +57,10 @@ export interface McpToolInvocation { readonly structuredContent?: unknown; } -export interface InMemoryMcpSessionOptions< +export interface InMemoryMcpSessionOptionsBase< TState = unknown, TEvents extends AgentStateEventSchemas = AgentStateEventSchemas, > { - /** Request-scoped overrides applied to every route render in this session. */ - readonly context?: RenderRouteContext; readonly manifest?: AgentBundleTestManifest; /** MCP server name. Optional when the project compiled exactly one server. */ readonly server?: string; @@ -73,6 +71,16 @@ export interface InMemoryMcpSessionOptions< }; } +/** + * Session options; `context` holds the request-scoped overrides applied to + * every route render in this session and is required once the project + * declares providers (see {@link RenderRouteContextInit}). + */ +export type InMemoryMcpSessionOptions< + TState = unknown, + TEvents extends AgentStateEventSchemas = AgentStateEventSchemas, +> = InMemoryMcpSessionOptionsBase & RenderRouteContextInit; + export interface InMemoryMcpSession extends AsyncDisposable { /** The real MCP SDK client, for protocol calls this module does not wrap. */ readonly client: Client; @@ -242,7 +250,7 @@ export const openInMemoryMcpServer = async < TState = unknown, TEvents extends AgentStateEventSchemas = AgentStateEventSchemas, >( - options: InMemoryMcpSessionOptions = {}, + ...[options = {}]: HarnessOptionsArguments> ): Promise => { const manifest = options.manifest ?? testManifest(); const serverName = resolveServerName(manifest, options.server); @@ -414,7 +422,7 @@ const asContentBlocks = (value: unknown): readonly McpContentBlock[] => */ export const invokeMcpTool = async ( tool: string, - options: McpInvocationOptions = {}, + ...[options = {}]: HarnessOptionsArguments ): Promise => withSession(options, async (session) => { const result = await session.client.callTool({ arguments: (options.input ?? {}) as Record, @@ -437,7 +445,7 @@ export interface McpResourceRead { /** Reads one compiled resource route by URI through the real protocol. */ export const readMcpResource = async ( uri: string, - options: InMemoryMcpSessionOptions = {}, + ...[options = {}]: HarnessOptionsArguments ): Promise => withSession(options, async (session) => { const result = await session.client.readResource({ uri }) as { contents?: unknown }; return Object.freeze({ @@ -454,7 +462,7 @@ export interface McpPromptResult { /** Gets one compiled prompt route through the real protocol. */ export const getMcpPrompt = async ( prompt: string, - options: McpInvocationOptions = {}, + ...[options = {}]: HarnessOptionsArguments ): Promise => withSession(options, async (session) => { const result = await session.client.getPrompt({ arguments: (options.input ?? {}) as Record, @@ -478,7 +486,7 @@ export interface McpSurfaceListing { * that a compiled route reached the protocol at all. */ export const listMcpSurface = async ( - options: InMemoryMcpSessionOptions = {}, + ...[options = {}]: HarnessOptionsArguments ): Promise => withSession(options, async (session) => { const [tools, resources, prompts] = await Promise.all([ session.client.listTools(), diff --git a/packages/agent-bundle/src/test/render.ts b/packages/agent-bundle/src/test/render.ts index 9cae3a154..af8dd334e 100644 --- a/packages/agent-bundle/src/test/render.ts +++ b/packages/agent-bundle/src/test/render.ts @@ -11,6 +11,7 @@ import type { AgentInvocationInput, AgentProgressReporter, AgentProgressUpdate, + AgentProviderValues, AgentRenderDispatch, AgentRenderEvent, AgentRenderInvocation, @@ -53,10 +54,21 @@ export type RenderRouteContext = Omit extends AgentProviderValues + ? { readonly context?: RenderRouteContext } + : { readonly context: RenderRouteContext }; + +export interface RenderRouteOptionsBase { /** CLI route arguments; `cli` routes only. */ readonly args?: readonly string[]; - readonly context?: RenderRouteContext; /** The route's input: tool input, event payload, or script input. */ readonly input?: unknown; /** Overrides the route kind when a module is rendered directly; ignored for manifest routes. */ @@ -69,6 +81,17 @@ export interface RenderRouteOptions { readonly signal?: AbortSignal; } +export type RenderRouteOptions = RenderRouteOptionsBase & RenderRouteContextInit; + +/** + * The trailing options parameter of every harness entry point. Provider-free + * projects may omit it; once the generated augmentation declares provider + * keys it is mandatory, so no harness call can silently skip the fixtures. + */ +export type HarnessOptionsArguments = Record extends AgentProviderValues + ? readonly [options?: Options] + : readonly [options: Options]; + export interface RenderedRoute { /** The final Agent Document the real renderer produced. */ readonly document: AgentDocument; @@ -769,7 +792,7 @@ const renderFailure = ( */ export const renderRoute = async ( target: RenderRouteTarget, - options: RenderRouteOptions = {}, + ...[options = {}]: HarnessOptionsArguments ): Promise => { const { close, collected, dispatcher, invocation, resolved, signal } = await prepareRender(target, options); try { @@ -804,7 +827,7 @@ export interface RenderedRouteEvents extends RenderedRoute { */ export const renderRouteEvents = async ( target: RenderRouteTarget, - options: RenderRouteOptions = {}, + ...[options = {}]: HarnessOptionsArguments ): Promise => { const { close, collected, dispatcher, invocation, resolved, signal } = await prepareRender(target, options); const events: AgentRenderEvent[] = []; diff --git a/packages/agent-bundle/tests/api.test.ts b/packages/agent-bundle/tests/api.test.ts index b5d0ed426..0fdb49463 100644 --- a/packages/agent-bundle/tests/api.test.ts +++ b/packages/agent-bundle/tests/api.test.ts @@ -14,6 +14,7 @@ import { } from '../src/adapters/hook-contract.ts'; import type { TargetAdapter } from '../src/adapters/types.ts'; import { inspectArtifactFilesystem } from '../src/build/emit.ts'; +import type { CapabilityState } from '../src/core/capabilities.ts'; import type { Diagnostic } from '../src/core/diagnostics.ts'; import { pathTokens, type NormalizedPlugin } from '../src/core/types.ts'; import { ProjectService } from '../src/dev/project-service.ts'; @@ -177,6 +178,49 @@ it('prepares and inspects a target owned only by the supplied advanced registry' } }); +it('projects only the capability contract fields of adapter-owned rows into inspection', async () => { + const root = await createProject(); + // A JavaScript or third-party adapter may decorate an otherwise valid row + // with extension fields. `isCapabilityState` admits them, so the inspection + // must not copy them: a `name` extension would shadow the canonical + // capability name and a cyclic value would break `inspect --json`. + const cyclic: Record = {}; + cyclic['self'] = cyclic; + const decorated = (row: CapabilityState): CapabilityState => + ({ ...row, cyclic, name: 'shadow' }) as unknown as CapabilityState; + const capabilities: Record = { + hooks: decorated({ evidence: { observedVersion: '1.0.0', target: syntheticTarget }, state: 'supported' }), + mcp: decorated({ evidence: { observedVersion: '1.0.0', target: syntheticTarget }, state: 'supported' }), + skills: decorated({ reason: 'partial skill support', state: 'degraded' }), + }; + const registry = new TargetRegistry().register({ ...syntheticAdapter, capabilities }, { default: true }); + try { + await writeFile(join(root, 'agent-bundle.config.ts'), [ + 'export default {', + " plugin: { name: 'synthetic-api-fixture', version: '1.0.0' },", + " hooks: { sessionStart: { handler: './src/hook.ts' } },", + " synthetic: { enabled: true },", + " targets: ['synthetic'],", + '};', + '', + ].join('\n')); + + const result = await readyInspection({ registry, root }); + const plan = result.plans[0]; + if (plan === undefined) throw new Error('Expected one synthetic plan.'); + + expect(plan.selected.map((component) => component.capability)).toEqual([ + { evidence: { observedVersion: '1.0.0', target: syntheticTarget }, name: 'hooks', state: 'supported' }, + ]); + expect(plan.skipped.map((component) => component.capability)).toEqual([ + { name: 'skills', reason: 'partial skill support', state: 'degraded' }, + ]); + expect(() => JSON.stringify(result.plans)).not.toThrow(); + } finally { + await rm(join(root, '..'), { force: true, recursive: true }); + } +}); + it('accepts claude.userConfig through the public inspection and build APIs', async () => { const root = await createProject(); const artifact = join(root, 'artifact'); diff --git a/packages/agent-bundle/tests/provider-typegen.test.ts b/packages/agent-bundle/tests/provider-typegen.test.ts index 5ed759784..34dc11abb 100644 --- a/packages/agent-bundle/tests/provider-typegen.test.ts +++ b/packages/agent-bundle/tests/provider-typegen.test.ts @@ -113,6 +113,34 @@ it('types (await agent()).providers. from the generated provider declaratio 'export const wrong = async (): Promise => (await agent()).providers.library;', '', ].join('\n')), + // Contexts that do not run src/providers/* — a custom runAgentRequest host + // or a route-unit fixture — must supply the declared keys, or the handler's + // typed `providers.library` would dereference undefined at runtime. + writeProjectFile(root, 'custom-scope.ts', [ + "import { runAgentRequest } from '@agent-bundle/runtime';", + "import { renderRoute } from 'agent-bundle/test';", + "import type { LibraryContext } from './src/providers/library.js';", + '', + "const library: LibraryContext = { stages: ['discover'], surface: 'tool' };", + 'export const complete = async (): Promise => {', + " await runAgentRequest({ invocation: { kind: 'tool' }, providers: { buildNumber: 7, library } }, async () => undefined);", + " await renderRoute('tool:curator/status', { context: { providers: { buildNumber: 7, library } } });", + '};', + '', + ].join('\n')), + writeProjectFile(root, 'missing-providers.ts', [ + "import { runAgentRequest } from '@agent-bundle/runtime';", + "export const omitted = runAgentRequest({ invocation: { kind: 'tool' } }, async () => undefined);", + '', + ].join('\n')), + writeProjectFile(root, 'missing-fixture.ts', [ + "import { renderRoute } from 'agent-bundle/test';", + "import type { LibraryContext } from './src/providers/library.js';", + "const library: LibraryContext = { stages: ['discover'], surface: 'tool' };", + "export const partial = renderRoute('tool:curator/status', { context: { providers: { library } } });", + "export const absent = renderRoute('tool:curator/status');", + '', + ].join('\n')), ]); const result = await inspect({ root }); @@ -126,4 +154,13 @@ it('types (await agent()).providers. from the generated provider declaratio const mismatch = typecheck(root, 'mismatch.ts'); expect(mismatch).toHaveLength(1); expect(mismatch[0]).toContain("Type 'LibraryContext' is not assignable to type 'number'"); + + expect(typecheck(root, 'custom-scope.ts')).toEqual([]); + const missingProviders = typecheck(root, 'missing-providers.ts'); + expect(missingProviders).toHaveLength(1); + expect(missingProviders[0]).toContain("Property 'providers' is missing"); + const missingFixture = typecheck(root, 'missing-fixture.ts'); + expect(missingFixture).toHaveLength(2); + expect(missingFixture[0]).toContain("Property '\"buildNumber\"' is missing"); + expect(missingFixture[1]).toContain('Expected 2 arguments, but got 1.'); }); diff --git a/packages/rsc-runtime/src/agent-request.ts b/packages/rsc-runtime/src/agent-request.ts index 3ec26137a..f8949ea58 100644 --- a/packages/rsc-runtime/src/agent-request.ts +++ b/packages/rsc-runtime/src/agent-request.ts @@ -173,7 +173,20 @@ export interface AgentRequestContext { readonly notices: AgentNoticesHandle | undefined; } -export interface AgentRequestInit { +/** + * The `providers` member of {@link AgentRequestInit}. It is optional only while + * {@link AgentProviderValues} has no required keys. Once a project's generated + * `.agent-bundle/routes.d.ts` augmentation declares its conventional providers, + * every direct `runAgentRequest` caller — custom hosts and route-unit fixtures + * alike — must supply the full record, so a handler typed against those keys + * never observes an unchecked `undefined`. Generated request scopes always run + * the providers before the handler and are unaffected. + */ +export type AgentRequestProvidersInit = Record extends AgentProviderValues + ? { readonly providers?: AgentProviderValues } + : { readonly providers: AgentProviderValues }; + +export interface AgentRequestInitBase { readonly actor?: Observed; readonly capabilities?: AgentRequestCapabilities; readonly host?: Observed; @@ -181,7 +194,6 @@ export interface AgentRequestInit { /** Optional durable notice authority; omitted projects load no notice code. */ readonly noticeLedger?: AgentNoticeLedger; readonly progress?: AgentProgressReporter; - readonly providers?: AgentProviderValues; readonly services?: AgentServiceRegistry; readonly session?: Observed; readonly signal?: AbortSignal; @@ -190,6 +202,8 @@ export interface AgentRequestInit { readonly workspace?: Observed; } +export type AgentRequestInit = AgentRequestInitBase & AgentRequestProvidersInit; + export type AgentRequestErrorCode = 'invalid-invocation' | 'outside-invocation' | 'request-closed' | 'store-version-conflict'; export class AgentRequestError extends Error { diff --git a/packages/rsc-runtime/src/plugin.ts b/packages/rsc-runtime/src/plugin.ts index 448177b43..11aad0747 100644 --- a/packages/rsc-runtime/src/plugin.ts +++ b/packages/rsc-runtime/src/plugin.ts @@ -29,6 +29,8 @@ export type { AgentRequestContext, AgentRequestErrorCode, AgentRequestInit, + AgentRequestInitBase, + AgentRequestProvidersInit, AgentServiceRegistry, AgentScriptInvocationProps, AgentSessionIdentity, From c2bcd7d0eb0f63da8935fb29ea864435209190ea Mon Sep 17 00:00:00 2001 From: ScriptedAlchemy Date: Thu, 3 Sep 2026 07:47:43 +0000 Subject: [PATCH 2/3] chore(changeset): follow the changeset summary convention (#409) --- .changeset/required-provider-fixtures.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/required-provider-fixtures.md b/.changeset/required-provider-fixtures.md index 738df2668..e18a91dde 100644 --- a/.changeset/required-provider-fixtures.md +++ b/.changeset/required-provider-fixtures.md @@ -3,4 +3,4 @@ "agent-bundle": patch --- -Require provider fixtures where providers are not executed. Once a project's generated `.agent-bundle/routes.d.ts` augmentation declares provider keys, `runAgentRequest`'s `providers`, the test harness `options` argument, and its `context.providers` become required (`AgentRequestProvidersInit`, `RenderRouteContextInit`, `HarnessOptionsArguments`), so a handler typed against `(await agent()).providers.` can never observe an unchecked `undefined` from a custom scope or a route-unit test. Provider-free projects are unchanged. `inspect` now projects only the four-state contract fields of adapter-owned capability rows into component accounting, so extension fields on JavaScript or third-party adapters cannot shadow the canonical capability name or break `inspect --json`. +Require provider fixtures wherever conventional providers do not run. Once a project's generated `.agent-bundle/routes.d.ts` augmentation declares provider keys, `runAgentRequest`'s `providers` (`AgentRequestProvidersInit`), the `renderRoute` / `invokeCli` / in-memory MCP harness `options` argument (`HarnessOptionsArguments`), and its `context.providers` (`RenderRouteContextInit`) become required, so a handler typed against `(await agent()).providers.` never observes an unchecked `undefined` from a custom scope or a route-unit test; provider-free projects are unchanged. Make `agent-bundle inspect` project only the four-state contract fields of adapter-owned capability rows into component accounting, so extension fields on JavaScript or third-party adapters cannot shadow the canonical capability name or break `inspect --json`. (#409) From bd0f013d7b6738e5535b8387e39087ecd4fd5cae Mon Sep 17 00:00:00 2001 From: ScriptedAlchemy Date: Thu, 3 Sep 2026 08:09:32 +0000 Subject: [PATCH 3/3] chore(changeset): classify the required provider fixtures as a pre-1.0 minor (#409) --- .changeset/required-provider-fixtures.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.changeset/required-provider-fixtures.md b/.changeset/required-provider-fixtures.md index e18a91dde..1ebd5afe1 100644 --- a/.changeset/required-provider-fixtures.md +++ b/.changeset/required-provider-fixtures.md @@ -1,6 +1,6 @@ --- -"@agent-bundle/runtime": patch -"agent-bundle": patch +"@agent-bundle/runtime": minor +"agent-bundle": minor --- -Require provider fixtures wherever conventional providers do not run. Once a project's generated `.agent-bundle/routes.d.ts` augmentation declares provider keys, `runAgentRequest`'s `providers` (`AgentRequestProvidersInit`), the `renderRoute` / `invokeCli` / in-memory MCP harness `options` argument (`HarnessOptionsArguments`), and its `context.providers` (`RenderRouteContextInit`) become required, so a handler typed against `(await agent()).providers.` never observes an unchecked `undefined` from a custom scope or a route-unit test; provider-free projects are unchanged. Make `agent-bundle inspect` project only the four-state contract fields of adapter-owned capability rows into component accounting, so extension fields on JavaScript or third-party adapters cannot shadow the canonical capability name or break `inspect --json`. (#409) +Require provider fixtures wherever conventional providers do not run (breaking for provider-enabled projects that omitted them). Once a project's generated `.agent-bundle/routes.d.ts` augmentation declares provider keys, `runAgentRequest`'s `providers` (`AgentRequestProvidersInit`), the `renderRoute` / `invokeCli` / in-memory MCP harness `options` argument (`HarnessOptionsArguments`), and its `context.providers` (`RenderRouteContextInit`) become required, so a handler typed against `(await agent()).providers.` never observes an unchecked `undefined` from a custom scope or a route-unit test; provider-free projects are unchanged. Make `agent-bundle inspect` project only the four-state contract fields of adapter-owned capability rows into component accounting, so extension fields on JavaScript or third-party adapters cannot shadow the canonical capability name or break `inspect --json`. (#409)