@robota-sdk/agent-transport 3.0.0-beta.73 → 3.0.0-beta.74
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/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-DCtHvyVf.cjs → headless-BeHAOlIM.cjs} +4 -3
- package/dist/node/{headless-C6tj35h3.js → headless-D02zUEGh.js} +4 -3
- package/dist/node/headless-D02zUEGh.js.map +1 -0
- package/dist/node/http/index.cjs +1 -1
- package/dist/node/http/index.d.ts +1 -1
- package/dist/node/http/index.js +1 -1
- package/dist/node/{http-Br10Ps8m.js → http-2Jiuflc1.js} +1 -1
- package/dist/node/http-2Jiuflc1.js.map +1 -0
- package/dist/node/http-CBAvefLw.cjs +1 -0
- package/dist/node/{index-BVNhOeeU.d.ts → index-BQLN_Lc9.d.ts} +5 -3
- package/dist/node/index-BQLN_Lc9.d.ts.map +1 -0
- package/dist/node/{index-C9LWCL4l.d.ts → index-BnAGE-u9.d.ts} +2 -3
- package/dist/node/index-BnAGE-u9.d.ts.map +1 -0
- package/dist/node/{index-COWvtBa2.d.ts → index-BrQ4gGw0.d.ts} +3 -3
- package/dist/node/index-BrQ4gGw0.d.ts.map +1 -0
- package/dist/node/{index-X2Zg8FEY.d.ts → index-CoeBF21y.d.ts} +3 -3
- package/dist/node/index-CoeBF21y.d.ts.map +1 -0
- package/dist/node/{index-27HV5PJB.d.ts → index-DE3-dHqw.d.ts} +8 -3
- package/dist/node/index-DE3-dHqw.d.ts.map +1 -0
- package/dist/node/{index-BRgV_MPB.d.ts → index-DHt-2VQ-.d.ts} +2 -3
- package/dist/node/index-DHt-2VQ-.d.ts.map +1 -0
- package/dist/node/{index-nBlMTFkZ.d.ts → index-DMwKN5Le.d.ts} +2 -3
- package/dist/node/index-DMwKN5Le.d.ts.map +1 -0
- package/dist/node/{index-TMAlNHuM.d.ts → index-IvYaYY6v.d.ts} +5 -3
- package/dist/node/index-IvYaYY6v.d.ts.map +1 -0
- package/dist/node/{index-BRchlFBE.d.ts → index-WKTgvhlg.d.ts} +8 -3
- package/dist/node/index-WKTgvhlg.d.ts.map +1 -0
- package/dist/node/{index-C5KNEBO9.d.ts → index-c0M42fsA.d.ts} +2 -3
- package/dist/node/index-c0M42fsA.d.ts.map +1 -0
- package/dist/node/index.cjs +1 -1
- package/dist/node/index.d.ts +6 -7
- 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/mcp/index.cjs +1 -1
- package/dist/node/mcp/index.d.ts +1 -1
- package/dist/node/mcp/index.js +1 -1
- package/dist/node/mcp-BOglBJNy.cjs +1 -0
- package/dist/node/{mcp-BAujHOMr.js → mcp-D3BBVK7C.js} +1 -1
- package/dist/node/mcp-D3BBVK7C.js.map +1 -0
- package/dist/node/{chunk-Bmb41Sf3.cjs → rolldown-runtime-CMqjfN_6.cjs} +1 -1
- package/dist/node/testing/index.cjs +1 -0
- package/dist/node/testing/index.d.ts +21 -0
- package/dist/node/testing/index.d.ts.map +1 -0
- package/dist/node/testing/index.js +2 -0
- package/dist/node/testing/index.js.map +1 -0
- package/dist/node/tui/index.cjs +1 -1
- package/dist/node/tui/index.d.ts +1 -1
- package/dist/node/tui/index.js +1 -1
- package/dist/node/{tui-DIdvTeiT.js → tui-Btb1q88j.js} +4 -4
- package/dist/node/tui-Btb1q88j.js.map +1 -0
- package/dist/node/tui-SbUT7Zlt.cjs +24 -0
- package/dist/node/ws/index.cjs +1 -1
- package/dist/node/ws/index.d.ts +1 -1
- package/dist/node/ws/index.js +1 -1
- package/dist/node/{ws-BWel8nzl.js → ws-Dc2RUwVs.js} +1 -1
- package/dist/node/ws-Dc2RUwVs.js.map +1 -0
- package/dist/node/ws-QNMQn5kg.cjs +1 -0
- package/package.json +35 -22
- package/src/headless/HeadlessInteractionChannel.ts +9 -1
- package/src/headless/__tests__/headless-channel-options.test.ts +106 -0
- package/src/headless/__tests__/headless-provider-failure.integration.test.ts +143 -0
- package/src/headless/__tests__/headless-runner-initialization.test.ts +1 -1
- package/src/headless/__tests__/headless-runner.test.ts +24 -3
- package/src/headless/__tests__/headless-transport.test.ts +1 -2
- package/src/headless/headless-runner.ts +3 -2
- package/src/headless/headless-stream-json.ts +5 -5
- package/src/headless/headless-transport.ts +1 -2
- package/src/http/__tests__/http-transport.test.ts +1 -1
- package/src/http/__tests__/routes.test.ts +1 -1
- package/src/http/http-transport.ts +1 -2
- package/src/http/routes.ts +1 -1
- package/src/mcp/__tests__/mcp-server.test.ts +1 -1
- package/src/mcp/__tests__/mcp-transport.test.ts +1 -1
- package/src/mcp/mcp-server.ts +1 -1
- package/src/mcp/mcp-transport.ts +1 -2
- package/src/testing/__tests__/scripted-provider.test.ts +73 -0
- package/src/testing/index.ts +7 -0
- package/src/testing/scripted-provider.ts +73 -0
- package/src/transport-registry.ts +1 -1
- package/src/tui/App.tsx +22 -11
- package/src/tui/BackgroundTaskPanel.tsx +1 -1
- package/src/tui/ExecutionWorkspaceDetailPane.tsx +1 -1
- package/src/tui/ExecutionWorkspaceSwitcher.tsx +1 -1
- package/src/tui/InputArea.tsx +2 -1
- package/src/tui/InteractivePrompt.tsx +2 -2
- package/src/tui/PluginTUI.tsx +1 -1
- package/src/tui/SessionPicker.tsx +1 -1
- package/src/tui/SessionStatusBar.tsx +1 -1
- package/src/tui/SlashAutocomplete.tsx +1 -1
- package/src/tui/StreamingIndicator.tsx +1 -1
- package/src/tui/TransportTUI.tsx +1 -1
- package/src/tui/TuiInteractionChannel.ts +60 -38
- package/src/tui/UsageSummaryEntry.tsx +1 -1
- package/src/tui/__tests__/PluginTUI.test.tsx +1 -1
- package/src/tui/__tests__/SlashAutocomplete.test.tsx +1 -1
- package/src/tui/__tests__/TuiInteractionChannel.display-contract.test.ts +1 -1
- package/src/tui/__tests__/TuiInteractionChannel.lifecycle.test.ts +5 -2
- package/src/tui/__tests__/TuiInteractionChannel.requestAction.test.ts +1 -1
- package/src/tui/__tests__/background-task-panel.test.tsx +1 -1
- package/src/tui/__tests__/background-task-row-format.test.ts +1 -1
- package/src/tui/__tests__/channel-factory-integration.test.ts +138 -0
- package/src/tui/__tests__/execution-workspace-switcher.test.tsx +1 -1
- package/src/tui/__tests__/execution-workspace-view-model.test.ts +1 -1
- package/src/tui/__tests__/fixtures/provider-setup-prompt-driver.tsx +1 -1
- package/src/tui/__tests__/input-area-flow.test.ts +1 -1
- package/src/tui/__tests__/pty/pty-driver.ts +135 -0
- package/src/tui/__tests__/pty/tui-pty.ptytest.ts +61 -0
- package/src/tui/__tests__/render-channel-options.test.ts +32 -0
- package/src/tui/__tests__/session-init-poller.test.ts +102 -0
- package/src/tui/__tests__/session-switch-channel.test.tsx +307 -0
- package/src/tui/__tests__/slash-routing-effects.test.ts +4 -1
- package/src/tui/__tests__/status-activity.test.ts +3 -3
- package/src/tui/__tests__/status-bar.test.tsx +6 -5
- package/src/tui/__tests__/tui-channel-init-failure.test.ts +57 -0
- package/src/tui/__tests__/tui-state-manager.test.ts +1 -1
- package/src/tui/background-task-row-format.ts +1 -1
- package/src/tui/execution-workspace-view-model.ts +1 -1
- package/src/tui/flows/input-area-flow.ts +1 -1
- package/src/tui/flows/permission-prompt-flow.ts +1 -1
- package/src/tui/flows/session-init-poller.ts +77 -0
- package/src/tui/hooks/command-effect-handler.ts +4 -1
- package/src/tui/hooks/command-effect-queue.ts +1 -1
- package/src/tui/hooks/side-effects-types.ts +2 -2
- package/src/tui/hooks/useAutocomplete.ts +3 -2
- package/src/tui/hooks/usePluginCallbacks.ts +1 -1
- package/src/tui/hooks/usePluginScreenData.ts +1 -1
- package/src/tui/hooks/useSideEffects.ts +1 -1
- package/src/tui/hooks/useSlashRouting.ts +3 -3
- package/src/tui/hooks/useStatusLineSettings.ts +1 -1
- package/src/tui/hooks/useTuiChannel.ts +3 -3
- package/src/tui/plugin-tui-handlers.ts +1 -1
- package/src/tui/render.tsx +38 -25
- package/src/tui/status-activity.ts +2 -2
- package/src/tui/tui-cli-adapter.ts +3 -3
- package/src/tui/tui-state-manager.ts +2 -2
- package/src/tui/tui-transport.ts +4 -2
- package/src/ws/__tests__/ws-handler.test.ts +6 -4
- package/src/ws/__tests__/ws-transport.test.ts +1 -1
- package/src/ws/ws-background-messages.ts +1 -1
- package/src/ws/ws-handler.ts +4 -4
- package/src/ws/ws-protocol.ts +6 -4
- package/src/ws/ws-transport-configurable.ts +4 -2
- package/src/ws/ws-transport.ts +1 -2
- package/dist/node/headless-C6tj35h3.js.map +0 -1
- package/dist/node/http-Br10Ps8m.js.map +0 -1
- package/dist/node/http-Da6Kw4oy.cjs +0 -1
- package/dist/node/index-27HV5PJB.d.ts.map +0 -1
- package/dist/node/index-BRchlFBE.d.ts.map +0 -1
- package/dist/node/index-BRgV_MPB.d.ts.map +0 -1
- package/dist/node/index-BVNhOeeU.d.ts.map +0 -1
- package/dist/node/index-C5KNEBO9.d.ts.map +0 -1
- package/dist/node/index-C9LWCL4l.d.ts.map +0 -1
- package/dist/node/index-COWvtBa2.d.ts.map +0 -1
- package/dist/node/index-TMAlNHuM.d.ts.map +0 -1
- package/dist/node/index-X2Zg8FEY.d.ts.map +0 -1
- package/dist/node/index-nBlMTFkZ.d.ts.map +0 -1
- package/dist/node/mcp-BAujHOMr.js.map +0 -1
- package/dist/node/mcp-Bl8jUfev.cjs +0 -1
- package/dist/node/tui-D30s8S5f.cjs +0 -24
- package/dist/node/tui-DIdvTeiT.js.map +0 -1
- package/dist/node/ws-BWel8nzl.js.map +0 -1
- package/dist/node/ws-tCjj2gPu.cjs +0 -1
- package/src/tui/InkTerminal.ts +0 -42
- package/src/tui/hooks/use-interactive-session-init.ts +0 -91
- package/src/tui/hooks/usePermissionQueue.ts +0 -52
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Provider-failure exit-code integration tests (CLI-064).
|
|
3
|
+
*
|
|
4
|
+
* Drives a real InteractiveSession with a provider whose chat() throws (the 401 class
|
|
5
|
+
* observed in product verification) and asserts the headless transport surfaces the
|
|
6
|
+
* failure: non-zero exit code and an error envelope/stderr message — never exit 0.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { mkdtempSync, rmSync } from 'node:fs';
|
|
10
|
+
import { tmpdir } from 'node:os';
|
|
11
|
+
import { join } from 'node:path';
|
|
12
|
+
|
|
13
|
+
import { InteractiveSession } from '@robota-sdk/agent-framework';
|
|
14
|
+
import { afterEach, describe, expect, it } from 'vitest';
|
|
15
|
+
|
|
16
|
+
import { createHeadlessTransport } from '../headless-transport.js';
|
|
17
|
+
|
|
18
|
+
import type { TInteractiveSessionOptions } from '@robota-sdk/agent-framework';
|
|
19
|
+
|
|
20
|
+
type TStandardSessionOptions = Extract<
|
|
21
|
+
TInteractiveSessionOptions,
|
|
22
|
+
{ cwd: string; provider: unknown }
|
|
23
|
+
>;
|
|
24
|
+
type TTestProvider = TStandardSessionOptions['provider'];
|
|
25
|
+
type TResolvedConfig = NonNullable<TStandardSessionOptions['config']>;
|
|
26
|
+
|
|
27
|
+
const AUTH_FAILURE_MESSAGE =
|
|
28
|
+
'401 {"type":"error","error":{"type":"authentication_error","message":"invalid x-api-key"}}';
|
|
29
|
+
|
|
30
|
+
function createConfig(): TResolvedConfig {
|
|
31
|
+
return {
|
|
32
|
+
defaultTrustLevel: 'moderate',
|
|
33
|
+
language: 'en',
|
|
34
|
+
provider: {
|
|
35
|
+
name: 'failing-test-provider',
|
|
36
|
+
model: 'failing-test-model',
|
|
37
|
+
apiKey: 'test-key',
|
|
38
|
+
},
|
|
39
|
+
permissions: { allow: [], deny: [] },
|
|
40
|
+
env: {},
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function createAuthFailingProvider(): TTestProvider {
|
|
45
|
+
return {
|
|
46
|
+
name: 'failing-test-provider',
|
|
47
|
+
version: '1.0.0',
|
|
48
|
+
async chat() {
|
|
49
|
+
throw new Error(AUTH_FAILURE_MESSAGE);
|
|
50
|
+
},
|
|
51
|
+
async generateResponse() {
|
|
52
|
+
return { content: 'unused' };
|
|
53
|
+
},
|
|
54
|
+
supportsTools() {
|
|
55
|
+
return true;
|
|
56
|
+
},
|
|
57
|
+
validateConfig() {
|
|
58
|
+
return true;
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function captureStream(stream: NodeJS.WriteStream): { writes: string[]; restore(): void } {
|
|
64
|
+
const writes: string[] = [];
|
|
65
|
+
const originalWrite = stream.write;
|
|
66
|
+
stream.write = ((chunk: string | Uint8Array, encodingOrCallback?: unknown) => {
|
|
67
|
+
writes.push(typeof chunk === 'string' ? chunk : Buffer.from(chunk).toString('utf8'));
|
|
68
|
+
if (typeof encodingOrCallback === 'function') {
|
|
69
|
+
encodingOrCallback();
|
|
70
|
+
}
|
|
71
|
+
return true;
|
|
72
|
+
}) as typeof stream.write;
|
|
73
|
+
return {
|
|
74
|
+
writes,
|
|
75
|
+
restore() {
|
|
76
|
+
stream.write = originalWrite;
|
|
77
|
+
},
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
describe('headless provider failure exit codes (CLI-064)', () => {
|
|
82
|
+
let cwd: string | undefined;
|
|
83
|
+
|
|
84
|
+
afterEach(() => {
|
|
85
|
+
if (cwd) rmSync(cwd, { recursive: true, force: true });
|
|
86
|
+
cwd = undefined;
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('TC-02: text format exits 1 and writes the auth failure to stderr', async () => {
|
|
90
|
+
cwd = mkdtempSync(join(tmpdir(), 'robota-headless-fail-'));
|
|
91
|
+
const session = new InteractiveSession({
|
|
92
|
+
cwd,
|
|
93
|
+
provider: createAuthFailingProvider(),
|
|
94
|
+
config: createConfig(),
|
|
95
|
+
permissionMode: 'bypassPermissions',
|
|
96
|
+
bare: true,
|
|
97
|
+
});
|
|
98
|
+
const stdout = captureStream(process.stdout);
|
|
99
|
+
const stderr = captureStream(process.stderr);
|
|
100
|
+
|
|
101
|
+
try {
|
|
102
|
+
const transport = createHeadlessTransport({ outputFormat: 'text', prompt: 'say hi' });
|
|
103
|
+
session.attachTransport(transport);
|
|
104
|
+
await transport.start();
|
|
105
|
+
|
|
106
|
+
expect(transport.getExitCode()).toBe(1);
|
|
107
|
+
expect(stderr.writes.join('')).toContain('authentication_error');
|
|
108
|
+
} finally {
|
|
109
|
+
stdout.restore();
|
|
110
|
+
stderr.restore();
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it('TC-02: json format exits 1 with subtype error and error_code api_error', async () => {
|
|
115
|
+
cwd = mkdtempSync(join(tmpdir(), 'robota-headless-fail-'));
|
|
116
|
+
const session = new InteractiveSession({
|
|
117
|
+
cwd,
|
|
118
|
+
provider: createAuthFailingProvider(),
|
|
119
|
+
config: createConfig(),
|
|
120
|
+
permissionMode: 'bypassPermissions',
|
|
121
|
+
bare: true,
|
|
122
|
+
});
|
|
123
|
+
const stdout = captureStream(process.stdout);
|
|
124
|
+
const stderr = captureStream(process.stderr);
|
|
125
|
+
|
|
126
|
+
try {
|
|
127
|
+
const transport = createHeadlessTransport({ outputFormat: 'json', prompt: 'say hi' });
|
|
128
|
+
session.attachTransport(transport);
|
|
129
|
+
await transport.start();
|
|
130
|
+
|
|
131
|
+
expect(transport.getExitCode()).toBe(1);
|
|
132
|
+
const parsed: unknown = JSON.parse(stdout.writes.join('').trim());
|
|
133
|
+
expect(parsed).toMatchObject({
|
|
134
|
+
type: 'result',
|
|
135
|
+
subtype: 'error',
|
|
136
|
+
error_code: 'api_error',
|
|
137
|
+
});
|
|
138
|
+
} finally {
|
|
139
|
+
stdout.restore();
|
|
140
|
+
stderr.restore();
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
});
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, it, expect, vi } from 'vitest';
|
|
2
|
-
import type { IInteractiveSession } from '@robota-sdk/agent-
|
|
2
|
+
import type { IInteractiveSession } from '@robota-sdk/agent-interface-transport';
|
|
3
3
|
import { createHeadlessRunner } from '../headless-runner.js';
|
|
4
4
|
|
|
5
5
|
describe('createHeadlessRunner initialization', () => {
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
2
|
-
import type {
|
|
3
|
-
|
|
4
|
-
|
|
2
|
+
import type {
|
|
3
|
+
IExecutionResult,
|
|
4
|
+
IInteractiveSession,
|
|
5
|
+
TBackgroundJobGroupEvent,
|
|
6
|
+
} from '@robota-sdk/agent-interface-transport';
|
|
5
7
|
import type { TBackgroundTaskEvent } from '@robota-sdk/agent-framework';
|
|
6
8
|
import { createHeadlessRunner } from '../headless-runner.js';
|
|
7
9
|
|
|
@@ -96,6 +98,25 @@ describe('createHeadlessRunner (text format)', () => {
|
|
|
96
98
|
expect(stdoutWriteSpy).not.toHaveBeenCalled();
|
|
97
99
|
});
|
|
98
100
|
|
|
101
|
+
it('TC-02 (CLI-064): text format writes the error message to stderr on error', async () => {
|
|
102
|
+
const stderrWrites: string[] = [];
|
|
103
|
+
const stderrSpy = vi.spyOn(process.stderr, 'write').mockImplementation(((chunk: unknown) => {
|
|
104
|
+
stderrWrites.push(String(chunk));
|
|
105
|
+
return true;
|
|
106
|
+
}) as never);
|
|
107
|
+
try {
|
|
108
|
+
const session = createMockSession('error');
|
|
109
|
+
const runner = createHeadlessRunner({ session, outputFormat: 'text' });
|
|
110
|
+
|
|
111
|
+
const exitCode = await runner.run('test prompt');
|
|
112
|
+
|
|
113
|
+
expect(exitCode).toBe(1);
|
|
114
|
+
expect(stderrWrites.join('')).toContain('test error');
|
|
115
|
+
} finally {
|
|
116
|
+
stderrSpy.mockRestore();
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
|
|
99
120
|
it('passes the prompt to session.submit', async () => {
|
|
100
121
|
const session = createMockSession('complete', 'ok');
|
|
101
122
|
const runner = createHeadlessRunner({ session, outputFormat: 'text' });
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
2
2
|
import { createHeadlessTransport } from '../headless-transport.js';
|
|
3
|
-
import type { IInteractiveSession } from '@robota-sdk/agent-
|
|
4
|
-
import type { IExecutionResult } from '@robota-sdk/agent-framework';
|
|
3
|
+
import type { IExecutionResult, IInteractiveSession } from '@robota-sdk/agent-interface-transport';
|
|
5
4
|
|
|
6
5
|
function createMockSession(): IInteractiveSession {
|
|
7
6
|
return {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { executeSlashCommandIfPresent, subscribeStreamJsonEvents } from './headless-stream-json.js';
|
|
2
2
|
|
|
3
|
-
import type {
|
|
3
|
+
import type { IExecutionResult, IInteractiveSession } from '@robota-sdk/agent-interface-transport';
|
|
4
4
|
|
|
5
5
|
export type TOutputFormat = 'text' | 'json' | 'stream-json';
|
|
6
6
|
|
|
@@ -78,8 +78,9 @@ function runTextFormat(session: IInteractiveSession, prompt: string): Promise<nu
|
|
|
78
78
|
if (result.response) process.stdout.write(result.response + '\n');
|
|
79
79
|
resolve(0);
|
|
80
80
|
};
|
|
81
|
-
const onError = (
|
|
81
|
+
const onError = (error: Error): void => {
|
|
82
82
|
cleanup();
|
|
83
|
+
process.stderr.write(error.message + '\n');
|
|
83
84
|
resolve(1);
|
|
84
85
|
};
|
|
85
86
|
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
2
|
|
|
3
|
+
import type { TBackgroundTaskEvent } from '@robota-sdk/agent-framework';
|
|
3
4
|
import type {
|
|
4
|
-
IInteractiveSession,
|
|
5
|
-
IExecutionResult,
|
|
6
5
|
ICommandResult,
|
|
6
|
+
IExecutionResult,
|
|
7
|
+
IInteractiveSession,
|
|
7
8
|
TBackgroundJobGroupEvent,
|
|
8
|
-
|
|
9
|
-
} from '@robota-sdk/agent-framework';
|
|
9
|
+
} from '@robota-sdk/agent-interface-transport';
|
|
10
10
|
|
|
11
11
|
type TSlashCommandExecution =
|
|
12
12
|
| { readonly kind: 'not-slash' }
|
|
@@ -65,7 +65,7 @@ interface IStreamJsonHandlers {
|
|
|
65
65
|
onError: (error: Error) => void;
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
-
|
|
68
|
+
function writeStreamJsonEvent(
|
|
69
69
|
session: IInteractiveSession,
|
|
70
70
|
getSessionId: (s: IInteractiveSession) => string,
|
|
71
71
|
event: TStreamJsonEvent,
|
|
@@ -8,8 +8,7 @@
|
|
|
8
8
|
import { createHeadlessRunner } from './headless-runner.js';
|
|
9
9
|
|
|
10
10
|
import type { TOutputFormat } from './headless-runner.js';
|
|
11
|
-
import type { IInteractiveSession } from '@robota-sdk/agent-
|
|
12
|
-
import type { ITransportAdapter } from '@robota-sdk/agent-interface-transport';
|
|
11
|
+
import type { IInteractiveSession, ITransportAdapter } from '@robota-sdk/agent-interface-transport';
|
|
13
12
|
|
|
14
13
|
export interface IHeadlessTransportOptions {
|
|
15
14
|
/** Output format: 'text', 'json', or 'stream-json'. */
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { describe, it, expect, vi } from 'vitest';
|
|
2
2
|
import { createHttpTransport } from '../http-transport.js';
|
|
3
|
-
import type { IInteractiveSession } from '@robota-sdk/agent-
|
|
3
|
+
import type { IInteractiveSession } from '@robota-sdk/agent-interface-transport';
|
|
4
4
|
|
|
5
5
|
function createMockSession(): IInteractiveSession {
|
|
6
6
|
return {
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
import { describe, it, expect, vi } from 'vitest';
|
|
7
7
|
import { createAgentRoutes } from '../routes.js';
|
|
8
|
-
import type { IInteractiveSession } from '@robota-sdk/agent-
|
|
8
|
+
import type { IInteractiveSession } from '@robota-sdk/agent-interface-transport';
|
|
9
9
|
|
|
10
10
|
function createMockSession(overrides?: Record<string, unknown>) {
|
|
11
11
|
return {
|
|
@@ -7,8 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
import { createAgentRoutes } from './routes.js';
|
|
9
9
|
|
|
10
|
-
import type { IInteractiveSession } from '@robota-sdk/agent-
|
|
11
|
-
import type { ITransportAdapter } from '@robota-sdk/agent-interface-transport';
|
|
10
|
+
import type { IInteractiveSession, ITransportAdapter } from '@robota-sdk/agent-interface-transport';
|
|
12
11
|
import type { Hono } from 'hono';
|
|
13
12
|
|
|
14
13
|
export interface IHttpTransportOptions {
|
package/src/http/routes.ts
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
import { Hono } from 'hono';
|
|
9
9
|
import { streamSSE } from 'hono/streaming';
|
|
10
10
|
|
|
11
|
-
import type { IInteractiveSession } from '@robota-sdk/agent-
|
|
11
|
+
import type { IInteractiveSession } from '@robota-sdk/agent-interface-transport';
|
|
12
12
|
import type { Context } from 'hono';
|
|
13
13
|
|
|
14
14
|
/** Callback that resolves an IInteractiveSession from the request context. */
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
import { describe, it, expect, vi } from 'vitest';
|
|
6
6
|
import { createAgentMcpServer } from '../mcp-server.js';
|
|
7
|
-
import type { IInteractiveSession } from '@robota-sdk/agent-
|
|
7
|
+
import type { IInteractiveSession } from '@robota-sdk/agent-interface-transport';
|
|
8
8
|
|
|
9
9
|
function createMockSession(commands?: Array<{ name: string; description: string }>) {
|
|
10
10
|
return {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { describe, it, expect, vi } from 'vitest';
|
|
2
2
|
import { createMcpTransport } from '../mcp-transport.js';
|
|
3
|
-
import type { IInteractiveSession } from '@robota-sdk/agent-
|
|
3
|
+
import type { IInteractiveSession } from '@robota-sdk/agent-interface-transport';
|
|
4
4
|
|
|
5
5
|
function createMockSession(): IInteractiveSession {
|
|
6
6
|
return {
|
package/src/mcp/mcp-server.ts
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
10
10
|
import { ListToolsRequestSchema, CallToolRequestSchema } from '@modelcontextprotocol/sdk/types.js';
|
|
11
11
|
|
|
12
|
-
import type {
|
|
12
|
+
import type { IExecutionResult, IInteractiveSession } from '@robota-sdk/agent-interface-transport';
|
|
13
13
|
|
|
14
14
|
export interface IAgentMcpOptions {
|
|
15
15
|
/** Name for the MCP server. */
|
package/src/mcp/mcp-transport.ts
CHANGED
|
@@ -8,8 +8,7 @@
|
|
|
8
8
|
import { createAgentMcpServer } from './mcp-server.js';
|
|
9
9
|
|
|
10
10
|
import type { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
11
|
-
import type { IInteractiveSession } from '@robota-sdk/agent-
|
|
12
|
-
import type { ITransportAdapter } from '@robota-sdk/agent-interface-transport';
|
|
11
|
+
import type { IInteractiveSession, ITransportAdapter } from '@robota-sdk/agent-interface-transport';
|
|
13
12
|
|
|
14
13
|
export interface IMcpTransportOptions {
|
|
15
14
|
/** Name for the MCP server. */
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Scripted provider fixture tests (CLI-074 TC-01).
|
|
3
|
+
*
|
|
4
|
+
* The fixture must replay declared turns in order, record every request's
|
|
5
|
+
* message array for assertions, and fail fast on script exhaustion — never
|
|
6
|
+
* silently improvise a response.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { describe, expect, it } from 'vitest';
|
|
10
|
+
|
|
11
|
+
import { createScriptedProvider } from '../scripted-provider.js';
|
|
12
|
+
|
|
13
|
+
describe('createScriptedProvider (CLI-074)', () => {
|
|
14
|
+
it('TC-01: replays text turns in order and records requests', async () => {
|
|
15
|
+
const { provider, requests } = createScriptedProvider([
|
|
16
|
+
{ text: 'first answer' },
|
|
17
|
+
{ text: 'second answer' },
|
|
18
|
+
]);
|
|
19
|
+
|
|
20
|
+
const first = await provider.chat([
|
|
21
|
+
{ id: 'u1', role: 'user', content: 'hi', state: 'complete', timestamp: new Date() },
|
|
22
|
+
]);
|
|
23
|
+
const second = await provider.chat([
|
|
24
|
+
{ id: 'u2', role: 'user', content: 'again', state: 'complete', timestamp: new Date() },
|
|
25
|
+
]);
|
|
26
|
+
|
|
27
|
+
expect(first.role).toBe('assistant');
|
|
28
|
+
expect(first.content).toBe('first answer');
|
|
29
|
+
expect(second.content).toBe('second answer');
|
|
30
|
+
expect(requests).toHaveLength(2);
|
|
31
|
+
expect(requests[0]?.map((message) => message.content)).toContain('hi');
|
|
32
|
+
expect(requests[1]?.map((message) => message.content)).toContain('again');
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('TC-01: replays tool_use turns as assistant toolCalls', async () => {
|
|
36
|
+
const { provider } = createScriptedProvider([
|
|
37
|
+
{ toolCalls: [{ name: 'Read', args: { file_path: '/tmp/a.txt' } }] },
|
|
38
|
+
{ text: 'done reading' },
|
|
39
|
+
]);
|
|
40
|
+
|
|
41
|
+
const turn = await provider.chat([
|
|
42
|
+
{ id: 'u1', role: 'user', content: 'read it', state: 'complete', timestamp: new Date() },
|
|
43
|
+
]);
|
|
44
|
+
|
|
45
|
+
expect(turn.role).toBe('assistant');
|
|
46
|
+
if (turn.role !== 'assistant') throw new Error('unreachable');
|
|
47
|
+
expect(turn.content).toBeNull();
|
|
48
|
+
expect(turn.toolCalls).toHaveLength(1);
|
|
49
|
+
expect(turn.toolCalls?.[0]?.function.name).toBe('Read');
|
|
50
|
+
expect(JSON.parse(turn.toolCalls?.[0]?.function.arguments ?? '{}')).toEqual({
|
|
51
|
+
file_path: '/tmp/a.txt',
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('TC-01: throws on script exhaustion instead of improvising', async () => {
|
|
56
|
+
const { provider } = createScriptedProvider([{ text: 'only turn' }]);
|
|
57
|
+
|
|
58
|
+
await provider.chat([
|
|
59
|
+
{ id: 'u1', role: 'user', content: 'one', state: 'complete', timestamp: new Date() },
|
|
60
|
+
]);
|
|
61
|
+
|
|
62
|
+
await expect(
|
|
63
|
+
provider.chat([
|
|
64
|
+
{ id: 'u2', role: 'user', content: 'two', state: 'complete', timestamp: new Date() },
|
|
65
|
+
]),
|
|
66
|
+
).rejects.toThrow(/script exhausted/i);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('supportsTools is true and validateConfig passes (agent-loop prerequisites)', () => {
|
|
70
|
+
const { provider } = createScriptedProvider([{ text: 'x' }]);
|
|
71
|
+
expect(provider.supportsTools()).toBe(true);
|
|
72
|
+
});
|
|
73
|
+
});
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deterministic scripted provider for E2E tests (CLI-074).
|
|
3
|
+
*
|
|
4
|
+
* Replays a declared sequence of assistant turns through the REAL agent loop —
|
|
5
|
+
* tool execution, permission gate, session persistence, and output transports all
|
|
6
|
+
* run unmocked. Test-only: exported via the `@robota-sdk/agent-transport/testing`
|
|
7
|
+
* subpath and never imported by runtime code.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { IAIProvider, IRawProviderResponse, TUniversalMessage } from '@robota-sdk/agent-core';
|
|
11
|
+
|
|
12
|
+
/** One scripted assistant turn: plain text or tool invocations. */
|
|
13
|
+
export type TScriptedTurn =
|
|
14
|
+
| { text: string }
|
|
15
|
+
| { toolCalls: ReadonlyArray<{ name: string; args: Record<string, unknown> }> };
|
|
16
|
+
|
|
17
|
+
export interface IScriptedProvider {
|
|
18
|
+
provider: IAIProvider;
|
|
19
|
+
/** Message arrays of every chat() call, in order, for request assertions. */
|
|
20
|
+
requests: TUniversalMessage[][];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function createScriptedProvider(turns: readonly TScriptedTurn[]): IScriptedProvider {
|
|
24
|
+
const requests: TUniversalMessage[][] = [];
|
|
25
|
+
let cursor = 0;
|
|
26
|
+
|
|
27
|
+
const provider: IAIProvider = {
|
|
28
|
+
name: 'scripted-test-provider',
|
|
29
|
+
version: 'test',
|
|
30
|
+
async chat(messages: TUniversalMessage[]): Promise<TUniversalMessage> {
|
|
31
|
+
requests.push([...messages]);
|
|
32
|
+
const turn = turns[cursor];
|
|
33
|
+
if (turn === undefined) {
|
|
34
|
+
throw new Error(
|
|
35
|
+
`Scripted provider: script exhausted at call ${cursor + 1} (script declares ${turns.length} turn(s)) — extend the script instead of relying on improvised responses`,
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
cursor += 1;
|
|
39
|
+
if ('text' in turn) {
|
|
40
|
+
return {
|
|
41
|
+
id: `scripted-${cursor}`,
|
|
42
|
+
role: 'assistant',
|
|
43
|
+
content: turn.text,
|
|
44
|
+
state: 'complete',
|
|
45
|
+
timestamp: new Date(),
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
return {
|
|
49
|
+
id: `scripted-${cursor}`,
|
|
50
|
+
role: 'assistant',
|
|
51
|
+
content: null,
|
|
52
|
+
state: 'complete',
|
|
53
|
+
timestamp: new Date(),
|
|
54
|
+
toolCalls: turn.toolCalls.map((call, index) => ({
|
|
55
|
+
id: `scripted-call-${cursor}-${index}`,
|
|
56
|
+
type: 'function' as const,
|
|
57
|
+
function: { name: call.name, arguments: JSON.stringify(call.args) },
|
|
58
|
+
})),
|
|
59
|
+
};
|
|
60
|
+
},
|
|
61
|
+
async generateResponse(): Promise<IRawProviderResponse> {
|
|
62
|
+
return { content: 'scripted provider does not implement raw responses' };
|
|
63
|
+
},
|
|
64
|
+
supportsTools(): boolean {
|
|
65
|
+
return true;
|
|
66
|
+
},
|
|
67
|
+
validateConfig(): boolean {
|
|
68
|
+
return true;
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
return { provider, requests };
|
|
73
|
+
}
|
|
@@ -11,9 +11,9 @@ import { readSettings, writeSettings, type TSettingsData } from '@robota-sdk/age
|
|
|
11
11
|
import { WsTransport } from './ws/index.js';
|
|
12
12
|
|
|
13
13
|
import type { TUniversalValue } from '@robota-sdk/agent-core';
|
|
14
|
-
import type { IInteractiveSession } from '@robota-sdk/agent-framework';
|
|
15
14
|
import type {
|
|
16
15
|
IConfigurableTransport,
|
|
16
|
+
IInteractiveSession,
|
|
17
17
|
ITransportConfig,
|
|
18
18
|
ITransportEntry,
|
|
19
19
|
} from '@robota-sdk/agent-interface-transport';
|
package/src/tui/App.tsx
CHANGED
|
@@ -31,16 +31,19 @@ import type { ITuiCliAdapter } from './tui-cli-adapter.js';
|
|
|
31
31
|
import type { TuiInteractionChannel } from './TuiInteractionChannel.js';
|
|
32
32
|
import type { TPermissionMode } from '@robota-sdk/agent-core';
|
|
33
33
|
import type {
|
|
34
|
+
IExecutionDetailPage,
|
|
34
35
|
IInteractiveSession,
|
|
35
36
|
IInteractiveSessionStore,
|
|
36
|
-
|
|
37
|
-
} from '@robota-sdk/agent-
|
|
38
|
-
import type { ITransportRegistryView } from '@robota-sdk/agent-interface-transport';
|
|
37
|
+
ITransportRegistryView,
|
|
38
|
+
} from '@robota-sdk/agent-interface-transport';
|
|
39
39
|
|
|
40
40
|
interface IProps {
|
|
41
41
|
cwd: string;
|
|
42
|
-
|
|
43
|
-
|
|
42
|
+
/**
|
|
43
|
+
* Sole channel source (CLI-B12): App owns the channel lifecycle in React state.
|
|
44
|
+
* The initial channel and every session-switch replacement come from this factory.
|
|
45
|
+
*/
|
|
46
|
+
createChannel: (resumeSessionId?: string) => TuiInteractionChannel;
|
|
44
47
|
providerOverride?: string | undefined;
|
|
45
48
|
providerType?: string | undefined;
|
|
46
49
|
modelId?: string;
|
|
@@ -55,10 +58,15 @@ interface IProps {
|
|
|
55
58
|
}
|
|
56
59
|
|
|
57
60
|
export default function App(props: IProps): React.ReactElement {
|
|
61
|
+
// Lazy initializer: channel construction is side-effect-free (object wiring only);
|
|
62
|
+
// I/O starts in AppInner's effect via channel.start(). Runs once per mount.
|
|
58
63
|
const [sessionState, setSessionState] = useState<{
|
|
59
64
|
channel: TuiInteractionChannel;
|
|
60
65
|
sessionId: string | undefined;
|
|
61
|
-
}>(
|
|
66
|
+
}>(() => ({
|
|
67
|
+
channel: props.createChannel(props.resumeSessionId),
|
|
68
|
+
sessionId: props.resumeSessionId,
|
|
69
|
+
}));
|
|
62
70
|
const [showInitialSessionPicker, setShowInitialSessionPicker] = useState(
|
|
63
71
|
props.showSessionPickerOnStart ?? false,
|
|
64
72
|
);
|
|
@@ -73,10 +81,10 @@ export default function App(props: IProps): React.ReactElement {
|
|
|
73
81
|
resumeSessionId={sessionState.sessionId}
|
|
74
82
|
onSessionSwitch={(sessionId) => {
|
|
75
83
|
setShowInitialSessionPicker(false);
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
84
|
+
// Stop the old channel BEFORE the new one becomes active so it can
|
|
85
|
+
// never receive events addressed to the new session (CLI-B12).
|
|
86
|
+
void sessionState.channel.stop();
|
|
87
|
+
setSessionState({ channel: props.createChannel(sessionId), sessionId });
|
|
80
88
|
}}
|
|
81
89
|
/>
|
|
82
90
|
</TuiCliAdapterProvider>
|
|
@@ -84,7 +92,10 @@ export default function App(props: IProps): React.ReactElement {
|
|
|
84
92
|
}
|
|
85
93
|
|
|
86
94
|
function AppInner(
|
|
87
|
-
props: IProps & {
|
|
95
|
+
props: IProps & {
|
|
96
|
+
channel: TuiInteractionChannel;
|
|
97
|
+
onSessionSwitch: (sessionId: string) => void;
|
|
98
|
+
},
|
|
88
99
|
): React.ReactElement {
|
|
89
100
|
const cwd = props.cwd;
|
|
90
101
|
const { channel } = props;
|
|
@@ -3,7 +3,7 @@ import React from 'react';
|
|
|
3
3
|
|
|
4
4
|
import { formatBackgroundTaskRow } from './background-task-row-format.js';
|
|
5
5
|
|
|
6
|
-
import type { IExecutionWorkspaceEntry } from '@robota-sdk/agent-
|
|
6
|
+
import type { IExecutionWorkspaceEntry } from '@robota-sdk/agent-interface-transport';
|
|
7
7
|
|
|
8
8
|
interface IProps {
|
|
9
9
|
entries: IExecutionWorkspaceEntry[];
|
package/src/tui/InputArea.tsx
CHANGED
|
@@ -25,7 +25,8 @@ import { expandPasteLabels } from './utils/paste-labels.js';
|
|
|
25
25
|
import WaveText from './WaveText.js';
|
|
26
26
|
|
|
27
27
|
import type { IHistoryEntry } from '@robota-sdk/agent-core';
|
|
28
|
-
import type { CommandRegistry
|
|
28
|
+
import type { CommandRegistry } from '@robota-sdk/agent-framework';
|
|
29
|
+
import type { ICommand } from '@robota-sdk/agent-interface-transport';
|
|
29
30
|
|
|
30
31
|
interface IProps {
|
|
31
32
|
onSubmit: (value: string) => void;
|
|
@@ -5,9 +5,9 @@ import ListPicker from './ListPicker.js';
|
|
|
5
5
|
import TextPrompt from './TextPrompt.js';
|
|
6
6
|
|
|
7
7
|
import type {
|
|
8
|
-
TCommandInteractionPrompt as TInteractivePrompt,
|
|
9
8
|
ICommandChoicePromptOption as IChoicePromptOption,
|
|
10
|
-
|
|
9
|
+
TCommandInteractionPrompt as TInteractivePrompt,
|
|
10
|
+
} from '@robota-sdk/agent-interface-transport';
|
|
11
11
|
|
|
12
12
|
interface IInteractivePromptProps {
|
|
13
13
|
prompt: TInteractivePrompt;
|
package/src/tui/PluginTUI.tsx
CHANGED
|
@@ -20,7 +20,7 @@ import {
|
|
|
20
20
|
import TextPrompt from './TextPrompt.js';
|
|
21
21
|
|
|
22
22
|
import type { IMenuSelectItem } from './MenuSelect.js';
|
|
23
|
-
import type { ICommandPluginAdapter } from '@robota-sdk/agent-
|
|
23
|
+
import type { ICommandPluginAdapter } from '@robota-sdk/agent-interface-transport';
|
|
24
24
|
|
|
25
25
|
type TScreenId =
|
|
26
26
|
| 'main'
|
|
@@ -8,7 +8,7 @@ import React from 'react';
|
|
|
8
8
|
|
|
9
9
|
import ListPicker from './ListPicker.js';
|
|
10
10
|
|
|
11
|
-
import type { IResumableSessionSummary } from '@robota-sdk/agent-
|
|
11
|
+
import type { IResumableSessionSummary } from '@robota-sdk/agent-interface-transport';
|
|
12
12
|
|
|
13
13
|
const SESSION_ID_DISPLAY_LENGTH = 8;
|
|
14
14
|
const SESSION_PREVIEW_DISPLAY_LENGTH = 60;
|
|
@@ -4,7 +4,7 @@ import StatusBar from './StatusBar.js';
|
|
|
4
4
|
import { useTuiCliAdapter } from './tui-cli-adapter-context.js';
|
|
5
5
|
|
|
6
6
|
import type { TPermissionMode } from '@robota-sdk/agent-core';
|
|
7
|
-
import type { IStatusLineCommandSettings } from '@robota-sdk/agent-
|
|
7
|
+
import type { IStatusLineCommandSettings } from '@robota-sdk/agent-interface-transport';
|
|
8
8
|
|
|
9
9
|
interface IProps {
|
|
10
10
|
cwd: string;
|