@robota-sdk/agent-transport 3.0.0-beta.75 → 3.0.0-beta.77
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +661 -21
- package/README.md +17 -14
- package/dist/node/headless/index.cjs +1 -1
- package/dist/node/headless/index.d.ts +1 -1
- package/dist/node/headless/index.js +1 -1
- package/dist/node/{headless-mRYilLfC.js → headless-DG_MEbf0.js} +7 -4
- package/dist/node/headless-DG_MEbf0.js.map +1 -0
- package/dist/node/headless-xq77JBq0.cjs +18 -0
- package/dist/node/{index-E8Gx4-lc.d.ts → index-CrJ2Lhn3.d.ts} +16 -3
- package/dist/node/index-CrJ2Lhn3.d.ts.map +1 -0
- package/dist/node/index-JIvzrtaA.d.ts +52 -0
- package/dist/node/index-JIvzrtaA.d.ts.map +1 -0
- package/dist/node/{index-CYl7ksS6.d.ts → index-JTD_xtEQ.d.ts} +15 -2
- package/dist/node/index-JTD_xtEQ.d.ts.map +1 -0
- package/dist/node/index-y_HBOfzO.d.ts +52 -0
- package/dist/node/index-y_HBOfzO.d.ts.map +1 -0
- package/dist/node/index.cjs +1 -1
- package/dist/node/index.d.ts +10 -9
- package/dist/node/index.d.ts.map +1 -1
- package/dist/node/index.js +1 -1
- package/dist/node/index.js.map +1 -1
- package/dist/node/programmatic/index.cjs +1 -0
- package/dist/node/programmatic/index.d.ts +2 -0
- package/dist/node/programmatic/index.js +1 -0
- package/dist/node/programmatic-6wLf68qJ.cjs +1 -0
- package/dist/node/programmatic-70dxIXtS.js +2 -0
- package/dist/node/programmatic-70dxIXtS.js.map +1 -0
- package/dist/node/testing/index.cjs +1 -1
- package/dist/node/testing/index.d.ts +2 -21
- package/dist/node/testing/index.js +1 -2
- package/package.json +15 -71
- package/src/__tests__/programmatic/programmatic-ask-user.test.ts +32 -0
- package/src/__tests__/programmatic/programmatic-driver.test.ts +107 -0
- package/src/__tests__/programmatic/programmatic-flows.test.ts +140 -0
- package/src/__tests__/transport-registry.stop-all.test.ts +53 -0
- package/src/headless/HeadlessInteractionChannel.ts +22 -5
- package/src/headless/__tests__/headless-goal-runner.test.ts +108 -0
- package/src/headless/__tests__/headless-runner.test.ts +2 -1
- package/src/headless/headless-runner.ts +67 -1
- package/src/headless/headless-stream-json.ts +1 -1
- package/src/headless/print-terminal.ts +1 -0
- package/src/index.ts +2 -5
- package/src/programmatic/ProgrammaticInteractionChannel.ts +82 -0
- package/src/programmatic/createProgrammaticAgent.ts +71 -0
- package/src/programmatic/index.ts +5 -0
- package/src/testing/index.ts +7 -4
- package/src/transport-registry.ts +15 -12
- package/dist/node/headless-CT2ibQnr.cjs +0 -15
- package/dist/node/headless-mRYilLfC.js.map +0 -1
- package/dist/node/http/index.cjs +0 -1
- package/dist/node/http/index.d.ts +0 -2
- package/dist/node/http/index.js +0 -1
- package/dist/node/http-2Jiuflc1.js +0 -2
- package/dist/node/http-2Jiuflc1.js.map +0 -1
- package/dist/node/http-CBAvefLw.cjs +0 -1
- package/dist/node/index-BNccqSpv.d.ts +0 -86
- package/dist/node/index-BNccqSpv.d.ts.map +0 -1
- package/dist/node/index-BUhHIf7X.d.ts +0 -86
- package/dist/node/index-BUhHIf7X.d.ts.map +0 -1
- package/dist/node/index-BnAGE-u9.d.ts +0 -33
- package/dist/node/index-BnAGE-u9.d.ts.map +0 -1
- package/dist/node/index-BrQ4gGw0.d.ts +0 -213
- package/dist/node/index-BrQ4gGw0.d.ts.map +0 -1
- package/dist/node/index-CYl7ksS6.d.ts.map +0 -1
- package/dist/node/index-CoeBF21y.d.ts +0 -213
- package/dist/node/index-CoeBF21y.d.ts.map +0 -1
- package/dist/node/index-DHt-2VQ-.d.ts +0 -46
- package/dist/node/index-DHt-2VQ-.d.ts.map +0 -1
- package/dist/node/index-DMwKN5Le.d.ts +0 -33
- package/dist/node/index-DMwKN5Le.d.ts.map +0 -1
- package/dist/node/index-E8Gx4-lc.d.ts.map +0 -1
- package/dist/node/index-c0M42fsA.d.ts +0 -46
- package/dist/node/index-c0M42fsA.d.ts.map +0 -1
- package/dist/node/mcp/index.cjs +0 -1
- package/dist/node/mcp/index.d.ts +0 -2
- package/dist/node/mcp/index.js +0 -1
- package/dist/node/mcp-BOglBJNy.cjs +0 -1
- package/dist/node/mcp-D3BBVK7C.js +0 -2
- package/dist/node/mcp-D3BBVK7C.js.map +0 -1
- package/dist/node/rolldown-runtime-CMqjfN_6.cjs +0 -1
- package/dist/node/testing/index.d.ts.map +0 -1
- package/dist/node/testing/index.js.map +0 -1
- package/dist/node/tui/index.cjs +0 -1
- package/dist/node/tui/index.d.ts +0 -2
- package/dist/node/tui/index.js +0 -1
- package/dist/node/tui-CcH5EsQh.js +0 -25
- package/dist/node/tui-CcH5EsQh.js.map +0 -1
- package/dist/node/tui-DznRbcku.cjs +0 -24
- package/dist/node/ws/index.cjs +0 -1
- package/dist/node/ws/index.d.ts +0 -2
- package/dist/node/ws/index.js +0 -1
- package/dist/node/ws-Dc2RUwVs.js +0 -2
- package/dist/node/ws-Dc2RUwVs.js.map +0 -1
- package/dist/node/ws-QNMQn5kg.cjs +0 -1
- package/src/http/__tests__/http-transport.test.ts +0 -55
- package/src/http/__tests__/routes.test.ts +0 -168
- package/src/http/http-transport.ts +0 -41
- package/src/http/index.ts +0 -4
- package/src/http/routes.ts +0 -152
- package/src/mcp/__tests__/mcp-server.test.ts +0 -66
- package/src/mcp/__tests__/mcp-transport.test.ts +0 -46
- package/src/mcp/index.ts +0 -4
- package/src/mcp/mcp-server.ts +0 -163
- package/src/mcp/mcp-transport.ts +0 -48
- package/src/testing/__tests__/scripted-provider.test.ts +0 -73
- package/src/testing/scripted-provider.ts +0 -73
- package/src/tui/App.tsx +0 -491
- package/src/tui/BackgroundTaskPanel.tsx +0 -36
- package/src/tui/CjkTextInput.tsx +0 -199
- package/src/tui/ConfirmPrompt.tsx +0 -70
- package/src/tui/ContextWarningBanner.tsx +0 -34
- package/src/tui/ExecutionWorkspaceDetailPane.tsx +0 -64
- package/src/tui/ExecutionWorkspaceSwitcher.tsx +0 -187
- package/src/tui/InputArea.tsx +0 -310
- package/src/tui/InteractivePrompt.tsx +0 -59
- package/src/tui/ListPicker.tsx +0 -95
- package/src/tui/MenuSelect.tsx +0 -104
- package/src/tui/MessageList.tsx +0 -284
- package/src/tui/PermissionPrompt.tsx +0 -86
- package/src/tui/PluginTUI.tsx +0 -258
- package/src/tui/SessionPicker.tsx +0 -68
- package/src/tui/SessionStatusBar.tsx +0 -73
- package/src/tui/SlashAutocomplete.tsx +0 -110
- package/src/tui/StatusBar.tsx +0 -236
- package/src/tui/StreamingIndicator.tsx +0 -93
- package/src/tui/TextPrompt.tsx +0 -81
- package/src/tui/ToolCommandOutput.tsx +0 -39
- package/src/tui/ToolDiffBlock.tsx +0 -32
- package/src/tui/TransportTUI.tsx +0 -117
- package/src/tui/TuiInteractionChannel.ts +0 -495
- package/src/tui/UpdateNotice.tsx +0 -14
- package/src/tui/UsageSummaryEntry.tsx +0 -39
- package/src/tui/WaveText.tsx +0 -44
- package/src/tui/__tests__/InteractivePrompt.test.tsx +0 -82
- package/src/tui/__tests__/ListPicker.test.tsx +0 -159
- package/src/tui/__tests__/MenuSelect.test.tsx +0 -103
- package/src/tui/__tests__/PluginTUI.test.tsx +0 -167
- package/src/tui/__tests__/SlashAutocomplete.test.tsx +0 -140
- package/src/tui/__tests__/TextPrompt.test.tsx +0 -98
- package/src/tui/__tests__/TuiInteractionChannel.display-contract.test.ts +0 -239
- package/src/tui/__tests__/TuiInteractionChannel.lifecycle.test.ts +0 -297
- package/src/tui/__tests__/TuiInteractionChannel.requestAction.test.ts +0 -124
- package/src/tui/__tests__/UpdateNotice.test.tsx +0 -15
- package/src/tui/__tests__/abort-after-permission.test.tsx +0 -169
- package/src/tui/__tests__/abort-streaming-e2e.test.tsx +0 -183
- package/src/tui/__tests__/background-task-panel.test.tsx +0 -53
- package/src/tui/__tests__/background-task-row-format.test.ts +0 -59
- package/src/tui/__tests__/channel-factory-integration.test.ts +0 -138
- package/src/tui/__tests__/cjk-text-input-flow.test.ts +0 -109
- package/src/tui/__tests__/cjk-text-input.test.ts +0 -191
- package/src/tui/__tests__/command-effect-handler.test.ts +0 -127
- package/src/tui/__tests__/command-output-summary.test.ts +0 -95
- package/src/tui/__tests__/compact-event-bridge.test.ts +0 -20
- package/src/tui/__tests__/confirm-permission-flow.test.ts +0 -130
- package/src/tui/__tests__/confirm-prompt.test.tsx +0 -87
- package/src/tui/__tests__/execution-workspace-switcher.test.tsx +0 -110
- package/src/tui/__tests__/execution-workspace-view-model.test.ts +0 -93
- package/src/tui/__tests__/fixtures/provider-setup-prompt-driver.tsx +0 -125
- package/src/tui/__tests__/input-area-flow.test.ts +0 -164
- package/src/tui/__tests__/message-list-rendering.test.tsx +0 -353
- package/src/tui/__tests__/prompt-queue.test.tsx +0 -255
- package/src/tui/__tests__/provider-setup-pty-e2e.test.ts +0 -233
- package/src/tui/__tests__/pty/pty-driver.ts +0 -135
- package/src/tui/__tests__/pty/tui-pty.ptytest.ts +0 -61
- package/src/tui/__tests__/render-channel-options.test.ts +0 -32
- package/src/tui/__tests__/render-markdown.test.ts +0 -72
- package/src/tui/__tests__/selection-flow.test.ts +0 -61
- package/src/tui/__tests__/session-init-poller.test.ts +0 -102
- package/src/tui/__tests__/session-naming.test.ts +0 -64
- package/src/tui/__tests__/session-switch-channel.test.tsx +0 -307
- package/src/tui/__tests__/slash-routing-effects.test.ts +0 -228
- package/src/tui/__tests__/status-activity.test.ts +0 -71
- package/src/tui/__tests__/status-bar.test.tsx +0 -177
- package/src/tui/__tests__/streaming-indicator.test.tsx +0 -137
- package/src/tui/__tests__/text-prompt-flow.test.ts +0 -77
- package/src/tui/__tests__/tui-channel-init-failure.test.ts +0 -57
- package/src/tui/__tests__/tui-state-manager.test.ts +0 -401
- package/src/tui/background-task-row-format.ts +0 -53
- package/src/tui/command-interaction.ts +0 -9
- package/src/tui/command-output-summary.ts +0 -122
- package/src/tui/create-default-tui-cli-adapter.ts +0 -41
- package/src/tui/execution-workspace-view-model.ts +0 -123
- package/src/tui/flows/cjk-text-input-flow.ts +0 -285
- package/src/tui/flows/confirm-prompt-flow.ts +0 -45
- package/src/tui/flows/input-area-flow.ts +0 -189
- package/src/tui/flows/permission-prompt-flow.ts +0 -85
- package/src/tui/flows/selection-flow.ts +0 -126
- package/src/tui/flows/session-init-poller.ts +0 -77
- package/src/tui/flows/text-prompt-flow.ts +0 -98
- package/src/tui/hooks/command-effect-handler.ts +0 -97
- package/src/tui/hooks/command-effect-queue.ts +0 -39
- package/src/tui/hooks/side-effects-types.ts +0 -35
- package/src/tui/hooks/useAutocomplete.ts +0 -87
- package/src/tui/hooks/usePluginCallbacks.ts +0 -31
- package/src/tui/hooks/usePluginScreenData.ts +0 -85
- package/src/tui/hooks/useSideEffects.ts +0 -175
- package/src/tui/hooks/useSlashRouting.ts +0 -118
- package/src/tui/hooks/useStatusLineSettings.ts +0 -37
- package/src/tui/hooks/useTuiChannel.ts +0 -95
- package/src/tui/index.ts +0 -14
- package/src/tui/interactions/CommandConfirm.tsx +0 -36
- package/src/tui/interactions/CommandPicker.tsx +0 -77
- package/src/tui/interactions/__tests__/CommandConfirm.test.tsx +0 -124
- package/src/tui/interactions/__tests__/CommandPicker.test.tsx +0 -138
- package/src/tui/plugin-tui-handlers.ts +0 -163
- package/src/tui/render-markdown.ts +0 -130
- package/src/tui/render.tsx +0 -129
- package/src/tui/session-naming.ts +0 -33
- package/src/tui/status-activity.ts +0 -63
- package/src/tui/tui-cli-adapter-context.tsx +0 -13
- package/src/tui/tui-cli-adapter.ts +0 -25
- package/src/tui/tui-state-manager.ts +0 -226
- package/src/tui/tui-transport.ts +0 -35
- package/src/tui/types.ts +0 -15
- package/src/tui/utils/__tests__/edit-diff.test.ts +0 -426
- package/src/tui/utils/__tests__/paste-detection.test.ts +0 -116
- package/src/tui/utils/__tests__/paste-labels.test.ts +0 -46
- package/src/tui/utils/__tests__/tool-call-extractor.test.ts +0 -227
- package/src/tui/utils/__tests__/tool-diff-summary.test.ts +0 -104
- package/src/tui/utils/edit-diff.ts +0 -153
- package/src/tui/utils/paste-labels.ts +0 -9
- package/src/tui/utils/tool-call-extractor.ts +0 -92
- package/src/tui/utils/tool-diff-summary.ts +0 -75
- package/src/ws/__tests__/ws-handler.test.ts +0 -409
- package/src/ws/__tests__/ws-transport.test.ts +0 -53
- package/src/ws/index.ts +0 -13
- package/src/ws/ws-background-messages.ts +0 -170
- package/src/ws/ws-handler.ts +0 -280
- package/src/ws/ws-protocol.ts +0 -78
- package/src/ws/ws-transport-configurable.ts +0 -128
- package/src/ws/ws-transport.ts +0 -42
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"programmatic-70dxIXtS.js","names":[],"sources":["../../src/programmatic/ProgrammaticInteractionChannel.ts","../../src/programmatic/createProgrammaticAgent.ts"],"sourcesContent":["/**\n * ProgrammaticInteractionChannel — an in-process IInteractionChannel adapter (INFRA-019).\n *\n * The \"programmatic preset\" adapter slot reserved by the interaction contract: instead of an Ink TUI\n * or a print runner, this channel lets a caller push a message in-process and read back the structured\n * `InteractionEvent` stream the framework already emits. It uses the documented one-way `write()`\n * protocol consumed by `createInteractiveRuntime` (not the TUI's direct-wiring path).\n *\n * Production transport adapter — lives in transport core. Tests and automation consume it; it never\n * depends on test code.\n */\n\nimport type { IActionRequest, TActionResponse } from '@robota-sdk/agent-core';\nimport type {\n IInteractionChannel,\n ICommandInfo,\n InteractionEvent,\n} from '@robota-sdk/agent-interface-transport';\n\nexport class ProgrammaticInteractionChannel implements IInteractionChannel {\n /** Full structured event stream pushed by the framework, in order. */\n readonly events: InteractionEvent[] = [];\n\n availableCommands: ICommandInfo[] = [];\n busy = false;\n started = false;\n stopped = false;\n\n private submitHandler: ((text: string) => Promise<void>) | null = null;\n private readonly userActionResponses: TActionResponse[] = [];\n\n // ── IInteractionChannel ──────────────────────────────────────\n\n onSubmit(handler: (text: string) => Promise<void>): void {\n this.submitHandler = handler;\n }\n\n write(event: InteractionEvent): void {\n this.events.push(event);\n }\n\n /**\n * CMD-004 unified ask. Resolves from the pre-supplied queue (FIFO); an empty queue resolves\n * `{ type: 'cancelled' }` so a programmatic run never blocks on an un-answered question.\n */\n async askUser(_request: IActionRequest): Promise<TActionResponse> {\n return this.userActionResponses.shift() ?? { type: 'cancelled' };\n }\n\n setAvailableCommands(commands: ICommandInfo[]): void {\n this.availableCommands = commands;\n }\n\n setBusy(busy: boolean): void {\n this.busy = busy;\n }\n\n async start(): Promise<void> {\n this.started = true;\n }\n\n async stop(): Promise<void> {\n this.stopped = true;\n }\n\n // ── Programmatic driving surface ─────────────────────────────\n\n /** Push a user submission into the framework (the programmatic \"user types and presses enter\"). */\n async submit(text: string): Promise<void> {\n if (!this.submitHandler) {\n throw new Error(\n 'ProgrammaticInteractionChannel: no submit handler registered — start the runtime first',\n );\n }\n await this.submitHandler(text);\n }\n\n /** Pre-answer the next `askUser` (CMD-004 unified ask). */\n queueUserAction(response: TActionResponse): void {\n this.userActionResponses.push(response);\n }\n}\n","/**\n * createProgrammaticAgent — the in-process implementation of the client-side agent contract\n * (`IAgentDriver`, INFRA-020; introduced as the programmatic driver in INFRA-019).\n *\n * Wraps `createInteractiveRuntime` with a {@link ProgrammaticInteractionChannel} so a caller can drive\n * the real agent structurally: `start()`, `send(text)` (awaits the whole turn), then read assistant\n * replies / tool calls / errors as data — no terminal, no PTY, no scraping. The observation accessors\n * delegate to the shared `read*` helpers in `@robota-sdk/agent-interface-transport`, so the\n * filter/derivation logic is not re-implemented here.\n */\n\nimport { createInteractiveRuntime } from '@robota-sdk/agent-framework';\nimport {\n readAssistantReplies,\n readErrors,\n readLastAssistantText,\n readToolCalls,\n} from '@robota-sdk/agent-interface-transport';\n\nimport { ProgrammaticInteractionChannel } from './ProgrammaticInteractionChannel.js';\n\nimport type { IAIProvider, TActionResponse, TPermissionMode } from '@robota-sdk/agent-core';\nimport type { ICommandModule, IInteractiveRuntime } from '@robota-sdk/agent-framework';\nimport type { IAgentDriver, IInteractiveSessionStore } from '@robota-sdk/agent-interface-transport';\n\nexport interface ICreateProgrammaticAgentOptions {\n /** Provider that answers the agent loop (e.g. a real provider, or the scripted provider in tests). */\n provider: IAIProvider;\n /** Working directory for session creation. */\n cwd: string;\n /** Slash-command modules to register (defaults to none). */\n commandModules?: readonly ICommandModule[];\n /** Optional session store for persistence. */\n sessionStore?: IInteractiveSessionStore;\n /** Permission mode for tool execution (e.g. `'bypassPermissions'` for unattended driving). */\n permissionMode?: TPermissionMode;\n}\n\n/**\n * Construct an in-process {@link IAgentDriver} bound to a real `InteractiveSession`. The returned\n * driver's accessors are the shared `read*` helpers applied to the captured event stream.\n */\nexport function createProgrammaticAgent(options: ICreateProgrammaticAgentOptions): IAgentDriver {\n const channel = new ProgrammaticInteractionChannel();\n const runtime: IInteractiveRuntime = createInteractiveRuntime({\n channel,\n commandModules: options.commandModules ?? [],\n provider: options.provider,\n cwd: options.cwd,\n sessionStore: options.sessionStore,\n permissionMode: options.permissionMode,\n });\n\n let started = false;\n\n return {\n events: channel.events,\n start: async (): Promise<void> => {\n if (started) return;\n started = true;\n await runtime.start();\n },\n send: (text: string): Promise<void> => channel.submit(text),\n queueUserAction: (response: TActionResponse): void => channel.queueUserAction(response),\n assistantReplies: (): string[] => readAssistantReplies(channel.events),\n lastAssistantText: (): string | undefined => readLastAssistantText(channel.events),\n toolCalls: () => readToolCalls(channel.events),\n errors: (): Error[] => readErrors(channel.events),\n stop: (): Promise<void> => runtime.stop(),\n };\n}\n"],"mappings":"kNAmBA,IAAa,EAAb,KAA2E,CAEzE,OAAsC,CAAC,EAEvC,kBAAoC,CAAC,EACrC,KAAO,GACP,QAAU,GACV,QAAU,GAEV,cAAkE,KAClE,oBAA0D,CAAC,EAI3D,SAAS,EAAgD,CACvD,KAAK,cAAgB,CACvB,CAEA,MAAM,EAA+B,CACnC,KAAK,OAAO,KAAK,CAAK,CACxB,CAMA,MAAM,QAAQ,EAAoD,CAChE,OAAO,KAAK,oBAAoB,MAAM,GAAK,CAAE,KAAM,WAAY,CACjE,CAEA,qBAAqB,EAAgC,CACnD,KAAK,kBAAoB,CAC3B,CAEA,QAAQ,EAAqB,CAC3B,KAAK,KAAO,CACd,CAEA,MAAM,OAAuB,CAC3B,KAAK,QAAU,EACjB,CAEA,MAAM,MAAsB,CAC1B,KAAK,QAAU,EACjB,CAKA,MAAM,OAAO,EAA6B,CACxC,GAAI,CAAC,KAAK,cACR,MAAU,MACR,wFACF,EAEF,MAAM,KAAK,cAAc,CAAI,CAC/B,CAGA,gBAAgB,EAAiC,CAC/C,KAAK,oBAAoB,KAAK,CAAQ,CACxC,CACF,ECvCA,SAAgB,EAAwB,EAAwD,CAC9F,IAAM,EAAU,IAAI,EACd,EAA+B,EAAyB,CAC5D,UACA,eAAgB,EAAQ,gBAAkB,CAAC,EAC3C,SAAU,EAAQ,SAClB,IAAK,EAAQ,IACb,aAAc,EAAQ,aACtB,eAAgB,EAAQ,cAC1B,CAAC,EAEG,EAAU,GAEd,MAAO,CACL,OAAQ,EAAQ,OAChB,MAAO,SAA2B,CAC5B,IACJ,EAAU,GACV,MAAM,EAAQ,MAAM,EACtB,EACA,KAAO,GAAgC,EAAQ,OAAO,CAAI,EAC1D,gBAAkB,GAAoC,EAAQ,gBAAgB,CAAQ,EACtF,qBAAkC,EAAqB,EAAQ,MAAM,EACrE,sBAA6C,EAAsB,EAAQ,MAAM,EACjF,cAAiB,EAAc,EAAQ,MAAM,EAC7C,WAAuB,EAAW,EAAQ,MAAM,EAChD,SAA2B,EAAQ,KAAK,CAC1C,CACF"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});
|
|
1
|
+
Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});let e=require("@robota-sdk/agent-core/testing");Object.defineProperty(exports,"createScriptedProvider",{enumerable:!0,get:function(){return e.createScriptedProvider}});
|
|
@@ -1,21 +1,2 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
//#region src/testing/scripted-provider.d.ts
|
|
4
|
-
/** One scripted assistant turn: plain text or tool invocations. */
|
|
5
|
-
type TScriptedTurn = {
|
|
6
|
-
text: string;
|
|
7
|
-
} | {
|
|
8
|
-
toolCalls: ReadonlyArray<{
|
|
9
|
-
name: string;
|
|
10
|
-
args: Record<string, unknown>;
|
|
11
|
-
}>;
|
|
12
|
-
};
|
|
13
|
-
interface IScriptedProvider {
|
|
14
|
-
provider: IAIProvider;
|
|
15
|
-
/** Message arrays of every chat() call, in order, for request assertions. */
|
|
16
|
-
requests: TUniversalMessage[][];
|
|
17
|
-
}
|
|
18
|
-
declare function createScriptedProvider(turns: readonly TScriptedTurn[]): IScriptedProvider;
|
|
19
|
-
//#endregion
|
|
20
|
-
export { type IScriptedProvider, type TScriptedTurn, createScriptedProvider };
|
|
21
|
-
//# sourceMappingURL=index.d.ts.map
|
|
1
|
+
import { IScriptedProvider, TScriptedTurn, createScriptedProvider } from "@robota-sdk/agent-core/testing";
|
|
2
|
+
export { type IScriptedProvider, type TScriptedTurn, createScriptedProvider };
|
|
@@ -1,2 +1 @@
|
|
|
1
|
-
|
|
2
|
-
//# sourceMappingURL=index.js.map
|
|
1
|
+
import{createScriptedProvider as e}from"@robota-sdk/agent-core/testing";export{e as createScriptedProvider};
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@robota-sdk/agent-transport",
|
|
3
|
-
"version": "3.0.0-beta.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "3.0.0-beta.77",
|
|
4
|
+
"description": "Core transport package for Robota SDK — headless adapter, scripted-provider testing fixtures, and the transport registry",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/node/index.js",
|
|
7
7
|
"types": "dist/node/index.d.ts",
|
|
@@ -42,52 +42,16 @@
|
|
|
42
42
|
"require": "./dist/node/testing/index.cjs"
|
|
43
43
|
}
|
|
44
44
|
},
|
|
45
|
-
"./
|
|
46
|
-
"types": "./dist/node/
|
|
47
|
-
"source": "./src/
|
|
45
|
+
"./programmatic": {
|
|
46
|
+
"types": "./dist/node/programmatic/index.d.ts",
|
|
47
|
+
"source": "./src/programmatic/index.ts",
|
|
48
48
|
"node": {
|
|
49
|
-
"import": "./dist/node/
|
|
50
|
-
"require": "./dist/node/
|
|
49
|
+
"import": "./dist/node/programmatic/index.js",
|
|
50
|
+
"require": "./dist/node/programmatic/index.cjs"
|
|
51
51
|
},
|
|
52
52
|
"default": {
|
|
53
|
-
"import": "./dist/node/
|
|
54
|
-
"require": "./dist/node/
|
|
55
|
-
}
|
|
56
|
-
},
|
|
57
|
-
"./ws": {
|
|
58
|
-
"types": "./dist/node/ws/index.d.ts",
|
|
59
|
-
"source": "./src/ws/index.ts",
|
|
60
|
-
"node": {
|
|
61
|
-
"import": "./dist/node/ws/index.js",
|
|
62
|
-
"require": "./dist/node/ws/index.cjs"
|
|
63
|
-
},
|
|
64
|
-
"default": {
|
|
65
|
-
"import": "./dist/node/ws/index.js",
|
|
66
|
-
"require": "./dist/node/ws/index.cjs"
|
|
67
|
-
}
|
|
68
|
-
},
|
|
69
|
-
"./mcp": {
|
|
70
|
-
"types": "./dist/node/mcp/index.d.ts",
|
|
71
|
-
"source": "./src/mcp/index.ts",
|
|
72
|
-
"node": {
|
|
73
|
-
"import": "./dist/node/mcp/index.js",
|
|
74
|
-
"require": "./dist/node/mcp/index.cjs"
|
|
75
|
-
},
|
|
76
|
-
"default": {
|
|
77
|
-
"import": "./dist/node/mcp/index.js",
|
|
78
|
-
"require": "./dist/node/mcp/index.cjs"
|
|
79
|
-
}
|
|
80
|
-
},
|
|
81
|
-
"./tui": {
|
|
82
|
-
"types": "./dist/node/tui/index.d.ts",
|
|
83
|
-
"source": "./src/tui/index.ts",
|
|
84
|
-
"node": {
|
|
85
|
-
"import": "./dist/node/tui/index.js",
|
|
86
|
-
"require": "./dist/node/tui/index.cjs"
|
|
87
|
-
},
|
|
88
|
-
"default": {
|
|
89
|
-
"import": "./dist/node/tui/index.js",
|
|
90
|
-
"require": "./dist/node/tui/index.cjs"
|
|
53
|
+
"import": "./dist/node/programmatic/index.js",
|
|
54
|
+
"require": "./dist/node/programmatic/index.cjs"
|
|
91
55
|
}
|
|
92
56
|
}
|
|
93
57
|
},
|
|
@@ -107,38 +71,19 @@
|
|
|
107
71
|
"src"
|
|
108
72
|
],
|
|
109
73
|
"dependencies": {
|
|
110
|
-
"@
|
|
111
|
-
"
|
|
112
|
-
"
|
|
113
|
-
"ink": "^7.0.5",
|
|
114
|
-
"ink-select-input": "^6.2.0",
|
|
115
|
-
"ink-spinner": "^5.0.0",
|
|
116
|
-
"ink-text-input": "^6.0.0",
|
|
117
|
-
"marked": "^9.1.6",
|
|
118
|
-
"marked-terminal": "^7.3.0",
|
|
119
|
-
"react": "19.2.4",
|
|
120
|
-
"string-width": "^8.2.1",
|
|
121
|
-
"ws": "^8.21.0",
|
|
122
|
-
"zod": "^3.25.76",
|
|
123
|
-
"@robota-sdk/agent-core": "3.0.0-beta.75",
|
|
124
|
-
"@robota-sdk/agent-framework": "3.0.0-beta.75",
|
|
125
|
-
"@robota-sdk/agent-interface-transport": "3.0.0-beta.75",
|
|
126
|
-
"@robota-sdk/agent-interface-tui": "3.0.0-beta.75"
|
|
74
|
+
"@robota-sdk/agent-core": "3.0.0-beta.77",
|
|
75
|
+
"@robota-sdk/agent-framework": "3.0.0-beta.77",
|
|
76
|
+
"@robota-sdk/agent-interface-transport": "3.0.0-beta.77"
|
|
127
77
|
},
|
|
128
78
|
"devDependencies": {
|
|
129
|
-
"@homebridge/node-pty-prebuilt-multiarch": "^0.13.1",
|
|
130
|
-
"@modelcontextprotocol/sdk": "^1.28.0",
|
|
131
|
-
"@types/react": "^19.2.17",
|
|
132
|
-
"@types/ws": "^8.18.1",
|
|
133
|
-
"ink-testing-library": "^4.0.0",
|
|
134
79
|
"rimraf": "^5.0.10",
|
|
135
80
|
"tsdown": "^0.22.2",
|
|
136
81
|
"tsx": "^4.22.4",
|
|
137
82
|
"typescript": "^5.9.3",
|
|
138
83
|
"vitest": "^3.2.6",
|
|
139
|
-
"@robota-sdk/agent-command": "3.0.0-beta.
|
|
84
|
+
"@robota-sdk/agent-command": "3.0.0-beta.77"
|
|
140
85
|
},
|
|
141
|
-
"license": "
|
|
86
|
+
"license": "AGPL-3.0-only OR LicenseRef-Commercial",
|
|
142
87
|
"publishConfig": {
|
|
143
88
|
"access": "public"
|
|
144
89
|
},
|
|
@@ -149,7 +94,6 @@
|
|
|
149
94
|
"typecheck": "tsc --noEmit",
|
|
150
95
|
"test": "vitest run --passWithNoTests",
|
|
151
96
|
"test:coverage": "vitest run --coverage --passWithNoTests",
|
|
152
|
-
"clean": "rimraf dist"
|
|
153
|
-
"test:pty": "vitest run --config vitest.pty.config.ts"
|
|
97
|
+
"clean": "rimraf dist"
|
|
154
98
|
}
|
|
155
99
|
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CMD-004 PR-E: ProgrammaticInteractionChannel.askUser() — the unified ask path on the programmatic
|
|
3
|
+
* channel. FIFO from the pre-supplied queue; empty queue → cancelled (never blocks a headless run).
|
|
4
|
+
*/
|
|
5
|
+
import { describe, expect, it } from 'vitest';
|
|
6
|
+
|
|
7
|
+
import { ProgrammaticInteractionChannel } from '../../programmatic/ProgrammaticInteractionChannel.js';
|
|
8
|
+
|
|
9
|
+
import type { IActionRequest } from '@robota-sdk/agent-core';
|
|
10
|
+
|
|
11
|
+
const REQUEST: IActionRequest = {
|
|
12
|
+
id: 'q',
|
|
13
|
+
title: 'Pick',
|
|
14
|
+
options: [{ value: 'a', label: 'A' }],
|
|
15
|
+
maxSelect: 1,
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
describe('ProgrammaticInteractionChannel.askUser', () => {
|
|
19
|
+
it('resolves cancelled when no answer is queued', async () => {
|
|
20
|
+
const channel = new ProgrammaticInteractionChannel();
|
|
21
|
+
expect(await channel.askUser(REQUEST)).toEqual({ type: 'cancelled' });
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('resolves queued answers FIFO, then cancelled once drained', async () => {
|
|
25
|
+
const channel = new ProgrammaticInteractionChannel();
|
|
26
|
+
channel.queueUserAction({ type: 'answer', values: ['a'] });
|
|
27
|
+
channel.queueUserAction({ type: 'cancelled' });
|
|
28
|
+
expect(await channel.askUser(REQUEST)).toEqual({ type: 'answer', values: ['a'] });
|
|
29
|
+
expect(await channel.askUser(REQUEST)).toEqual({ type: 'cancelled' });
|
|
30
|
+
expect(await channel.askUser(REQUEST)).toEqual({ type: 'cancelled' });
|
|
31
|
+
});
|
|
32
|
+
});
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* INFRA-019: in-process programmatic driving of the REAL agent.
|
|
3
|
+
*
|
|
4
|
+
* Drives `createProgrammaticAgent` (→ `createInteractiveRuntime` → a real `InteractiveSession`) with
|
|
5
|
+
* the deterministic scripted provider. No terminal, no PTY, no mocks of the framework loop: a message
|
|
6
|
+
* is pushed in-process and the structured `InteractionEvent` stream is asserted as data. This is the
|
|
7
|
+
* in-process form of "drive the agent at will" (TEST-008 north star).
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
11
|
+
import { tmpdir } from 'node:os';
|
|
12
|
+
import { join } from 'node:path';
|
|
13
|
+
|
|
14
|
+
import { createScriptedProvider } from '@robota-sdk/agent-core/testing';
|
|
15
|
+
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
16
|
+
|
|
17
|
+
import {
|
|
18
|
+
createProgrammaticAgent,
|
|
19
|
+
ProgrammaticInteractionChannel,
|
|
20
|
+
} from '../../programmatic/index.js';
|
|
21
|
+
|
|
22
|
+
import type { IAgentDriver } from '@robota-sdk/agent-interface-transport';
|
|
23
|
+
|
|
24
|
+
describe('programmatic in-process agent driver (INFRA-019)', () => {
|
|
25
|
+
let cwd: string;
|
|
26
|
+
let driver: IAgentDriver | undefined;
|
|
27
|
+
|
|
28
|
+
beforeEach(() => {
|
|
29
|
+
cwd = mkdtempSync(join(tmpdir(), 'robota-programmatic-'));
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
afterEach(async () => {
|
|
33
|
+
await driver?.stop();
|
|
34
|
+
driver = undefined;
|
|
35
|
+
rmSync(cwd, { recursive: true, force: true });
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('TC-01/02: send() drives a real turn and the scripted assistant reply is captured as data', async () => {
|
|
39
|
+
const scripted = createScriptedProvider([{ text: 'DRIVEN_ANSWER_42' }]);
|
|
40
|
+
driver = createProgrammaticAgent({ provider: scripted.provider, cwd });
|
|
41
|
+
|
|
42
|
+
await driver.start();
|
|
43
|
+
await driver.send('hello');
|
|
44
|
+
|
|
45
|
+
// TC-02: the reply is captured structurally — no terminal involved.
|
|
46
|
+
expect(driver.lastAssistantText()).toBe('DRIVEN_ANSWER_42');
|
|
47
|
+
expect(driver.assistantReplies()).toEqual(['DRIVEN_ANSWER_42']);
|
|
48
|
+
|
|
49
|
+
// Event order: the user message is recorded before the assistant completion.
|
|
50
|
+
const types = driver.events.map((e) => e.type);
|
|
51
|
+
expect(types).toContain('user-message');
|
|
52
|
+
expect(types).toContain('assistant-done');
|
|
53
|
+
expect(types.indexOf('user-message')).toBeLessThan(types.indexOf('assistant-done'));
|
|
54
|
+
expect(driver.errors()).toEqual([]);
|
|
55
|
+
|
|
56
|
+
// The scripted provider saw exactly the driven message.
|
|
57
|
+
const firstRequest = scripted.requests[0] ?? [];
|
|
58
|
+
const contents = firstRequest.map((m) => String(m.content));
|
|
59
|
+
expect(contents.some((c) => c.includes('hello'))).toBe(true);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('TC-03: a scripted tool-call turn runs the real tool loop and is captured in toolCalls()', async () => {
|
|
63
|
+
const target = join(cwd, 'greet.txt');
|
|
64
|
+
writeFileSync(target, 'Hello, world\n', 'utf8');
|
|
65
|
+
|
|
66
|
+
const scripted = createScriptedProvider([
|
|
67
|
+
{
|
|
68
|
+
toolCalls: [
|
|
69
|
+
{ name: 'Edit', args: { filePath: target, oldString: 'Hello', newString: 'Goodbye' } },
|
|
70
|
+
],
|
|
71
|
+
},
|
|
72
|
+
{ text: 'edit done' },
|
|
73
|
+
]);
|
|
74
|
+
driver = createProgrammaticAgent({
|
|
75
|
+
provider: scripted.provider,
|
|
76
|
+
cwd,
|
|
77
|
+
permissionMode: 'bypassPermissions',
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
await driver.start();
|
|
81
|
+
await driver.send('change the greeting');
|
|
82
|
+
|
|
83
|
+
// The real tool loop executed: the file on disk was mutated.
|
|
84
|
+
expect(readFileSync(target, 'utf8')).toBe('Goodbye, world\n');
|
|
85
|
+
// The tool call surfaced as structured data.
|
|
86
|
+
const calls = driver.toolCalls();
|
|
87
|
+
expect(calls.some((c) => c.name === 'Edit')).toBe(true);
|
|
88
|
+
expect(driver.lastAssistantText()).toBe('edit done');
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('TC-04: queueUserAction pre-answers an askUser; empty queue resolves to cancelled', async () => {
|
|
92
|
+
const scripted = createScriptedProvider([{ text: 'noop' }]);
|
|
93
|
+
driver = createProgrammaticAgent({ provider: scripted.provider, cwd });
|
|
94
|
+
await driver.start();
|
|
95
|
+
|
|
96
|
+
// Exercise the unified ask contract in isolation (the FIFO queue the driver's queueUserAction feeds).
|
|
97
|
+
const channel = new ProgrammaticInteractionChannel();
|
|
98
|
+
const request = { id: 'x', title: 'ok?', options: [{ value: 'a', label: 'A' }], maxSelect: 1 };
|
|
99
|
+
|
|
100
|
+
channel.queueUserAction({ type: 'answer', values: ['a'] });
|
|
101
|
+
const answered = await channel.askUser(request);
|
|
102
|
+
expect(answered).toEqual({ type: 'answer', values: ['a'] });
|
|
103
|
+
|
|
104
|
+
const defaulted = await channel.askUser(request);
|
|
105
|
+
expect(defaulted).toEqual({ type: 'cancelled' });
|
|
106
|
+
});
|
|
107
|
+
});
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TEST-009 Phase 2: conversation + slash-command flows driven through `IAgentDriver`.
|
|
3
|
+
*
|
|
4
|
+
* Exercises the in-process driver (`createProgrammaticAgent`) against the REAL framework loop with the
|
|
5
|
+
* scripted provider: multi-turn context, a slash command producing a `command-result`, and the CMD-004
|
|
6
|
+
* ask seam (a command self-asks via `getUserInteraction()`, pre-answered through `queueUserAction`).
|
|
7
|
+
* All assertions read the shared `InteractionEvent` stream the contract exposes.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { mkdtempSync, rmSync } from 'node:fs';
|
|
11
|
+
import { tmpdir } from 'node:os';
|
|
12
|
+
import { join } from 'node:path';
|
|
13
|
+
|
|
14
|
+
import { confirmAction, isConfirmed } from '@robota-sdk/agent-core';
|
|
15
|
+
import { createScriptedProvider } from '@robota-sdk/agent-core/testing';
|
|
16
|
+
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
17
|
+
|
|
18
|
+
import { createProgrammaticAgent } from '../../programmatic/index.js';
|
|
19
|
+
|
|
20
|
+
import type { ICommandModule } from '@robota-sdk/agent-framework';
|
|
21
|
+
import type { IAgentDriver, InteractionEvent } from '@robota-sdk/agent-interface-transport';
|
|
22
|
+
|
|
23
|
+
function commandResults(
|
|
24
|
+
events: readonly InteractionEvent[],
|
|
25
|
+
): Array<{ name: string; output: string }> {
|
|
26
|
+
return events
|
|
27
|
+
.filter(
|
|
28
|
+
(e): e is Extract<InteractionEvent, { type: 'command-result' }> =>
|
|
29
|
+
e.type === 'command-result',
|
|
30
|
+
)
|
|
31
|
+
.map((e) => ({ name: e.name, output: e.output }));
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** A read-only `/ping` command that returns a fixed result — no host adapters needed. */
|
|
35
|
+
const PING_MODULE: ICommandModule = {
|
|
36
|
+
name: 'ping',
|
|
37
|
+
systemCommands: [
|
|
38
|
+
{
|
|
39
|
+
name: 'ping',
|
|
40
|
+
description: 'test ping',
|
|
41
|
+
userInvocable: true,
|
|
42
|
+
safety: 'read-only',
|
|
43
|
+
requiresPermission: false,
|
|
44
|
+
execute: () => ({ message: 'PONG', success: true }),
|
|
45
|
+
},
|
|
46
|
+
],
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
/** A `/danger` command that self-asks for confirmation via the CMD-004 ask seam. */
|
|
50
|
+
const CONFIRM_MODULE: ICommandModule = {
|
|
51
|
+
name: 'danger',
|
|
52
|
+
systemCommands: [
|
|
53
|
+
{
|
|
54
|
+
name: 'danger',
|
|
55
|
+
description: 'requires confirmation',
|
|
56
|
+
userInvocable: true,
|
|
57
|
+
safety: 'read-only',
|
|
58
|
+
requiresPermission: false,
|
|
59
|
+
lifecycle: 'inline',
|
|
60
|
+
execute: async (context) => {
|
|
61
|
+
const ui = context.getUserInteraction?.();
|
|
62
|
+
const response = ui ? await ui.ask(confirmAction('danger', 'Are you sure?')) : undefined;
|
|
63
|
+
if (!response || !isConfirmed(response)) {
|
|
64
|
+
return { message: 'DANGER_CANCELLED', success: true };
|
|
65
|
+
}
|
|
66
|
+
return { message: 'DANGER_RAN', success: true };
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
],
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
describe('IAgentDriver conversation + slash flows (TEST-009 Phase 2)', () => {
|
|
73
|
+
let cwd: string;
|
|
74
|
+
let driver: IAgentDriver | undefined;
|
|
75
|
+
|
|
76
|
+
beforeEach(() => {
|
|
77
|
+
cwd = mkdtempSync(join(tmpdir(), 'robota-flows-'));
|
|
78
|
+
});
|
|
79
|
+
afterEach(async () => {
|
|
80
|
+
await driver?.stop();
|
|
81
|
+
driver = undefined;
|
|
82
|
+
rmSync(cwd, { recursive: true, force: true });
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('carries multi-turn conversation context across sends', async () => {
|
|
86
|
+
const scripted = createScriptedProvider([{ text: 'NOTED_42' }, { text: 'IT_WAS_42' }]);
|
|
87
|
+
driver = createProgrammaticAgent({ provider: scripted.provider, cwd });
|
|
88
|
+
await driver.start();
|
|
89
|
+
|
|
90
|
+
await driver.send('Remember the number 42');
|
|
91
|
+
expect(driver.lastAssistantText()).toBe('NOTED_42');
|
|
92
|
+
|
|
93
|
+
await driver.send('What number?');
|
|
94
|
+
expect(driver.lastAssistantText()).toBe('IT_WAS_42');
|
|
95
|
+
expect(driver.assistantReplies()).toEqual(['NOTED_42', 'IT_WAS_42']);
|
|
96
|
+
|
|
97
|
+
// The second request must carry the prior turn (real session context).
|
|
98
|
+
const second = (scripted.requests[1] ?? []).map((m) => String(m.content)).join('\n');
|
|
99
|
+
expect(second).toContain('Remember the number 42');
|
|
100
|
+
expect(second).toContain('NOTED_42');
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('runs a slash command and surfaces a command-result event', async () => {
|
|
104
|
+
const scripted = createScriptedProvider([{ text: 'unused' }]);
|
|
105
|
+
driver = createProgrammaticAgent({
|
|
106
|
+
provider: scripted.provider,
|
|
107
|
+
cwd,
|
|
108
|
+
commandModules: [PING_MODULE],
|
|
109
|
+
});
|
|
110
|
+
await driver.start();
|
|
111
|
+
|
|
112
|
+
await driver.send('/ping');
|
|
113
|
+
expect(commandResults(driver.events)).toEqual([{ name: 'ping', output: 'PONG' }]);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it('drives the ask seam: queueUserAction confirms a self-asking command; empty queue cancels it', async () => {
|
|
117
|
+
const scripted = createScriptedProvider([{ text: 'unused' }, { text: 'unused' }]);
|
|
118
|
+
driver = createProgrammaticAgent({
|
|
119
|
+
provider: scripted.provider,
|
|
120
|
+
cwd,
|
|
121
|
+
commandModules: [CONFIRM_MODULE],
|
|
122
|
+
});
|
|
123
|
+
await driver.start();
|
|
124
|
+
|
|
125
|
+
// Confirmed → the command runs (askUser resolved from the pre-supplied queue).
|
|
126
|
+
driver.queueUserAction({ type: 'answer', values: ['yes'] });
|
|
127
|
+
await driver.send('/danger');
|
|
128
|
+
const ranAfterConfirm = commandResults(driver.events).filter(
|
|
129
|
+
(c) => c.output === 'DANGER_RAN',
|
|
130
|
+
).length;
|
|
131
|
+
expect(ranAfterConfirm).toBe(1);
|
|
132
|
+
|
|
133
|
+
// Empty queue → askUser resolves `cancelled` → the command takes its cancel path (does NOT run).
|
|
134
|
+
await driver.send('/danger');
|
|
135
|
+
expect(commandResults(driver.events).filter((c) => c.output === 'DANGER_RAN').length).toBe(
|
|
136
|
+
ranAfterConfirm,
|
|
137
|
+
);
|
|
138
|
+
expect(commandResults(driver.events).some((c) => c.output === 'DANGER_CANCELLED')).toBe(true);
|
|
139
|
+
});
|
|
140
|
+
});
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { mkdtempSync } from 'node:fs';
|
|
2
|
+
import { tmpdir } from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
|
|
5
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
6
|
+
|
|
7
|
+
import { TransportRegistry } from '../transport-registry';
|
|
8
|
+
|
|
9
|
+
import type { IInteractiveSession } from '@robota-sdk/agent-interface-transport';
|
|
10
|
+
import type { IConfigurableTransport } from '@robota-sdk/agent-interface-transport';
|
|
11
|
+
|
|
12
|
+
function makeTransport(
|
|
13
|
+
name: string,
|
|
14
|
+
stop: () => Promise<void>,
|
|
15
|
+
): IConfigurableTransport<IInteractiveSession> {
|
|
16
|
+
return {
|
|
17
|
+
name,
|
|
18
|
+
defaultEnabled: true,
|
|
19
|
+
attach: vi.fn(),
|
|
20
|
+
start: vi.fn().mockResolvedValue(undefined),
|
|
21
|
+
stop,
|
|
22
|
+
} as unknown as IConfigurableTransport<IInteractiveSession>;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
describe('TransportRegistry.stopAll (best-effort, CORE-013)', () => {
|
|
26
|
+
it('stops every transport even when one stop fails, and collects the error', async () => {
|
|
27
|
+
const registry = new TransportRegistry(
|
|
28
|
+
path.join(mkdtempSync(path.join(tmpdir(), 'registry-')), 'settings.json'),
|
|
29
|
+
);
|
|
30
|
+
const stoppedSecond = vi.fn().mockResolvedValue(undefined);
|
|
31
|
+
registry.register(
|
|
32
|
+
makeTransport('failing', async () => {
|
|
33
|
+
throw new Error('stop boom');
|
|
34
|
+
}),
|
|
35
|
+
);
|
|
36
|
+
registry.register(makeTransport('healthy', stoppedSecond));
|
|
37
|
+
|
|
38
|
+
const result = await registry.stopAll();
|
|
39
|
+
|
|
40
|
+
expect(stoppedSecond).toHaveBeenCalledTimes(1);
|
|
41
|
+
expect(result.errors).toHaveLength(1);
|
|
42
|
+
expect(result.errors[0].message).toBe('stop boom');
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('returns an empty error list when all stops succeed', async () => {
|
|
46
|
+
const registry = new TransportRegistry(
|
|
47
|
+
path.join(mkdtempSync(path.join(tmpdir(), 'registry-')), 'settings.json'),
|
|
48
|
+
);
|
|
49
|
+
registry.register(makeTransport('a', vi.fn().mockResolvedValue(undefined)));
|
|
50
|
+
|
|
51
|
+
await expect(registry.stopAll()).resolves.toEqual({ errors: [] });
|
|
52
|
+
});
|
|
53
|
+
});
|
|
@@ -64,15 +64,36 @@ export class HeadlessInteractionChannel {
|
|
|
64
64
|
}
|
|
65
65
|
|
|
66
66
|
async run(prompt: string): Promise<void> {
|
|
67
|
+
const session = this.createSession();
|
|
68
|
+
const runner = createHeadlessRunner({ session, outputFormat: this.opts.outputFormat });
|
|
69
|
+
this.exitCode = await runner.run(prompt);
|
|
70
|
+
await session.shutdown({ reason: 'prompt_input_exit', message: 'Headless transport complete' });
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* GOAL-001: run an autonomous goal to completion (or a stop condition) in headless mode.
|
|
75
|
+
* Mirrors {@link run} but drives the framework goal loop instead of a single prompt.
|
|
76
|
+
*/
|
|
77
|
+
async runGoal(objective: string, options: { maxIterations?: number } = {}): Promise<void> {
|
|
78
|
+
const session = this.createSession();
|
|
79
|
+
const runner = createHeadlessRunner({ session, outputFormat: this.opts.outputFormat });
|
|
80
|
+
this.exitCode = await runner.runGoal(objective, options);
|
|
81
|
+
await session.shutdown({ reason: 'prompt_input_exit', message: 'Headless goal complete' });
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
private createSession(): InteractiveSession {
|
|
67
85
|
const shellExec: TShellExecFn =
|
|
68
86
|
this.opts.shellExec ??
|
|
69
87
|
((command: string) =>
|
|
70
88
|
execSync(command, { timeout: 5000, encoding: 'utf-8', stdio: 'pipe' }).trimEnd());
|
|
71
89
|
|
|
72
|
-
|
|
90
|
+
return new InteractiveSession({
|
|
73
91
|
cwd: this.opts.cwd,
|
|
74
92
|
provider: this.opts.provider,
|
|
75
93
|
permissionMode: this.opts.permissionMode ?? 'bypassPermissions',
|
|
94
|
+
// CMD-004: headless has no interactive renderer, so it injects no askHandler — the session's
|
|
95
|
+
// getUserInteraction() returns undefined and each command applies its explicit no-human path
|
|
96
|
+
// (e.g. /mode reports current, /exit and /clear proceed — never a silent guess).
|
|
76
97
|
maxTurns: this.opts.maxTurns,
|
|
77
98
|
sessionStore: this.opts.sessionStore,
|
|
78
99
|
resumeSessionId: this.opts.resumeSessionId,
|
|
@@ -100,10 +121,6 @@ export class HeadlessInteractionChannel {
|
|
|
100
121
|
? { selfVerification: this.opts.selfVerification }
|
|
101
122
|
: {}),
|
|
102
123
|
});
|
|
103
|
-
|
|
104
|
-
const runner = createHeadlessRunner({ session, outputFormat: this.opts.outputFormat });
|
|
105
|
-
this.exitCode = await runner.run(prompt);
|
|
106
|
-
await session.shutdown({ reason: 'prompt_input_exit', message: 'Headless transport complete' });
|
|
107
124
|
}
|
|
108
125
|
|
|
109
126
|
getExitCode(): number {
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GOAL-001: headless autonomous goal runner. A fake session emits goal lifecycle events; the
|
|
3
|
+
* runner resolves to exit 0 when satisfied, GOAL_NOT_SATISFIED_EXIT_CODE at a bound, and 1 on error.
|
|
4
|
+
*/
|
|
5
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
6
|
+
|
|
7
|
+
import { createHeadlessRunner, GOAL_NOT_SATISFIED_EXIT_CODE } from '../headless-runner.js';
|
|
8
|
+
|
|
9
|
+
import type {
|
|
10
|
+
IGoalEvent,
|
|
11
|
+
IGoalState,
|
|
12
|
+
IInteractiveSession,
|
|
13
|
+
} from '@robota-sdk/agent-interface-transport';
|
|
14
|
+
|
|
15
|
+
type TListener = (...args: unknown[]) => void;
|
|
16
|
+
|
|
17
|
+
function goalState(overrides: Partial<IGoalState> = {}): IGoalState {
|
|
18
|
+
return {
|
|
19
|
+
id: 'g1',
|
|
20
|
+
objective: 'do it',
|
|
21
|
+
status: 'satisfied',
|
|
22
|
+
iterations: 2,
|
|
23
|
+
maxIterations: 25,
|
|
24
|
+
startedAt: '2026-06-27T00:00:00.000Z',
|
|
25
|
+
progress: [],
|
|
26
|
+
...overrides,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Minimal fake session whose setGoal drives a scripted sequence of events. */
|
|
31
|
+
function createFakeSession(
|
|
32
|
+
script: (emit: (e: string, ...a: unknown[]) => void) => void,
|
|
33
|
+
): IInteractiveSession {
|
|
34
|
+
const listeners = new Map<string, Set<TListener>>();
|
|
35
|
+
const emit = (event: string, ...args: unknown[]): void => {
|
|
36
|
+
for (const fn of listeners.get(event) ?? []) fn(...args);
|
|
37
|
+
};
|
|
38
|
+
return {
|
|
39
|
+
on: (event: string, handler: TListener) => {
|
|
40
|
+
if (!listeners.has(event)) listeners.set(event, new Set());
|
|
41
|
+
listeners.get(event)!.add(handler);
|
|
42
|
+
},
|
|
43
|
+
off: (event: string, handler: TListener) => listeners.get(event)?.delete(handler),
|
|
44
|
+
setGoal: async () => {
|
|
45
|
+
queueMicrotask(() => script(emit));
|
|
46
|
+
return goalState();
|
|
47
|
+
},
|
|
48
|
+
getSession: () => ({ getSessionId: () => 'sess' }),
|
|
49
|
+
} as unknown as IInteractiveSession;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const stdoutChunks: string[] = [];
|
|
53
|
+
const stderrChunks: string[] = [];
|
|
54
|
+
|
|
55
|
+
beforeEach(() => {
|
|
56
|
+
stdoutChunks.length = 0;
|
|
57
|
+
stderrChunks.length = 0;
|
|
58
|
+
vi.spyOn(process.stdout, 'write').mockImplementation(((chunk: unknown) => {
|
|
59
|
+
stdoutChunks.push(String(chunk));
|
|
60
|
+
return true;
|
|
61
|
+
}) as never);
|
|
62
|
+
vi.spyOn(process.stderr, 'write').mockImplementation(((chunk: unknown) => {
|
|
63
|
+
stderrChunks.push(String(chunk));
|
|
64
|
+
return true;
|
|
65
|
+
}) as never);
|
|
66
|
+
});
|
|
67
|
+
afterEach(() => {
|
|
68
|
+
vi.restoreAllMocks();
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
describe('headless runGoal', () => {
|
|
72
|
+
it('exits 0 and prints turn responses when the goal is satisfied', async () => {
|
|
73
|
+
const session = createFakeSession((emit) => {
|
|
74
|
+
emit('complete', { response: 'did step 1' });
|
|
75
|
+
emit('goal_event', {
|
|
76
|
+
type: 'goal_started',
|
|
77
|
+
goal: goalState({ status: 'active' }),
|
|
78
|
+
} as IGoalEvent);
|
|
79
|
+
emit('goal_event', {
|
|
80
|
+
type: 'goal_stopped',
|
|
81
|
+
goal: goalState({ stopReason: 'satisfied' }),
|
|
82
|
+
} as IGoalEvent);
|
|
83
|
+
});
|
|
84
|
+
const code = await createHeadlessRunner({ session, outputFormat: 'text' }).runGoal('do it');
|
|
85
|
+
expect(code).toBe(0);
|
|
86
|
+
expect(stdoutChunks).toContain('did step 1\n');
|
|
87
|
+
expect(stdoutChunks.join('')).toContain('Goal satisfied');
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('exits GOAL_NOT_SATISFIED_EXIT_CODE when stopped at a bound', async () => {
|
|
91
|
+
const session = createFakeSession((emit) => {
|
|
92
|
+
emit('goal_event', {
|
|
93
|
+
type: 'goal_stopped',
|
|
94
|
+
goal: goalState({ status: 'stopped', stopReason: 'max-iterations' }),
|
|
95
|
+
} as IGoalEvent);
|
|
96
|
+
});
|
|
97
|
+
const code = await createHeadlessRunner({ session, outputFormat: 'text' }).runGoal('do it');
|
|
98
|
+
expect(code).toBe(GOAL_NOT_SATISFIED_EXIT_CODE);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('exits 1 on a turn error', async () => {
|
|
102
|
+
const session = createFakeSession((emit) => {
|
|
103
|
+
emit('error', new Error('provider exploded'));
|
|
104
|
+
});
|
|
105
|
+
const code = await createHeadlessRunner({ session, outputFormat: 'text' }).runGoal('do it');
|
|
106
|
+
expect(code).toBe(1);
|
|
107
|
+
});
|
|
108
|
+
});
|