diff --git a/sdk/src/gsd-transport.ts b/sdk/src/gsd-transport.ts index 3857d2886..eecea7238 100644 --- a/sdk/src/gsd-transport.ts +++ b/sdk/src/gsd-transport.ts @@ -55,7 +55,7 @@ export class GSDTransport { } else { const reason = this.subprocessReason(request, policy); onDecision?.({ dispatchMode: 'subprocess', reason }); - if (!policy.allowFallbackToSubprocess) { + if (!policy.allowFallbackToSubprocess && reason === 'native_unregistered') { throw GSDToolsError.failure( `Subprocess fallback disabled: command '${request.registryCommand}' cannot run without native dispatch`, request.legacyCommand, diff --git a/sdk/src/index.ts b/sdk/src/index.ts index d9e8f811a..5e83fc430 100644 --- a/sdk/src/index.ts +++ b/sdk/src/index.ts @@ -50,6 +50,8 @@ export class GSD { private readonly defaultMaxTurns: number; private readonly autoMode: boolean; private readonly workstream?: string; + private readonly strictSdk?: boolean; + private readonly allowFallbackToSubprocess?: boolean; readonly eventStream: GSDEventStream; constructor(options: GSDOptions) { @@ -62,6 +64,8 @@ export class GSD { this.defaultMaxTurns = options.maxTurns ?? 50; this.autoMode = options.autoMode ?? false; this.workstream = options.workstream; + this.strictSdk = options.strictSdk; + this.allowFallbackToSubprocess = options.allowFallbackToSubprocess; this.eventStream = new GSDEventStream(); } @@ -128,6 +132,16 @@ export class GSD { workstream: this.workstream, eventStream: this.eventStream, sessionId: this.sessionId, + strictSdk: this.strictSdk, + allowFallbackToSubprocess: this.allowFallbackToSubprocess, + onDispatchEvent: (event) => { + this.eventStream.emitEvent({ + type: GSDEventType.StreamEvent, + timestamp: new Date().toISOString(), + sessionId: this.sessionId ?? '', + event, + }); + }, }); } diff --git a/sdk/src/runtime-bridge-options.test.ts b/sdk/src/runtime-bridge-options.test.ts new file mode 100644 index 000000000..443aaae0f --- /dev/null +++ b/sdk/src/runtime-bridge-options.test.ts @@ -0,0 +1,33 @@ +import { describe, it, expect } from 'vitest'; +import { GSD } from './index.js'; +import { GSDEventType, type GSDEvent } from './types.js'; + +describe('GSD runtime bridge options', () => { + it('strictSdk option is honored by createTools dispatch seam', async () => { + const gsd = new GSD({ + projectDir: process.cwd(), + strictSdk: true, + allowFallbackToSubprocess: true, + sessionId: 'test-session', + }); + + const events: GSDEvent[] = []; + gsd.onEvent((event) => events.push(event)); + + await expect(gsd.createTools().exec('nonexistent-command', [])).rejects.toThrow( + "Strict SDK mode: command 'nonexistent-command' has no native adapter", + ); + + const streamEvent = events.find((event) => event.type === GSDEventType.StreamEvent); + expect(streamEvent).toBeDefined(); + expect(streamEvent).toMatchObject({ + type: GSDEventType.StreamEvent, + sessionId: 'test-session', + event: { + type: 'query_dispatch', + command: 'nonexistent-command', + outcome: 'error', + }, + }); + }); +}); diff --git a/sdk/src/types.ts b/sdk/src/types.ts index b57b07363..f0958e648 100644 --- a/sdk/src/types.ts +++ b/sdk/src/types.ts @@ -203,6 +203,10 @@ export interface GSDOptions { * Optional session correlation id for query mutation events when using {@link GSD.createTools}. */ sessionId?: string; + /** Strict SDK runtime bridge mode: fail fast when a query command has no native adapter. */ + strictSdk?: boolean; + /** Explicit subprocess fallback policy for the runtime bridge. Default false. */ + allowFallbackToSubprocess?: boolean; /** Model to use for execution sessions. */ model?: string; /** Maximum budget per plan execution in USD. Default: 5.0. */