@robota-sdk/agent-transport 3.0.0-beta.64
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/dist/node/headless/index.cjs +1 -0
- package/dist/node/headless/index.d.ts +2 -0
- package/dist/node/headless/index.js +1 -0
- package/dist/node/headless-CWEpJXFK.js +7 -0
- package/dist/node/headless-CWEpJXFK.js.map +1 -0
- package/dist/node/headless-CsZFelG9.cjs +6 -0
- package/dist/node/http/index.cjs +1 -0
- package/dist/node/http/index.d.ts +2 -0
- package/dist/node/http/index.js +1 -0
- package/dist/node/http-CM3TJhrF.cjs +1 -0
- package/dist/node/http-DwO1AHG-.js +2 -0
- package/dist/node/http-DwO1AHG-.js.map +1 -0
- package/dist/node/index--Ti9NzQX.d.ts +64 -0
- package/dist/node/index--Ti9NzQX.d.ts.map +1 -0
- package/dist/node/index-B_rcr14p.d.ts +47 -0
- package/dist/node/index-B_rcr14p.d.ts.map +1 -0
- package/dist/node/index-C9LWCL4l.d.ts +34 -0
- package/dist/node/index-C9LWCL4l.d.ts.map +1 -0
- package/dist/node/index-CAr3ioVh.d.ts +64 -0
- package/dist/node/index-CAr3ioVh.d.ts.map +1 -0
- package/dist/node/index-CEs25wVk.d.ts +213 -0
- package/dist/node/index-CEs25wVk.d.ts.map +1 -0
- package/dist/node/index-CvXLpjJO.d.ts +213 -0
- package/dist/node/index-CvXLpjJO.d.ts.map +1 -0
- package/dist/node/index-D34WUfFH.d.ts +26 -0
- package/dist/node/index-D34WUfFH.d.ts.map +1 -0
- package/dist/node/index-Y0zHb1Bz.d.ts +47 -0
- package/dist/node/index-Y0zHb1Bz.d.ts.map +1 -0
- package/dist/node/index-k3TUjA-T.d.ts +26 -0
- package/dist/node/index-k3TUjA-T.d.ts.map +1 -0
- package/dist/node/index-nBlMTFkZ.d.ts +34 -0
- package/dist/node/index-nBlMTFkZ.d.ts.map +1 -0
- package/dist/node/index.cjs +1 -0
- package/dist/node/index.d.ts +6 -0
- package/dist/node/index.js +1 -0
- package/dist/node/mcp/index.cjs +1 -0
- package/dist/node/mcp/index.d.ts +2 -0
- package/dist/node/mcp/index.js +1 -0
- package/dist/node/mcp-BXBwF6Wu.js +2 -0
- package/dist/node/mcp-BXBwF6Wu.js.map +1 -0
- package/dist/node/mcp-DcHuGokt.cjs +1 -0
- package/dist/node/tui/index.cjs +1 -0
- package/dist/node/tui/index.d.ts +2 -0
- package/dist/node/tui/index.js +1 -0
- package/dist/node/tui-CeD_6rSo.cjs +24 -0
- package/dist/node/tui-zmDTPk4b.js +25 -0
- package/dist/node/tui-zmDTPk4b.js.map +1 -0
- package/dist/node/ws/index.cjs +1 -0
- package/dist/node/ws/index.d.ts +2 -0
- package/dist/node/ws/index.js +1 -0
- package/dist/node/ws-B-oRccFl.js +2 -0
- package/dist/node/ws-B-oRccFl.js.map +1 -0
- package/dist/node/ws-COnIgnmn.cjs +1 -0
- package/package.json +141 -0
- package/src/headless/__tests__/headless-runner-initialization.test.ts +45 -0
- package/src/headless/__tests__/headless-runner.test.ts +484 -0
- package/src/headless/__tests__/headless-skill-activation.integration.test.ts +430 -0
- package/src/headless/__tests__/headless-transport.test.ts +268 -0
- package/src/headless/headless-runner.ts +141 -0
- package/src/headless/headless-stream-json.ts +142 -0
- package/src/headless/headless-transport.ts +43 -0
- package/src/headless/index.ts +4 -0
- package/src/http/__tests__/http-transport.test.ts +55 -0
- package/src/http/__tests__/routes.test.ts +168 -0
- package/src/http/http-transport.ts +42 -0
- package/src/http/index.ts +4 -0
- package/src/http/routes.ts +151 -0
- package/src/index.ts +5 -0
- package/src/mcp/__tests__/mcp-server.test.ts +66 -0
- package/src/mcp/__tests__/mcp-transport.test.ts +46 -0
- package/src/mcp/index.ts +4 -0
- package/src/mcp/mcp-server.ts +162 -0
- package/src/mcp/mcp-transport.ts +48 -0
- package/src/tui/App.tsx +478 -0
- package/src/tui/BackgroundTaskPanel.tsx +34 -0
- package/src/tui/CjkTextInput.tsx +204 -0
- package/src/tui/ConfirmPrompt.tsx +69 -0
- package/src/tui/ExecutionWorkspaceDetailPane.tsx +62 -0
- package/src/tui/ExecutionWorkspaceSwitcher.tsx +185 -0
- package/src/tui/InkTerminal.ts +42 -0
- package/src/tui/InputArea.tsx +298 -0
- package/src/tui/InteractivePrompt.tsx +57 -0
- package/src/tui/ListPicker.tsx +94 -0
- package/src/tui/MenuSelect.tsx +103 -0
- package/src/tui/MessageList.tsx +282 -0
- package/src/tui/PermissionPrompt.tsx +84 -0
- package/src/tui/PluginTUI.tsx +256 -0
- package/src/tui/SessionPicker.tsx +66 -0
- package/src/tui/SessionStatusBar.tsx +66 -0
- package/src/tui/SlashAutocomplete.tsx +110 -0
- package/src/tui/StatusBar.tsx +213 -0
- package/src/tui/StreamingIndicator.tsx +91 -0
- package/src/tui/TextPrompt.tsx +80 -0
- package/src/tui/ToolCommandOutput.tsx +37 -0
- package/src/tui/ToolDiffBlock.tsx +30 -0
- package/src/tui/TransportTUI.tsx +116 -0
- package/src/tui/UpdateNotice.tsx +14 -0
- package/src/tui/UsageSummaryEntry.tsx +38 -0
- package/src/tui/WaveText.tsx +44 -0
- package/src/tui/__tests__/InteractivePrompt.test.tsx +82 -0
- package/src/tui/__tests__/ListPicker.test.tsx +159 -0
- package/src/tui/__tests__/MenuSelect.test.tsx +103 -0
- package/src/tui/__tests__/PluginTUI.test.tsx +167 -0
- package/src/tui/__tests__/SlashAutocomplete.test.tsx +140 -0
- package/src/tui/__tests__/TextPrompt.test.tsx +98 -0
- package/src/tui/__tests__/UpdateNotice.test.tsx +15 -0
- package/src/tui/__tests__/abort-after-permission.test.tsx +169 -0
- package/src/tui/__tests__/abort-streaming-e2e.test.tsx +183 -0
- package/src/tui/__tests__/background-task-panel.test.tsx +53 -0
- package/src/tui/__tests__/background-task-row-format.test.ts +59 -0
- package/src/tui/__tests__/cjk-text-input-flow.test.ts +109 -0
- package/src/tui/__tests__/cjk-text-input.test.ts +191 -0
- package/src/tui/__tests__/command-effect-handler.test.ts +128 -0
- package/src/tui/__tests__/command-output-summary.test.ts +95 -0
- package/src/tui/__tests__/compact-event-bridge.test.ts +20 -0
- package/src/tui/__tests__/confirm-permission-flow.test.ts +91 -0
- package/src/tui/__tests__/confirm-prompt.test.tsx +87 -0
- package/src/tui/__tests__/execution-workspace-switcher.test.tsx +110 -0
- package/src/tui/__tests__/execution-workspace-view-model.test.ts +93 -0
- package/src/tui/__tests__/fixtures/provider-setup-prompt-driver.tsx +122 -0
- package/src/tui/__tests__/input-area-flow.test.ts +152 -0
- package/src/tui/__tests__/message-list-rendering.test.tsx +353 -0
- package/src/tui/__tests__/model-change-side-effect.test.ts +91 -0
- package/src/tui/__tests__/prompt-queue.test.tsx +255 -0
- package/src/tui/__tests__/provider-setup-pty-e2e.test.ts +233 -0
- package/src/tui/__tests__/render-markdown.test.ts +72 -0
- package/src/tui/__tests__/selection-flow.test.ts +61 -0
- package/src/tui/__tests__/slash-routing-effects.test.ts +225 -0
- package/src/tui/__tests__/status-activity.test.ts +71 -0
- package/src/tui/__tests__/status-bar.test.tsx +157 -0
- package/src/tui/__tests__/streaming-indicator.test.tsx +137 -0
- package/src/tui/__tests__/text-prompt-flow.test.ts +77 -0
- package/src/tui/__tests__/tui-state-manager.test.ts +401 -0
- package/src/tui/background-task-row-format.ts +52 -0
- package/src/tui/command-output-summary.ts +122 -0
- package/src/tui/execution-workspace-view-model.ts +123 -0
- package/src/tui/flows/cjk-text-input-flow.ts +285 -0
- package/src/tui/flows/confirm-prompt-flow.ts +45 -0
- package/src/tui/flows/input-area-flow.ts +186 -0
- package/src/tui/flows/permission-prompt-flow.ts +76 -0
- package/src/tui/flows/selection-flow.ts +126 -0
- package/src/tui/flows/text-prompt-flow.ts +98 -0
- package/src/tui/hooks/command-effect-handler.ts +98 -0
- package/src/tui/hooks/command-effect-queue.ts +39 -0
- package/src/tui/hooks/model-change-side-effect.ts +63 -0
- package/src/tui/hooks/side-effects-types.ts +38 -0
- package/src/tui/hooks/use-interactive-session-init.ts +50 -0
- package/src/tui/hooks/useAutocomplete.ts +85 -0
- package/src/tui/hooks/useInteractiveSession.ts +273 -0
- package/src/tui/hooks/usePermissionQueue.ts +51 -0
- package/src/tui/hooks/usePluginCallbacks.ts +30 -0
- package/src/tui/hooks/usePluginScreenData.ts +84 -0
- package/src/tui/hooks/useSideEffects.ts +210 -0
- package/src/tui/hooks/useSlashRouting.ts +117 -0
- package/src/tui/hooks/useStatusLineSettings.ts +35 -0
- package/src/tui/index.ts +3 -0
- package/src/tui/plugin-tui-handlers.ts +163 -0
- package/src/tui/render-markdown.ts +129 -0
- package/src/tui/render.tsx +60 -0
- package/src/tui/status-activity.ts +63 -0
- package/src/tui/tui-cli-adapter-context.tsx +12 -0
- package/src/tui/tui-cli-adapter.ts +25 -0
- package/src/tui/tui-state-manager.ts +225 -0
- package/src/tui/tui-transport.ts +32 -0
- package/src/tui/types.ts +14 -0
- package/src/tui/utils/__tests__/edit-diff.test.ts +426 -0
- package/src/tui/utils/__tests__/paste-detection.test.ts +116 -0
- package/src/tui/utils/__tests__/paste-labels.test.ts +46 -0
- package/src/tui/utils/__tests__/tool-call-extractor.test.ts +227 -0
- package/src/tui/utils/__tests__/tool-diff-summary.test.ts +104 -0
- package/src/tui/utils/edit-diff.ts +152 -0
- package/src/tui/utils/paste-labels.ts +9 -0
- package/src/tui/utils/tool-call-extractor.ts +91 -0
- package/src/tui/utils/tool-diff-summary.ts +75 -0
- package/src/ws/__tests__/ws-handler.test.ts +407 -0
- package/src/ws/__tests__/ws-transport.test.ts +53 -0
- package/src/ws/index.ts +13 -0
- package/src/ws/ws-background-messages.ts +170 -0
- package/src/ws/ws-handler.ts +279 -0
- package/src/ws/ws-protocol.ts +76 -0
- package/src/ws/ws-transport-configurable.ts +123 -0
- package/src/ws/ws-transport.ts +42 -0
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import type { IInteractiveSession, IExecutionResult } from '@robota-sdk/agent-framework';
|
|
2
|
+
import { executeSlashCommandIfPresent, subscribeStreamJsonEvents } from './headless-stream-json.js';
|
|
3
|
+
|
|
4
|
+
export type TOutputFormat = 'text' | 'json' | 'stream-json';
|
|
5
|
+
|
|
6
|
+
export interface IHeadlessRunnerOptions {
|
|
7
|
+
session: IInteractiveSession;
|
|
8
|
+
outputFormat: TOutputFormat;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function createHeadlessRunner(options: IHeadlessRunnerOptions): {
|
|
12
|
+
run: (prompt: string) => Promise<number>;
|
|
13
|
+
} {
|
|
14
|
+
const { session, outputFormat } = options;
|
|
15
|
+
return {
|
|
16
|
+
run: (prompt: string): Promise<number> => {
|
|
17
|
+
if (outputFormat === 'text') return runTextFormat(session, prompt);
|
|
18
|
+
if (outputFormat === 'json') return runJsonFormat(session, prompt);
|
|
19
|
+
return runStreamJsonFormat(session, prompt);
|
|
20
|
+
},
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function writeJsonResult(
|
|
25
|
+
sessionId: string,
|
|
26
|
+
result: string,
|
|
27
|
+
subtype: 'success' | 'error',
|
|
28
|
+
): void {
|
|
29
|
+
const output = JSON.stringify({ type: 'result', result, session_id: sessionId, subtype });
|
|
30
|
+
process.stdout.write(output + '\n');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function getSessionId(session: IInteractiveSession): string {
|
|
34
|
+
try {
|
|
35
|
+
return session.getSession().getSessionId();
|
|
36
|
+
} catch {
|
|
37
|
+
// allow-fallback: session may not be initialized yet
|
|
38
|
+
return '';
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function runTextFormat(session: IInteractiveSession, prompt: string): Promise<number> {
|
|
43
|
+
return new Promise<number>((resolve) => {
|
|
44
|
+
const cleanup = (): void => {
|
|
45
|
+
session.off('complete', onComplete);
|
|
46
|
+
session.off('interrupted', onInterrupted);
|
|
47
|
+
session.off('error', onError);
|
|
48
|
+
};
|
|
49
|
+
const onComplete = (result: IExecutionResult): void => {
|
|
50
|
+
cleanup();
|
|
51
|
+
process.stdout.write(result.response + '\n');
|
|
52
|
+
resolve(0);
|
|
53
|
+
};
|
|
54
|
+
const onInterrupted = (result: IExecutionResult): void => {
|
|
55
|
+
cleanup();
|
|
56
|
+
if (result.response) process.stdout.write(result.response + '\n');
|
|
57
|
+
resolve(0);
|
|
58
|
+
};
|
|
59
|
+
const onError = (_error: Error): void => {
|
|
60
|
+
cleanup();
|
|
61
|
+
resolve(1);
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
session.on('complete', onComplete);
|
|
65
|
+
session.on('interrupted', onInterrupted);
|
|
66
|
+
session.on('error', onError);
|
|
67
|
+
|
|
68
|
+
void executeSlashCommandIfPresent(session, prompt).then((cmd) => {
|
|
69
|
+
if (cmd.kind === 'command-result') {
|
|
70
|
+
cleanup();
|
|
71
|
+
process.stdout.write(cmd.result.message + '\n');
|
|
72
|
+
resolve(cmd.result.success ? 0 : 1);
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
if (cmd.kind !== 'session-execution') void session.submit(prompt);
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function runJsonFormat(session: IInteractiveSession, prompt: string): Promise<number> {
|
|
81
|
+
return new Promise<number>((resolve) => {
|
|
82
|
+
const cleanup = (): void => {
|
|
83
|
+
session.off('complete', onComplete);
|
|
84
|
+
session.off('interrupted', onInterrupted);
|
|
85
|
+
session.off('error', onError);
|
|
86
|
+
};
|
|
87
|
+
const onComplete = (result: IExecutionResult): void => {
|
|
88
|
+
cleanup();
|
|
89
|
+
writeJsonResult(getSessionId(session), result.response, 'success');
|
|
90
|
+
resolve(0);
|
|
91
|
+
};
|
|
92
|
+
const onInterrupted = (result: IExecutionResult): void => {
|
|
93
|
+
cleanup();
|
|
94
|
+
writeJsonResult(getSessionId(session), result.response, 'success');
|
|
95
|
+
resolve(0);
|
|
96
|
+
};
|
|
97
|
+
const onError = (_error: Error): void => {
|
|
98
|
+
cleanup();
|
|
99
|
+
writeJsonResult(getSessionId(session), '', 'error');
|
|
100
|
+
resolve(1);
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
session.on('complete', onComplete);
|
|
104
|
+
session.on('interrupted', onInterrupted);
|
|
105
|
+
session.on('error', onError);
|
|
106
|
+
|
|
107
|
+
void executeSlashCommandIfPresent(session, prompt).then((cmd) => {
|
|
108
|
+
if (cmd.kind === 'command-result') {
|
|
109
|
+
cleanup();
|
|
110
|
+
writeJsonResult(
|
|
111
|
+
getSessionId(session),
|
|
112
|
+
cmd.result.message,
|
|
113
|
+
cmd.result.success ? 'success' : 'error',
|
|
114
|
+
);
|
|
115
|
+
resolve(cmd.result.success ? 0 : 1);
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
if (cmd.kind !== 'session-execution') void session.submit(prompt);
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function runStreamJsonFormat(session: IInteractiveSession, prompt: string): Promise<number> {
|
|
124
|
+
return new Promise<number>((resolve) => {
|
|
125
|
+
const cleanup = subscribeStreamJsonEvents(session, getSessionId, writeJsonResult, resolve);
|
|
126
|
+
|
|
127
|
+
void executeSlashCommandIfPresent(session, prompt).then((cmd) => {
|
|
128
|
+
if (cmd.kind === 'command-result') {
|
|
129
|
+
cleanup();
|
|
130
|
+
writeJsonResult(
|
|
131
|
+
getSessionId(session),
|
|
132
|
+
cmd.result.message,
|
|
133
|
+
cmd.result.success ? 'success' : 'error',
|
|
134
|
+
);
|
|
135
|
+
resolve(cmd.result.success ? 0 : 1);
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
if (cmd.kind !== 'session-execution') void session.submit(prompt);
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import type {
|
|
3
|
+
IInteractiveSession,
|
|
4
|
+
IExecutionResult,
|
|
5
|
+
ICommandResult,
|
|
6
|
+
TBackgroundJobGroupEvent,
|
|
7
|
+
TBackgroundTaskEvent,
|
|
8
|
+
} from '@robota-sdk/agent-framework';
|
|
9
|
+
|
|
10
|
+
type TSlashCommandExecution =
|
|
11
|
+
| { readonly kind: 'not-slash' }
|
|
12
|
+
| { readonly kind: 'command-result'; readonly result: ICommandResult }
|
|
13
|
+
| { readonly kind: 'session-execution' };
|
|
14
|
+
|
|
15
|
+
function parseSlashCommand(prompt: string): { name: string; args: string } | null {
|
|
16
|
+
const trimmed = prompt.trimStart();
|
|
17
|
+
if (!trimmed.startsWith('/')) return null;
|
|
18
|
+
const withoutSlash = trimmed.slice(1);
|
|
19
|
+
const [name = '', ...args] = withoutSlash.split(/\s+/);
|
|
20
|
+
if (name.length === 0) return null;
|
|
21
|
+
return { name, args: args.join(' ') };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export async function executeSlashCommandIfPresent(
|
|
25
|
+
session: IInteractiveSession,
|
|
26
|
+
prompt: string,
|
|
27
|
+
): Promise<TSlashCommandExecution> {
|
|
28
|
+
const command = parseSlashCommand(prompt);
|
|
29
|
+
if (!command) return { kind: 'not-slash' };
|
|
30
|
+
|
|
31
|
+
const result = await session.executeCommand(command.name, command.args);
|
|
32
|
+
if (result) {
|
|
33
|
+
if (result.effects?.some((effect) => effect.type === 'session-execution-started')) {
|
|
34
|
+
return { kind: 'session-execution' };
|
|
35
|
+
}
|
|
36
|
+
return { kind: 'command-result', result };
|
|
37
|
+
}
|
|
38
|
+
return {
|
|
39
|
+
kind: 'command-result',
|
|
40
|
+
result: { message: `Unknown command "/${command.name}".`, success: false },
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
type TStreamJsonEvent =
|
|
45
|
+
| {
|
|
46
|
+
type: 'content_block_delta';
|
|
47
|
+
delta: { type: 'text_delta'; text: string };
|
|
48
|
+
}
|
|
49
|
+
| {
|
|
50
|
+
type: 'background_task_event';
|
|
51
|
+
background_task_event: TBackgroundTaskEvent;
|
|
52
|
+
}
|
|
53
|
+
| {
|
|
54
|
+
type: 'background_job_group_event';
|
|
55
|
+
background_job_group_event: TBackgroundJobGroupEvent;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
interface IStreamJsonHandlers {
|
|
59
|
+
onTextDelta: (text: string) => void;
|
|
60
|
+
onBackgroundTaskEvent: (event: TBackgroundTaskEvent) => void;
|
|
61
|
+
onBackgroundJobGroupEvent: (event: TBackgroundJobGroupEvent) => void;
|
|
62
|
+
onComplete: (result: IExecutionResult) => void;
|
|
63
|
+
onInterrupted: (result: IExecutionResult) => void;
|
|
64
|
+
onError: (error: Error) => void;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function writeStreamJsonEvent(
|
|
68
|
+
session: IInteractiveSession,
|
|
69
|
+
getSessionId: (s: IInteractiveSession) => string,
|
|
70
|
+
event: TStreamJsonEvent,
|
|
71
|
+
): void {
|
|
72
|
+
const output = JSON.stringify({
|
|
73
|
+
type: 'stream_event',
|
|
74
|
+
event,
|
|
75
|
+
session_id: getSessionId(session),
|
|
76
|
+
uuid: randomUUID(),
|
|
77
|
+
});
|
|
78
|
+
process.stdout.write(output + '\n');
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function subscribeStreamJsonEvents(
|
|
82
|
+
session: IInteractiveSession,
|
|
83
|
+
getSessionId: (s: IInteractiveSession) => string,
|
|
84
|
+
writeJsonResult: (sessionId: string, result: string, subtype: 'success' | 'error') => void,
|
|
85
|
+
resolve: (exitCode: number) => void,
|
|
86
|
+
): () => void {
|
|
87
|
+
const emit = (event: TStreamJsonEvent): void =>
|
|
88
|
+
writeStreamJsonEvent(session, getSessionId, event);
|
|
89
|
+
|
|
90
|
+
const onTextDelta = (text: string): void =>
|
|
91
|
+
emit({ type: 'content_block_delta', delta: { type: 'text_delta', text } });
|
|
92
|
+
const onBackgroundTaskEvent = (event: TBackgroundTaskEvent): void =>
|
|
93
|
+
emit({ type: 'background_task_event', background_task_event: event });
|
|
94
|
+
const onBackgroundJobGroupEvent = (event: TBackgroundJobGroupEvent): void =>
|
|
95
|
+
emit({ type: 'background_job_group_event', background_job_group_event: event });
|
|
96
|
+
|
|
97
|
+
const cleanup = (): void =>
|
|
98
|
+
unsubscribeStreamJsonEvents(session, {
|
|
99
|
+
onTextDelta,
|
|
100
|
+
onBackgroundTaskEvent,
|
|
101
|
+
onBackgroundJobGroupEvent,
|
|
102
|
+
onComplete,
|
|
103
|
+
onInterrupted,
|
|
104
|
+
onError,
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
const onComplete = (result: IExecutionResult): void => {
|
|
108
|
+
cleanup();
|
|
109
|
+
writeJsonResult(getSessionId(session), result.response, 'success');
|
|
110
|
+
resolve(0);
|
|
111
|
+
};
|
|
112
|
+
const onInterrupted = (result: IExecutionResult): void => {
|
|
113
|
+
cleanup();
|
|
114
|
+
writeJsonResult(getSessionId(session), result.response, 'success');
|
|
115
|
+
resolve(0);
|
|
116
|
+
};
|
|
117
|
+
const onError = (_error: Error): void => {
|
|
118
|
+
cleanup();
|
|
119
|
+
writeJsonResult(getSessionId(session), '', 'error');
|
|
120
|
+
resolve(1);
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
session.on('text_delta', onTextDelta);
|
|
124
|
+
session.on('background_task_event', onBackgroundTaskEvent);
|
|
125
|
+
session.on('background_job_group_event', onBackgroundJobGroupEvent);
|
|
126
|
+
session.on('complete', onComplete);
|
|
127
|
+
session.on('interrupted', onInterrupted);
|
|
128
|
+
session.on('error', onError);
|
|
129
|
+
return cleanup;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function unsubscribeStreamJsonEvents(
|
|
133
|
+
session: IInteractiveSession,
|
|
134
|
+
handlers: IStreamJsonHandlers,
|
|
135
|
+
): void {
|
|
136
|
+
session.off('text_delta', handlers.onTextDelta);
|
|
137
|
+
session.off('background_task_event', handlers.onBackgroundTaskEvent);
|
|
138
|
+
session.off('background_job_group_event', handlers.onBackgroundJobGroupEvent);
|
|
139
|
+
session.off('complete', handlers.onComplete);
|
|
140
|
+
session.off('interrupted', handlers.onInterrupted);
|
|
141
|
+
session.off('error', handlers.onError);
|
|
142
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ITransportAdapter implementation for headless transport.
|
|
3
|
+
*
|
|
4
|
+
* Wraps createHeadlessRunner into the unified ITransportAdapter interface.
|
|
5
|
+
* After start() completes, getExitCode() returns the runner's exit code.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { IInteractiveSession } from '@robota-sdk/agent-framework';
|
|
9
|
+
import type { ITransportAdapter } from '@robota-sdk/agent-interface-transport';
|
|
10
|
+
import { createHeadlessRunner } from './headless-runner.js';
|
|
11
|
+
import type { TOutputFormat } from './headless-runner.js';
|
|
12
|
+
|
|
13
|
+
export interface IHeadlessTransportOptions {
|
|
14
|
+
/** Output format: 'text', 'json', or 'stream-json'. */
|
|
15
|
+
outputFormat: TOutputFormat;
|
|
16
|
+
/** The prompt to execute. */
|
|
17
|
+
prompt: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function createHeadlessTransport(
|
|
21
|
+
options: IHeadlessTransportOptions,
|
|
22
|
+
): ITransportAdapter<IInteractiveSession> & { getExitCode(): number } {
|
|
23
|
+
let session: IInteractiveSession | null = null;
|
|
24
|
+
let exitCode = 0;
|
|
25
|
+
|
|
26
|
+
return {
|
|
27
|
+
name: 'headless',
|
|
28
|
+
attach(s: IInteractiveSession) {
|
|
29
|
+
session = s;
|
|
30
|
+
},
|
|
31
|
+
async start() {
|
|
32
|
+
if (!session) throw new Error('No session attached. Call attach() first.');
|
|
33
|
+
const runner = createHeadlessRunner({ session, outputFormat: options.outputFormat });
|
|
34
|
+
exitCode = await runner.run(options.prompt);
|
|
35
|
+
},
|
|
36
|
+
async stop() {
|
|
37
|
+
/* no-op: headless runner completes in start() */
|
|
38
|
+
},
|
|
39
|
+
getExitCode() {
|
|
40
|
+
return exitCode;
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export { createHeadlessRunner } from './headless-runner.js';
|
|
2
|
+
export type { IHeadlessRunnerOptions, TOutputFormat } from './headless-runner.js';
|
|
3
|
+
export { createHeadlessTransport } from './headless-transport.js';
|
|
4
|
+
export type { IHeadlessTransportOptions } from './headless-transport.js';
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
2
|
+
import { createHttpTransport } from '../http-transport.js';
|
|
3
|
+
import type { IInteractiveSession } from '@robota-sdk/agent-framework';
|
|
4
|
+
|
|
5
|
+
function createMockSession(): IInteractiveSession {
|
|
6
|
+
return {
|
|
7
|
+
submit: vi.fn(),
|
|
8
|
+
abort: vi.fn(),
|
|
9
|
+
cancelQueue: vi.fn(),
|
|
10
|
+
getMessages: vi.fn().mockReturnValue([]),
|
|
11
|
+
getContextState: vi
|
|
12
|
+
.fn()
|
|
13
|
+
.mockReturnValue({ usedPercentage: 0, usedTokens: 0, maxTokens: 200000 }),
|
|
14
|
+
isExecuting: vi.fn().mockReturnValue(false),
|
|
15
|
+
getPendingPrompt: vi.fn().mockReturnValue(null),
|
|
16
|
+
executeCommand: vi.fn().mockResolvedValue({ message: 'ok', success: true }),
|
|
17
|
+
listCommands: vi.fn().mockReturnValue([]),
|
|
18
|
+
on: vi.fn(),
|
|
19
|
+
off: vi.fn(),
|
|
20
|
+
} as unknown as IInteractiveSession;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
describe('createHttpTransport', () => {
|
|
24
|
+
it('returns an adapter with name "http"', () => {
|
|
25
|
+
const transport = createHttpTransport();
|
|
26
|
+
expect(transport.name).toBe('http');
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('throws if start() is called without attach()', async () => {
|
|
30
|
+
const transport = createHttpTransport();
|
|
31
|
+
await expect(transport.start()).rejects.toThrow('No session attached');
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('throws if getApp() is called before start()', () => {
|
|
35
|
+
const transport = createHttpTransport();
|
|
36
|
+
expect(() => transport.getApp()).toThrow('Transport not started');
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('creates a Hono app after attach + start', async () => {
|
|
40
|
+
const transport = createHttpTransport();
|
|
41
|
+
transport.attach(createMockSession() as never);
|
|
42
|
+
await transport.start();
|
|
43
|
+
const app = transport.getApp();
|
|
44
|
+
expect(app).toBeDefined();
|
|
45
|
+
expect(typeof app.fetch).toBe('function');
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('nullifies app after stop()', async () => {
|
|
49
|
+
const transport = createHttpTransport();
|
|
50
|
+
transport.attach(createMockSession() as never);
|
|
51
|
+
await transport.start();
|
|
52
|
+
await transport.stop();
|
|
53
|
+
expect(() => transport.getApp()).toThrow('Transport not started');
|
|
54
|
+
});
|
|
55
|
+
});
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for HTTP transport routes.
|
|
3
|
+
* Uses Hono's built-in test client — no real HTTP server needed.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
7
|
+
import { createAgentRoutes } from '../routes.js';
|
|
8
|
+
import type { IInteractiveSession } from '@robota-sdk/agent-framework';
|
|
9
|
+
|
|
10
|
+
function createMockSession(overrides?: Record<string, unknown>) {
|
|
11
|
+
return {
|
|
12
|
+
submit: vi.fn(),
|
|
13
|
+
abort: vi.fn(),
|
|
14
|
+
cancelQueue: vi.fn(),
|
|
15
|
+
getMessages: vi.fn().mockReturnValue([
|
|
16
|
+
{ role: 'user', content: 'hello' },
|
|
17
|
+
{ role: 'assistant', content: 'world' },
|
|
18
|
+
]),
|
|
19
|
+
getContextState: vi.fn().mockReturnValue({
|
|
20
|
+
usedPercentage: 10,
|
|
21
|
+
usedTokens: 1000,
|
|
22
|
+
maxTokens: 200000,
|
|
23
|
+
}),
|
|
24
|
+
isExecuting: vi.fn().mockReturnValue(false),
|
|
25
|
+
getPendingPrompt: vi.fn().mockReturnValue(null),
|
|
26
|
+
executeCommand: vi.fn().mockResolvedValue({
|
|
27
|
+
message: 'Conversation cleared.',
|
|
28
|
+
success: true,
|
|
29
|
+
}),
|
|
30
|
+
listCommands: vi.fn().mockReturnValue([]),
|
|
31
|
+
on: vi.fn(),
|
|
32
|
+
off: vi.fn(),
|
|
33
|
+
...overrides,
|
|
34
|
+
} as unknown as IInteractiveSession;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
describe('HTTP Transport Routes', () => {
|
|
38
|
+
function createApp(session?: IInteractiveSession) {
|
|
39
|
+
const mockSession = session ?? createMockSession();
|
|
40
|
+
const app = createAgentRoutes({
|
|
41
|
+
sessionFactory: () => mockSession,
|
|
42
|
+
});
|
|
43
|
+
return { app, mockSession };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// ── POST /abort ───────────────────────────────────────────────
|
|
47
|
+
|
|
48
|
+
it('POST /abort calls session.abort()', async () => {
|
|
49
|
+
const { app, mockSession } = createApp();
|
|
50
|
+
const res = await app.request('/abort', { method: 'POST' });
|
|
51
|
+
expect(res.status).toBe(200);
|
|
52
|
+
expect(await res.json()).toEqual({ ok: true });
|
|
53
|
+
expect(mockSession.abort).toHaveBeenCalled();
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
// ── POST /cancel-queue ────────────────────────────────────────
|
|
57
|
+
|
|
58
|
+
it('POST /cancel-queue calls session.cancelQueue()', async () => {
|
|
59
|
+
const { app, mockSession } = createApp();
|
|
60
|
+
const res = await app.request('/cancel-queue', { method: 'POST' });
|
|
61
|
+
expect(res.status).toBe(200);
|
|
62
|
+
expect(await res.json()).toEqual({ ok: true });
|
|
63
|
+
expect(mockSession.cancelQueue).toHaveBeenCalled();
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
// ── GET /messages ─────────────────────────────────────────────
|
|
67
|
+
|
|
68
|
+
it('GET /messages returns message history', async () => {
|
|
69
|
+
const { app } = createApp();
|
|
70
|
+
const res = await app.request('/messages');
|
|
71
|
+
expect(res.status).toBe(200);
|
|
72
|
+
const body = await res.json();
|
|
73
|
+
expect(body).toHaveLength(2);
|
|
74
|
+
expect(body[0].role).toBe('user');
|
|
75
|
+
expect(body[1].role).toBe('assistant');
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
// ── GET /context ──────────────────────────────────────────────
|
|
79
|
+
|
|
80
|
+
it('GET /context returns context window state', async () => {
|
|
81
|
+
const { app } = createApp();
|
|
82
|
+
const res = await app.request('/context');
|
|
83
|
+
expect(res.status).toBe(200);
|
|
84
|
+
const body = await res.json();
|
|
85
|
+
expect(body.usedTokens).toBe(1000);
|
|
86
|
+
expect(body.maxTokens).toBe(200000);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
// ── GET /executing ────────────────────────────────────────────
|
|
90
|
+
|
|
91
|
+
it('GET /executing returns execution status', async () => {
|
|
92
|
+
const { app } = createApp();
|
|
93
|
+
const res = await app.request('/executing');
|
|
94
|
+
expect(res.status).toBe(200);
|
|
95
|
+
expect(await res.json()).toEqual({ executing: false });
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
// ── GET /pending ──────────────────────────────────────────────
|
|
99
|
+
|
|
100
|
+
it('GET /pending returns null when no queue', async () => {
|
|
101
|
+
const { app } = createApp();
|
|
102
|
+
const res = await app.request('/pending');
|
|
103
|
+
expect(res.status).toBe(200);
|
|
104
|
+
expect(await res.json()).toEqual({ pending: null });
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it('GET /pending returns queued prompt', async () => {
|
|
108
|
+
const mockSession = createMockSession({
|
|
109
|
+
getPendingPrompt: vi.fn().mockReturnValue('queued prompt'),
|
|
110
|
+
});
|
|
111
|
+
const { app } = createApp(mockSession);
|
|
112
|
+
const res = await app.request('/pending');
|
|
113
|
+
expect(res.status).toBe(200);
|
|
114
|
+
expect(await res.json()).toEqual({ pending: 'queued prompt' });
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
// ── POST /command ─────────────────────────────────────────────
|
|
118
|
+
|
|
119
|
+
it('POST /command executes system command via session.executeCommand()', async () => {
|
|
120
|
+
const { app, mockSession } = createApp();
|
|
121
|
+
const res = await app.request('/command', {
|
|
122
|
+
method: 'POST',
|
|
123
|
+
headers: { 'Content-Type': 'application/json' },
|
|
124
|
+
body: JSON.stringify({ name: 'clear', args: '' }),
|
|
125
|
+
});
|
|
126
|
+
expect(res.status).toBe(200);
|
|
127
|
+
const body = await res.json();
|
|
128
|
+
expect(body.success).toBe(true);
|
|
129
|
+
expect(body.message).toBe('Conversation cleared.');
|
|
130
|
+
expect(mockSession.executeCommand).toHaveBeenCalledWith('clear', '');
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it('POST /command returns 400 without name', async () => {
|
|
134
|
+
const { app } = createApp();
|
|
135
|
+
const res = await app.request('/command', {
|
|
136
|
+
method: 'POST',
|
|
137
|
+
headers: { 'Content-Type': 'application/json' },
|
|
138
|
+
body: JSON.stringify({}),
|
|
139
|
+
});
|
|
140
|
+
expect(res.status).toBe(400);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it('POST /command returns 404 for unknown command', async () => {
|
|
144
|
+
const mockSession = createMockSession({
|
|
145
|
+
executeCommand: vi.fn().mockResolvedValue(null),
|
|
146
|
+
});
|
|
147
|
+
const { app } = createApp(mockSession);
|
|
148
|
+
|
|
149
|
+
const res = await app.request('/command', {
|
|
150
|
+
method: 'POST',
|
|
151
|
+
headers: { 'Content-Type': 'application/json' },
|
|
152
|
+
body: JSON.stringify({ name: 'nonexistent' }),
|
|
153
|
+
});
|
|
154
|
+
expect(res.status).toBe(404);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
// ── POST /submit validation ───────────────────────────────────
|
|
158
|
+
|
|
159
|
+
it('POST /submit returns 400 without prompt', async () => {
|
|
160
|
+
const { app } = createApp();
|
|
161
|
+
const res = await app.request('/submit', {
|
|
162
|
+
method: 'POST',
|
|
163
|
+
headers: { 'Content-Type': 'application/json' },
|
|
164
|
+
body: JSON.stringify({}),
|
|
165
|
+
});
|
|
166
|
+
expect(res.status).toBe(400);
|
|
167
|
+
});
|
|
168
|
+
});
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ITransportAdapter implementation for HTTP transport.
|
|
3
|
+
*
|
|
4
|
+
* Wraps createAgentRoutes into the unified ITransportAdapter interface
|
|
5
|
+
* while exposing the underlying Hono app via getApp().
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { ITransportAdapter } from '@robota-sdk/agent-interface-transport';
|
|
9
|
+
import type { IInteractiveSession } from '@robota-sdk/agent-framework';
|
|
10
|
+
|
|
11
|
+
import { createAgentRoutes } from './routes.js';
|
|
12
|
+
import type { Hono } from 'hono';
|
|
13
|
+
|
|
14
|
+
export interface IHttpTransportOptions {
|
|
15
|
+
/** Optional: base path prefix for routes. */
|
|
16
|
+
basePath?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function createHttpTransport(
|
|
20
|
+
options?: IHttpTransportOptions,
|
|
21
|
+
): ITransportAdapter<IInteractiveSession> & { getApp(): Hono } {
|
|
22
|
+
let session: IInteractiveSession | null = null;
|
|
23
|
+
let app: Hono | null = null;
|
|
24
|
+
|
|
25
|
+
return {
|
|
26
|
+
name: 'http',
|
|
27
|
+
attach(s: IInteractiveSession) {
|
|
28
|
+
session = s;
|
|
29
|
+
},
|
|
30
|
+
async start() {
|
|
31
|
+
if (!session) throw new Error('No session attached. Call attach() first.');
|
|
32
|
+
app = createAgentRoutes({ sessionFactory: () => session! });
|
|
33
|
+
},
|
|
34
|
+
async stop() {
|
|
35
|
+
app = null;
|
|
36
|
+
},
|
|
37
|
+
getApp() {
|
|
38
|
+
if (!app) throw new Error('Transport not started. Call start() first.');
|
|
39
|
+
return app;
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
}
|