diff --git a/src/main/runtime/rpc/errors.ts b/src/main/runtime/rpc/errors.ts index ef4b0b3024d..9483a72bd15 100644 --- a/src/main/runtime/rpc/errors.ts +++ b/src/main/runtime/rpc/errors.ts @@ -109,6 +109,9 @@ const STRUCTURED_RUNTIME_PASSTHROUGH_CODES: ReadonlySet = new Set([ 'runtime_timeout', 'invalid_runtime_response', 'capability_unsupported', + // The whole `agentSession.*` gate refusal. Coded so a client can route on it without matching + // the message; the message is unchanged so older clients still classify. + 'structured_agent_session_unsupported', 'relay_quota_exceeded', 'dispatch_capability_invalid', 'agent_unconfigured', diff --git a/src/main/runtime/rpc/methods/structured-agent-session-gate.ts b/src/main/runtime/rpc/methods/structured-agent-session-gate.ts index 60b28425057..5b23a648f8c 100644 --- a/src/main/runtime/rpc/methods/structured-agent-session-gate.ts +++ b/src/main/runtime/rpc/methods/structured-agent-session-gate.ts @@ -8,6 +8,7 @@ // for old mobile clients while structured chat is enabled so they receive a fallback row, and that // path constructs the host. `agentSession.*` stays refused either way, which is what this gate is for. +import { OrchestrationError } from '../../orchestration/orchestration-error' import { getStructuredAgentSessionHost } from '../../../native-chat/agent-session-wire/structured-agent-session-registry' import type { StructuredAgentSessionHost } from '../../../native-chat/agent-session-wire/structured-agent-session-host' import type { StructuredAgentSessionCaller } from '../../../native-chat/agent-session-wire/structured-agent-session-host-types' @@ -17,6 +18,17 @@ import { supportsStructuredAgentSessions } from './structured-agent-session-policy' +/** + * Carries a code so a client can tell "this host refuses structured chat" apart from any other + * runtime fault. The message stays the bare token: older clients classify by matching it. + */ +function structuredSessionsUnsupported(): OrchestrationError { + return new OrchestrationError( + 'structured_agent_session_unsupported', + 'structured_agent_session_unsupported' + ) +} + /** * In-process callers are the same build as the host, so they carry no negotiated * capability list; every remote client must say it can read structured sessions. @@ -27,7 +39,7 @@ export function supportsStructuredSessions(ctx: RpcContext): boolean { export function requireStructuredCapability(ctx: RpcContext): void { if (!supportsStructuredSessions(ctx)) { - throw new Error('structured_agent_session_unsupported') + throw structuredSessionsUnsupported() } } @@ -35,7 +47,7 @@ export function requireStructuredHost(ctx: RpcContext): StructuredAgentSessionHo requireStructuredCapability(ctx) const host = getStructuredAgentSessionHost() if (!host) { - throw new Error('structured_agent_session_unsupported') + throw structuredSessionsUnsupported() } return host } @@ -64,11 +76,11 @@ export function requireStructuredHost(ctx: RpcContext): StructuredAgentSessionHo */ export function requireStructuredCleanupHost(ctx: RpcContext): StructuredAgentSessionHost { if (!supportsStructuredAgentSessionCapability(ctx)) { - throw new Error('structured_agent_session_unsupported') + throw structuredSessionsUnsupported() } const host = getStructuredAgentSessionHost() if (!host) { - throw new Error('structured_agent_session_unsupported') + throw structuredSessionsUnsupported() } return host } diff --git a/src/main/runtime/rpc/methods/structured-agent-session.test.ts b/src/main/runtime/rpc/methods/structured-agent-session.test.ts index c2d46b09818..bec7827f0cb 100644 --- a/src/main/runtime/rpc/methods/structured-agent-session.test.ts +++ b/src/main/runtime/rpc/methods/structured-agent-session.test.ts @@ -177,6 +177,20 @@ describe('capability gating', () => { expect(hostCalls.send).not.toHaveBeenCalled() }) + it('refuses with a code a client can route on, not just a message it must parse', async () => { + const response = await call('agentSession.send', sendParams(), { + clientKind: 'runtime', + clientCapabilities: ['terminal.stream.v1'] + }) + expect(response).toMatchObject({ + ok: false, + error: { + code: 'structured_agent_session_unsupported', + message: 'structured_agent_session_unsupported' + } + }) + }) + it('rejects create intent before resolving host-owned fields for an old client', async () => { const worktree = 'id:workspace-1' const response = await call(