@robota-sdk/agent-transport 3.0.0-beta.72 → 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-4hA-SMtS.js → tui-Btb1q88j.js} +5 -5
- 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 +38 -29
- package/src/tui/BackgroundTaskPanel.tsx +1 -1
- package/src/tui/CjkTextInput.tsx +4 -8
- package/src/tui/ExecutionWorkspaceDetailPane.tsx +1 -1
- package/src/tui/ExecutionWorkspaceSwitcher.tsx +1 -1
- package/src/tui/InputArea.tsx +15 -7
- 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/StatusBar.tsx +1 -7
- 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 +7 -6
- 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-4hA-SMtS.js.map +0 -1
- package/dist/node/tui-CcLmEJ1r.cjs +0 -24
- 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;
|
|
@@ -436,22 +447,6 @@ function AppInner(
|
|
|
436
447
|
/>
|
|
437
448
|
)}
|
|
438
449
|
<ContextWarningBanner percentage={contextState.percentage} />
|
|
439
|
-
<SessionStatusBar
|
|
440
|
-
cwd={cwd}
|
|
441
|
-
permissionMode={permissionMode}
|
|
442
|
-
modelId={props.modelId}
|
|
443
|
-
providerType={props.providerType}
|
|
444
|
-
sessionId={sessionId}
|
|
445
|
-
isThinking={isThinking}
|
|
446
|
-
activeToolCount={activeTools.length}
|
|
447
|
-
activeBackgroundTaskCount={activeBackgroundTaskCount}
|
|
448
|
-
hasPendingPrompt={pendingPrompt !== null}
|
|
449
|
-
contextState={contextState}
|
|
450
|
-
sessionName={sessionName}
|
|
451
|
-
settings={statusLineSettings}
|
|
452
|
-
activeAgentLabel={activeAgentLabel}
|
|
453
|
-
gitRefreshToken={gitRefreshToken}
|
|
454
|
-
/>
|
|
455
450
|
<InputArea
|
|
456
451
|
onSubmit={handleSubmitWithGitRefresh}
|
|
457
452
|
onCancelQueue={handleCancelQueue}
|
|
@@ -472,8 +467,22 @@ function AppInner(
|
|
|
472
467
|
sessionName={sessionName}
|
|
473
468
|
history={history}
|
|
474
469
|
/>
|
|
475
|
-
|
|
476
|
-
|
|
470
|
+
<SessionStatusBar
|
|
471
|
+
cwd={cwd}
|
|
472
|
+
permissionMode={permissionMode}
|
|
473
|
+
modelId={props.modelId}
|
|
474
|
+
providerType={props.providerType}
|
|
475
|
+
sessionId={sessionId}
|
|
476
|
+
isThinking={isThinking}
|
|
477
|
+
activeToolCount={activeTools.length}
|
|
478
|
+
activeBackgroundTaskCount={activeBackgroundTaskCount}
|
|
479
|
+
hasPendingPrompt={pendingPrompt !== null}
|
|
480
|
+
contextState={contextState}
|
|
481
|
+
sessionName={sessionName}
|
|
482
|
+
settings={statusLineSettings}
|
|
483
|
+
activeAgentLabel={activeAgentLabel}
|
|
484
|
+
gitRefreshToken={gitRefreshToken}
|
|
485
|
+
/>
|
|
477
486
|
</Box>
|
|
478
487
|
);
|
|
479
488
|
}
|
|
@@ -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/CjkTextInput.tsx
CHANGED
|
@@ -79,14 +79,10 @@ export default function CjkTextInput({
|
|
|
79
79
|
forceRender,
|
|
80
80
|
});
|
|
81
81
|
|
|
82
|
-
//
|
|
83
|
-
//
|
|
84
|
-
//
|
|
85
|
-
//
|
|
86
|
-
// (same behavior as Claude Code, issue #19207), but Terminal.app does not crash.
|
|
87
|
-
//
|
|
88
|
-
// A correct fix would require knowing the total rendered height to pass the right
|
|
89
|
-
// y coordinate, which ink does not expose to components.
|
|
82
|
+
// Real terminal cursor positioning is intentionally omitted.
|
|
83
|
+
// setCursorPosition(x, 0) crashes Terminal.app via Korean IME SIGSEGV.
|
|
84
|
+
// Correct fix requires the input row's y offset from the bottom of the render,
|
|
85
|
+
// which Ink does not expose. Tracked as a known limitation.
|
|
90
86
|
|
|
91
87
|
return (
|
|
92
88
|
<Text>
|