@robota-sdk/agent-transport 3.0.0-beta.73 → 3.0.0-beta.75

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.
Files changed (170) hide show
  1. package/dist/node/headless/index.cjs +1 -1
  2. package/dist/node/headless/index.d.ts +1 -1
  3. package/dist/node/headless/index.js +1 -1
  4. package/dist/node/{headless-DCtHvyVf.cjs → headless-CT2ibQnr.cjs} +4 -3
  5. package/dist/node/{headless-C6tj35h3.js → headless-mRYilLfC.js} +4 -3
  6. package/dist/node/headless-mRYilLfC.js.map +1 -0
  7. package/dist/node/http/index.cjs +1 -1
  8. package/dist/node/http/index.d.ts +1 -1
  9. package/dist/node/http/index.js +1 -1
  10. package/dist/node/{http-Br10Ps8m.js → http-2Jiuflc1.js} +1 -1
  11. package/dist/node/http-2Jiuflc1.js.map +1 -0
  12. package/dist/node/http-CBAvefLw.cjs +1 -0
  13. package/dist/node/{index-BVNhOeeU.d.ts → index-BNccqSpv.d.ts} +13 -3
  14. package/dist/node/index-BNccqSpv.d.ts.map +1 -0
  15. package/dist/node/{index-TMAlNHuM.d.ts → index-BUhHIf7X.d.ts} +13 -3
  16. package/dist/node/index-BUhHIf7X.d.ts.map +1 -0
  17. package/dist/node/{index-C9LWCL4l.d.ts → index-BnAGE-u9.d.ts} +2 -3
  18. package/dist/node/index-BnAGE-u9.d.ts.map +1 -0
  19. package/dist/node/{index-COWvtBa2.d.ts → index-BrQ4gGw0.d.ts} +3 -3
  20. package/dist/node/index-BrQ4gGw0.d.ts.map +1 -0
  21. package/dist/node/{index-27HV5PJB.d.ts → index-CYl7ksS6.d.ts} +18 -3
  22. package/dist/node/index-CYl7ksS6.d.ts.map +1 -0
  23. package/dist/node/{index-X2Zg8FEY.d.ts → index-CoeBF21y.d.ts} +3 -3
  24. package/dist/node/index-CoeBF21y.d.ts.map +1 -0
  25. package/dist/node/{index-BRgV_MPB.d.ts → index-DHt-2VQ-.d.ts} +2 -3
  26. package/dist/node/index-DHt-2VQ-.d.ts.map +1 -0
  27. package/dist/node/{index-nBlMTFkZ.d.ts → index-DMwKN5Le.d.ts} +2 -3
  28. package/dist/node/index-DMwKN5Le.d.ts.map +1 -0
  29. package/dist/node/{index-BRchlFBE.d.ts → index-E8Gx4-lc.d.ts} +18 -3
  30. package/dist/node/index-E8Gx4-lc.d.ts.map +1 -0
  31. package/dist/node/{index-C5KNEBO9.d.ts → index-c0M42fsA.d.ts} +2 -3
  32. package/dist/node/index-c0M42fsA.d.ts.map +1 -0
  33. package/dist/node/index.cjs +1 -1
  34. package/dist/node/index.d.ts +6 -7
  35. package/dist/node/index.d.ts.map +1 -1
  36. package/dist/node/index.js +1 -1
  37. package/dist/node/index.js.map +1 -1
  38. package/dist/node/mcp/index.cjs +1 -1
  39. package/dist/node/mcp/index.d.ts +1 -1
  40. package/dist/node/mcp/index.js +1 -1
  41. package/dist/node/mcp-BOglBJNy.cjs +1 -0
  42. package/dist/node/{mcp-BAujHOMr.js → mcp-D3BBVK7C.js} +1 -1
  43. package/dist/node/mcp-D3BBVK7C.js.map +1 -0
  44. package/dist/node/{chunk-Bmb41Sf3.cjs → rolldown-runtime-CMqjfN_6.cjs} +1 -1
  45. package/dist/node/testing/index.cjs +1 -0
  46. package/dist/node/testing/index.d.ts +21 -0
  47. package/dist/node/testing/index.d.ts.map +1 -0
  48. package/dist/node/testing/index.js +2 -0
  49. package/dist/node/testing/index.js.map +1 -0
  50. package/dist/node/tui/index.cjs +1 -1
  51. package/dist/node/tui/index.d.ts +1 -1
  52. package/dist/node/tui/index.js +1 -1
  53. package/dist/node/{tui-DIdvTeiT.js → tui-CcH5EsQh.js} +4 -4
  54. package/dist/node/tui-CcH5EsQh.js.map +1 -0
  55. package/dist/node/tui-DznRbcku.cjs +24 -0
  56. package/dist/node/ws/index.cjs +1 -1
  57. package/dist/node/ws/index.d.ts +1 -1
  58. package/dist/node/ws/index.js +1 -1
  59. package/dist/node/{ws-BWel8nzl.js → ws-Dc2RUwVs.js} +1 -1
  60. package/dist/node/ws-Dc2RUwVs.js.map +1 -0
  61. package/dist/node/ws-QNMQn5kg.cjs +1 -0
  62. package/package.json +35 -22
  63. package/src/headless/HeadlessInteractionChannel.ts +30 -2
  64. package/src/headless/__tests__/headless-channel-options.test.ts +106 -0
  65. package/src/headless/__tests__/headless-provider-failure.integration.test.ts +143 -0
  66. package/src/headless/__tests__/headless-runner-initialization.test.ts +1 -1
  67. package/src/headless/__tests__/headless-runner.test.ts +24 -3
  68. package/src/headless/__tests__/headless-transport.test.ts +1 -2
  69. package/src/headless/headless-runner.ts +3 -2
  70. package/src/headless/headless-stream-json.ts +5 -5
  71. package/src/headless/headless-transport.ts +1 -2
  72. package/src/http/__tests__/http-transport.test.ts +1 -1
  73. package/src/http/__tests__/routes.test.ts +1 -1
  74. package/src/http/http-transport.ts +1 -2
  75. package/src/http/routes.ts +1 -1
  76. package/src/mcp/__tests__/mcp-server.test.ts +1 -1
  77. package/src/mcp/__tests__/mcp-transport.test.ts +1 -1
  78. package/src/mcp/mcp-server.ts +1 -1
  79. package/src/mcp/mcp-transport.ts +1 -2
  80. package/src/testing/__tests__/scripted-provider.test.ts +73 -0
  81. package/src/testing/index.ts +7 -0
  82. package/src/testing/scripted-provider.ts +73 -0
  83. package/src/transport-registry.ts +1 -1
  84. package/src/tui/App.tsx +25 -11
  85. package/src/tui/BackgroundTaskPanel.tsx +1 -1
  86. package/src/tui/ExecutionWorkspaceDetailPane.tsx +1 -1
  87. package/src/tui/ExecutionWorkspaceSwitcher.tsx +1 -1
  88. package/src/tui/InputArea.tsx +2 -1
  89. package/src/tui/InteractivePrompt.tsx +2 -2
  90. package/src/tui/PluginTUI.tsx +1 -1
  91. package/src/tui/SessionPicker.tsx +1 -1
  92. package/src/tui/SessionStatusBar.tsx +4 -1
  93. package/src/tui/SlashAutocomplete.tsx +1 -1
  94. package/src/tui/StatusBar.tsx +27 -0
  95. package/src/tui/StreamingIndicator.tsx +1 -1
  96. package/src/tui/TransportTUI.tsx +1 -1
  97. package/src/tui/TuiInteractionChannel.ts +72 -38
  98. package/src/tui/UsageSummaryEntry.tsx +1 -1
  99. package/src/tui/__tests__/PluginTUI.test.tsx +1 -1
  100. package/src/tui/__tests__/SlashAutocomplete.test.tsx +1 -1
  101. package/src/tui/__tests__/TuiInteractionChannel.display-contract.test.ts +1 -1
  102. package/src/tui/__tests__/TuiInteractionChannel.lifecycle.test.ts +5 -2
  103. package/src/tui/__tests__/TuiInteractionChannel.requestAction.test.ts +1 -1
  104. package/src/tui/__tests__/background-task-panel.test.tsx +1 -1
  105. package/src/tui/__tests__/background-task-row-format.test.ts +1 -1
  106. package/src/tui/__tests__/channel-factory-integration.test.ts +138 -0
  107. package/src/tui/__tests__/execution-workspace-switcher.test.tsx +1 -1
  108. package/src/tui/__tests__/execution-workspace-view-model.test.ts +1 -1
  109. package/src/tui/__tests__/fixtures/provider-setup-prompt-driver.tsx +1 -1
  110. package/src/tui/__tests__/input-area-flow.test.ts +1 -1
  111. package/src/tui/__tests__/pty/pty-driver.ts +135 -0
  112. package/src/tui/__tests__/pty/tui-pty.ptytest.ts +61 -0
  113. package/src/tui/__tests__/render-channel-options.test.ts +32 -0
  114. package/src/tui/__tests__/session-init-poller.test.ts +102 -0
  115. package/src/tui/__tests__/session-switch-channel.test.tsx +307 -0
  116. package/src/tui/__tests__/slash-routing-effects.test.ts +4 -1
  117. package/src/tui/__tests__/status-activity.test.ts +3 -3
  118. package/src/tui/__tests__/status-bar.test.tsx +25 -5
  119. package/src/tui/__tests__/tui-channel-init-failure.test.ts +57 -0
  120. package/src/tui/__tests__/tui-state-manager.test.ts +1 -1
  121. package/src/tui/background-task-row-format.ts +1 -1
  122. package/src/tui/execution-workspace-view-model.ts +1 -1
  123. package/src/tui/flows/input-area-flow.ts +1 -1
  124. package/src/tui/flows/permission-prompt-flow.ts +1 -1
  125. package/src/tui/flows/session-init-poller.ts +77 -0
  126. package/src/tui/hooks/command-effect-handler.ts +4 -1
  127. package/src/tui/hooks/command-effect-queue.ts +1 -1
  128. package/src/tui/hooks/side-effects-types.ts +2 -2
  129. package/src/tui/hooks/useAutocomplete.ts +3 -2
  130. package/src/tui/hooks/usePluginCallbacks.ts +1 -1
  131. package/src/tui/hooks/usePluginScreenData.ts +1 -1
  132. package/src/tui/hooks/useSideEffects.ts +1 -1
  133. package/src/tui/hooks/useSlashRouting.ts +3 -3
  134. package/src/tui/hooks/useStatusLineSettings.ts +1 -1
  135. package/src/tui/hooks/useTuiChannel.ts +3 -3
  136. package/src/tui/plugin-tui-handlers.ts +1 -1
  137. package/src/tui/render.tsx +50 -25
  138. package/src/tui/status-activity.ts +2 -2
  139. package/src/tui/tui-cli-adapter.ts +3 -3
  140. package/src/tui/tui-state-manager.ts +2 -2
  141. package/src/tui/tui-transport.ts +4 -2
  142. package/src/ws/__tests__/ws-handler.test.ts +6 -4
  143. package/src/ws/__tests__/ws-transport.test.ts +1 -1
  144. package/src/ws/ws-background-messages.ts +1 -1
  145. package/src/ws/ws-handler.ts +4 -4
  146. package/src/ws/ws-protocol.ts +6 -4
  147. package/src/ws/ws-transport-configurable.ts +4 -2
  148. package/src/ws/ws-transport.ts +1 -2
  149. package/dist/node/headless-C6tj35h3.js.map +0 -1
  150. package/dist/node/http-Br10Ps8m.js.map +0 -1
  151. package/dist/node/http-Da6Kw4oy.cjs +0 -1
  152. package/dist/node/index-27HV5PJB.d.ts.map +0 -1
  153. package/dist/node/index-BRchlFBE.d.ts.map +0 -1
  154. package/dist/node/index-BRgV_MPB.d.ts.map +0 -1
  155. package/dist/node/index-BVNhOeeU.d.ts.map +0 -1
  156. package/dist/node/index-C5KNEBO9.d.ts.map +0 -1
  157. package/dist/node/index-C9LWCL4l.d.ts.map +0 -1
  158. package/dist/node/index-COWvtBa2.d.ts.map +0 -1
  159. package/dist/node/index-TMAlNHuM.d.ts.map +0 -1
  160. package/dist/node/index-X2Zg8FEY.d.ts.map +0 -1
  161. package/dist/node/index-nBlMTFkZ.d.ts.map +0 -1
  162. package/dist/node/mcp-BAujHOMr.js.map +0 -1
  163. package/dist/node/mcp-Bl8jUfev.cjs +0 -1
  164. package/dist/node/tui-D30s8S5f.cjs +0 -24
  165. package/dist/node/tui-DIdvTeiT.js.map +0 -1
  166. package/dist/node/ws-BWel8nzl.js.map +0 -1
  167. package/dist/node/ws-tCjj2gPu.cjs +0 -1
  168. package/src/tui/InkTerminal.ts +0 -42
  169. package/src/tui/hooks/use-interactive-session-init.ts +0 -91
  170. 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-framework';
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 { IInteractiveSession } from '@robota-sdk/agent-framework';
3
- import type { IExecutionResult } from '@robota-sdk/agent-framework';
4
- import type { TBackgroundJobGroupEvent } from '@robota-sdk/agent-framework';
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-framework';
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 { IInteractiveSession, IExecutionResult } from '@robota-sdk/agent-framework';
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 = (_error: Error): void => {
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
- TBackgroundTaskEvent,
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
- export function writeStreamJsonEvent(
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-framework';
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-framework';
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-framework';
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-framework';
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 {
@@ -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-framework';
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-framework';
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-framework';
3
+ import type { IInteractiveSession } from '@robota-sdk/agent-interface-transport';
4
4
 
5
5
  function createMockSession(): IInteractiveSession {
6
6
  return {
@@ -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 { IInteractiveSession, IExecutionResult } from '@robota-sdk/agent-framework';
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. */
@@ -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-framework';
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,7 @@
1
+ /**
2
+ * Test-only fixtures (CLI-074). Exported via the `./testing` subpath; never import
3
+ * from runtime code.
4
+ */
5
+
6
+ export { createScriptedProvider } from './scripted-provider.js';
7
+ export type { IScriptedProvider, TScriptedTurn } from './scripted-provider.js';
@@ -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
- IExecutionDetailPage,
37
- } from '@robota-sdk/agent-framework';
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
- channel: TuiInteractionChannel;
43
- createChannel?: (resumeSessionId?: string) => TuiInteractionChannel;
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
- }>({ channel: props.channel, sessionId: props.resumeSessionId });
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
- const oldChannel = sessionState.channel;
77
- const newChannel = props.createChannel ? props.createChannel(sessionId) : props.channel;
78
- setSessionState({ channel: newChannel, sessionId });
79
- void oldChannel.stop();
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 & { onSessionSwitch: (sessionId: string) => void },
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;
@@ -339,10 +350,12 @@ function AppInner(
339
350
  // Session may not be initialized yet
340
351
  let permissionMode: TPermissionMode = props.permissionMode ?? 'default';
341
352
  let sessionId = '';
353
+ let activePresetId: string | undefined;
342
354
  try {
343
355
  // allow-fallback: session initializes asynchronously; use defaults until ready
344
356
  const session = interactiveSession.getSession();
345
357
  permissionMode = session.getPermissionMode();
358
+ activePresetId = session.getActivePresetId?.();
346
359
  sessionId = session.getSessionId();
347
360
  } catch {
348
361
  // allow-fallback: session initializes asynchronously; use defaults until ready
@@ -470,6 +483,7 @@ function AppInner(
470
483
  sessionName={sessionName}
471
484
  settings={statusLineSettings}
472
485
  activeAgentLabel={activeAgentLabel}
486
+ activePresetId={activePresetId}
473
487
  gitRefreshToken={gitRefreshToken}
474
488
  />
475
489
  </Box>
@@ -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-framework';
6
+ import type { IExecutionWorkspaceEntry } from '@robota-sdk/agent-interface-transport';
7
7
 
8
8
  interface IProps {
9
9
  entries: IExecutionWorkspaceEntry[];
@@ -10,7 +10,7 @@ import type {
10
10
  IExecutionDetailPage,
11
11
  IExecutionWorkspaceEntry,
12
12
  TExecutionDetailRecordKind,
13
- } from '@robota-sdk/agent-framework';
13
+ } from '@robota-sdk/agent-interface-transport';
14
14
 
15
15
  const MAX_VISIBLE_DETAIL_RECORDS = 12;
16
16
 
@@ -14,7 +14,7 @@ import {
14
14
  import type {
15
15
  IExecutionWorkspaceEntry,
16
16
  IExecutionWorkspaceSnapshot,
17
- } from '@robota-sdk/agent-framework';
17
+ } from '@robota-sdk/agent-interface-transport';
18
18
 
19
19
  const MAX_VISIBLE_WORKSPACE_ENTRIES = 8;
20
20
 
@@ -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, ICommand } from '@robota-sdk/agent-framework';
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
- } from '@robota-sdk/agent-framework';
9
+ TCommandInteractionPrompt as TInteractivePrompt,
10
+ } from '@robota-sdk/agent-interface-transport';
11
11
 
12
12
  interface IInteractivePromptProps {
13
13
  prompt: TInteractivePrompt;
@@ -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-framework';
23
+ import type { ICommandPluginAdapter } from '@robota-sdk/agent-interface-transport';
24
24
 
25
25
  type TScreenId =
26
26
  | 'main'