@robota-sdk/agent-transport 3.0.0-beta.78 → 3.0.0-beta.81
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/README.md +84 -26
- package/dist/browser/client.d.ts +2 -0
- package/dist/browser/client.js +1 -0
- package/dist/browser/index.d.ts +520 -0
- package/dist/browser/index.d.ts.map +1 -0
- package/dist/browser/index.js +2 -0
- package/dist/browser/index.js.map +1 -0
- package/dist/browser/message-decoders-BB8n-H2X.d.ts +252 -0
- package/dist/browser/message-decoders-BB8n-H2X.d.ts.map +1 -0
- package/dist/browser/message-decoders-CDmDoZPR.js +2 -0
- package/dist/browser/message-decoders-CDmDoZPR.js.map +1 -0
- package/dist/node/index.cjs +1 -1
- package/dist/node/index.d.cts +766 -0
- package/dist/node/index.d.cts.map +1 -0
- package/dist/node/index.d.ts +765 -27
- 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/node.cjs +1 -0
- package/dist/node/node.d.cts +109 -0
- package/dist/node/node.d.cts.map +1 -0
- package/dist/node/node.d.ts +109 -0
- package/dist/node/node.d.ts.map +1 -0
- package/dist/node/node.js +2 -0
- package/dist/node/node.js.map +1 -0
- package/package.json +68 -47
- package/src/__tests__/access-token-verifier.test.ts +672 -0
- package/src/__tests__/admission.test.ts +130 -0
- package/src/__tests__/channel-frames.test.ts +214 -0
- package/src/__tests__/driver-attribution-capability.test.ts +83 -0
- package/src/__tests__/handoff-chunking.test.ts +215 -0
- package/src/__tests__/handoff-manifest.test.ts +52 -0
- package/src/__tests__/message-decoders.test.ts +249 -0
- package/src/__tests__/outbound-delivery.test.ts +558 -0
- package/src/__tests__/peer-message-ledger.test.ts +200 -0
- package/src/__tests__/personal-usage-report.test.ts +193 -0
- package/src/__tests__/resume-buffer.test.ts +93 -0
- package/src/__tests__/session-event-delivery.test.ts +101 -0
- package/src/__tests__/session-message-handler-e5.test.ts +99 -0
- package/src/__tests__/session-message-handler.test.ts +590 -0
- package/src/__tests__/session-resume-bridge.test.ts +406 -0
- package/src/__tests__/submit-prompt-shape.test.ts +78 -0
- package/src/__tests__/usage-report-carrier.test.ts +98 -0
- package/src/__tests__/ws-broadcast-events.test.ts +84 -0
- package/src/background-messages.ts +169 -0
- package/src/channel-frames.ts +182 -0
- package/src/client.ts +9 -0
- package/src/handoff-chunking.ts +235 -0
- package/src/index.ts +112 -3
- package/src/message-decoders.ts +229 -0
- package/src/message-parser.ts +33 -0
- package/src/node/access-token-verifier.ts +363 -0
- package/src/node/admission.ts +153 -0
- package/src/node/handoff-manifest.ts +77 -0
- package/src/node/index.ts +10 -0
- package/src/outbound-delivery.ts +231 -0
- package/src/peer-message-ledger.ts +193 -0
- package/src/protocol-session.ts +28 -0
- package/src/resume-buffer.ts +123 -0
- package/src/session-events.ts +198 -0
- package/src/session-message-handler.ts +282 -0
- package/src/session-query-messages.ts +42 -0
- package/src/session-resume-bridge.ts +252 -0
- package/src/usage-messages.ts +195 -0
- package/src/wire-messages.ts +165 -0
- package/dist/node/headless/index.cjs +0 -1
- package/dist/node/headless/index.d.ts +0 -2
- package/dist/node/headless/index.js +0 -1
- package/dist/node/headless-DG_MEbf0.js +0 -19
- package/dist/node/headless-DG_MEbf0.js.map +0 -1
- package/dist/node/headless-xq77JBq0.cjs +0 -18
- package/dist/node/index-CrJ2Lhn3.d.ts +0 -96
- package/dist/node/index-CrJ2Lhn3.d.ts.map +0 -1
- package/dist/node/index-JIvzrtaA.d.ts +0 -52
- package/dist/node/index-JIvzrtaA.d.ts.map +0 -1
- package/dist/node/index-JTD_xtEQ.d.ts +0 -96
- package/dist/node/index-JTD_xtEQ.d.ts.map +0 -1
- package/dist/node/index-y_HBOfzO.d.ts +0 -52
- package/dist/node/index-y_HBOfzO.d.ts.map +0 -1
- package/dist/node/programmatic/index.cjs +0 -1
- package/dist/node/programmatic/index.d.ts +0 -2
- package/dist/node/programmatic/index.js +0 -1
- package/dist/node/programmatic-6wLf68qJ.cjs +0 -1
- package/dist/node/programmatic-70dxIXtS.js +0 -2
- package/dist/node/programmatic-70dxIXtS.js.map +0 -1
- package/dist/node/testing/index.cjs +0 -1
- package/dist/node/testing/index.d.ts +0 -2
- package/dist/node/testing/index.js +0 -1
- package/src/__tests__/programmatic/programmatic-ask-user.test.ts +0 -32
- package/src/__tests__/programmatic/programmatic-driver.test.ts +0 -107
- package/src/__tests__/programmatic/programmatic-flows.test.ts +0 -140
- package/src/__tests__/transport-registry.stop-all.test.ts +0 -53
- package/src/headless/HeadlessInteractionChannel.ts +0 -129
- package/src/headless/__tests__/headless-channel-options.test.ts +0 -106
- package/src/headless/__tests__/headless-goal-runner.test.ts +0 -108
- package/src/headless/__tests__/headless-provider-failure.integration.test.ts +0 -143
- package/src/headless/__tests__/headless-runner-initialization.test.ts +0 -45
- package/src/headless/__tests__/headless-runner.test.ts +0 -508
- package/src/headless/__tests__/headless-skill-activation.integration.test.ts +0 -430
- package/src/headless/__tests__/headless-transport.test.ts +0 -267
- package/src/headless/cli-input.ts +0 -50
- package/src/headless/headless-runner.ts +0 -230
- package/src/headless/headless-stream-json.ts +0 -148
- package/src/headless/headless-transport.ts +0 -43
- package/src/headless/index.ts +0 -8
- package/src/headless/print-terminal.ts +0 -57
- package/src/programmatic/ProgrammaticInteractionChannel.ts +0 -82
- package/src/programmatic/createProgrammaticAgent.ts +0 -71
- package/src/programmatic/index.ts +0 -5
- package/src/testing/index.ts +0 -10
- package/src/transport-registry.ts +0 -103
|
@@ -1,140 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* TEST-009 Phase 2: conversation + slash-command flows driven through `IAgentDriver`.
|
|
3
|
-
*
|
|
4
|
-
* Exercises the in-process driver (`createProgrammaticAgent`) against the REAL framework loop with the
|
|
5
|
-
* scripted provider: multi-turn context, a slash command producing a `command-result`, and the CMD-004
|
|
6
|
-
* ask seam (a command self-asks via `getUserInteraction()`, pre-answered through `queueUserAction`).
|
|
7
|
-
* All assertions read the shared `InteractionEvent` stream the contract exposes.
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
import { mkdtempSync, rmSync } from 'node:fs';
|
|
11
|
-
import { tmpdir } from 'node:os';
|
|
12
|
-
import { join } from 'node:path';
|
|
13
|
-
|
|
14
|
-
import { confirmAction, isConfirmed } from '@robota-sdk/agent-core';
|
|
15
|
-
import { createScriptedProvider } from '@robota-sdk/agent-core/testing';
|
|
16
|
-
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
17
|
-
|
|
18
|
-
import { createProgrammaticAgent } from '../../programmatic/index.js';
|
|
19
|
-
|
|
20
|
-
import type { ICommandModule } from '@robota-sdk/agent-framework';
|
|
21
|
-
import type { IAgentDriver, InteractionEvent } from '@robota-sdk/agent-interface-transport';
|
|
22
|
-
|
|
23
|
-
function commandResults(
|
|
24
|
-
events: readonly InteractionEvent[],
|
|
25
|
-
): Array<{ name: string; output: string }> {
|
|
26
|
-
return events
|
|
27
|
-
.filter(
|
|
28
|
-
(e): e is Extract<InteractionEvent, { type: 'command-result' }> =>
|
|
29
|
-
e.type === 'command-result',
|
|
30
|
-
)
|
|
31
|
-
.map((e) => ({ name: e.name, output: e.output }));
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
/** A read-only `/ping` command that returns a fixed result — no host adapters needed. */
|
|
35
|
-
const PING_MODULE: ICommandModule = {
|
|
36
|
-
name: 'ping',
|
|
37
|
-
systemCommands: [
|
|
38
|
-
{
|
|
39
|
-
name: 'ping',
|
|
40
|
-
description: 'test ping',
|
|
41
|
-
userInvocable: true,
|
|
42
|
-
safety: 'read-only',
|
|
43
|
-
requiresPermission: false,
|
|
44
|
-
execute: () => ({ message: 'PONG', success: true }),
|
|
45
|
-
},
|
|
46
|
-
],
|
|
47
|
-
};
|
|
48
|
-
|
|
49
|
-
/** A `/danger` command that self-asks for confirmation via the CMD-004 ask seam. */
|
|
50
|
-
const CONFIRM_MODULE: ICommandModule = {
|
|
51
|
-
name: 'danger',
|
|
52
|
-
systemCommands: [
|
|
53
|
-
{
|
|
54
|
-
name: 'danger',
|
|
55
|
-
description: 'requires confirmation',
|
|
56
|
-
userInvocable: true,
|
|
57
|
-
safety: 'read-only',
|
|
58
|
-
requiresPermission: false,
|
|
59
|
-
lifecycle: 'inline',
|
|
60
|
-
execute: async (context) => {
|
|
61
|
-
const ui = context.getUserInteraction?.();
|
|
62
|
-
const response = ui ? await ui.ask(confirmAction('danger', 'Are you sure?')) : undefined;
|
|
63
|
-
if (!response || !isConfirmed(response)) {
|
|
64
|
-
return { message: 'DANGER_CANCELLED', success: true };
|
|
65
|
-
}
|
|
66
|
-
return { message: 'DANGER_RAN', success: true };
|
|
67
|
-
},
|
|
68
|
-
},
|
|
69
|
-
],
|
|
70
|
-
};
|
|
71
|
-
|
|
72
|
-
describe('IAgentDriver conversation + slash flows (TEST-009 Phase 2)', () => {
|
|
73
|
-
let cwd: string;
|
|
74
|
-
let driver: IAgentDriver | undefined;
|
|
75
|
-
|
|
76
|
-
beforeEach(() => {
|
|
77
|
-
cwd = mkdtempSync(join(tmpdir(), 'robota-flows-'));
|
|
78
|
-
});
|
|
79
|
-
afterEach(async () => {
|
|
80
|
-
await driver?.stop();
|
|
81
|
-
driver = undefined;
|
|
82
|
-
rmSync(cwd, { recursive: true, force: true });
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
it('carries multi-turn conversation context across sends', async () => {
|
|
86
|
-
const scripted = createScriptedProvider([{ text: 'NOTED_42' }, { text: 'IT_WAS_42' }]);
|
|
87
|
-
driver = createProgrammaticAgent({ provider: scripted.provider, cwd });
|
|
88
|
-
await driver.start();
|
|
89
|
-
|
|
90
|
-
await driver.send('Remember the number 42');
|
|
91
|
-
expect(driver.lastAssistantText()).toBe('NOTED_42');
|
|
92
|
-
|
|
93
|
-
await driver.send('What number?');
|
|
94
|
-
expect(driver.lastAssistantText()).toBe('IT_WAS_42');
|
|
95
|
-
expect(driver.assistantReplies()).toEqual(['NOTED_42', 'IT_WAS_42']);
|
|
96
|
-
|
|
97
|
-
// The second request must carry the prior turn (real session context).
|
|
98
|
-
const second = (scripted.requests[1] ?? []).map((m) => String(m.content)).join('\n');
|
|
99
|
-
expect(second).toContain('Remember the number 42');
|
|
100
|
-
expect(second).toContain('NOTED_42');
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
it('runs a slash command and surfaces a command-result event', async () => {
|
|
104
|
-
const scripted = createScriptedProvider([{ text: 'unused' }]);
|
|
105
|
-
driver = createProgrammaticAgent({
|
|
106
|
-
provider: scripted.provider,
|
|
107
|
-
cwd,
|
|
108
|
-
commandModules: [PING_MODULE],
|
|
109
|
-
});
|
|
110
|
-
await driver.start();
|
|
111
|
-
|
|
112
|
-
await driver.send('/ping');
|
|
113
|
-
expect(commandResults(driver.events)).toEqual([{ name: 'ping', output: 'PONG' }]);
|
|
114
|
-
});
|
|
115
|
-
|
|
116
|
-
it('drives the ask seam: queueUserAction confirms a self-asking command; empty queue cancels it', async () => {
|
|
117
|
-
const scripted = createScriptedProvider([{ text: 'unused' }, { text: 'unused' }]);
|
|
118
|
-
driver = createProgrammaticAgent({
|
|
119
|
-
provider: scripted.provider,
|
|
120
|
-
cwd,
|
|
121
|
-
commandModules: [CONFIRM_MODULE],
|
|
122
|
-
});
|
|
123
|
-
await driver.start();
|
|
124
|
-
|
|
125
|
-
// Confirmed → the command runs (askUser resolved from the pre-supplied queue).
|
|
126
|
-
driver.queueUserAction({ type: 'answer', values: ['yes'] });
|
|
127
|
-
await driver.send('/danger');
|
|
128
|
-
const ranAfterConfirm = commandResults(driver.events).filter(
|
|
129
|
-
(c) => c.output === 'DANGER_RAN',
|
|
130
|
-
).length;
|
|
131
|
-
expect(ranAfterConfirm).toBe(1);
|
|
132
|
-
|
|
133
|
-
// Empty queue → askUser resolves `cancelled` → the command takes its cancel path (does NOT run).
|
|
134
|
-
await driver.send('/danger');
|
|
135
|
-
expect(commandResults(driver.events).filter((c) => c.output === 'DANGER_RAN').length).toBe(
|
|
136
|
-
ranAfterConfirm,
|
|
137
|
-
);
|
|
138
|
-
expect(commandResults(driver.events).some((c) => c.output === 'DANGER_CANCELLED')).toBe(true);
|
|
139
|
-
});
|
|
140
|
-
});
|
|
@@ -1,53 +0,0 @@
|
|
|
1
|
-
import { mkdtempSync } from 'node:fs';
|
|
2
|
-
import { tmpdir } from 'node:os';
|
|
3
|
-
import path from 'node:path';
|
|
4
|
-
|
|
5
|
-
import { describe, expect, it, vi } from 'vitest';
|
|
6
|
-
|
|
7
|
-
import { TransportRegistry } from '../transport-registry';
|
|
8
|
-
|
|
9
|
-
import type { IInteractiveSession } from '@robota-sdk/agent-interface-transport';
|
|
10
|
-
import type { IConfigurableTransport } from '@robota-sdk/agent-interface-transport';
|
|
11
|
-
|
|
12
|
-
function makeTransport(
|
|
13
|
-
name: string,
|
|
14
|
-
stop: () => Promise<void>,
|
|
15
|
-
): IConfigurableTransport<IInteractiveSession> {
|
|
16
|
-
return {
|
|
17
|
-
name,
|
|
18
|
-
defaultEnabled: true,
|
|
19
|
-
attach: vi.fn(),
|
|
20
|
-
start: vi.fn().mockResolvedValue(undefined),
|
|
21
|
-
stop,
|
|
22
|
-
} as unknown as IConfigurableTransport<IInteractiveSession>;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
describe('TransportRegistry.stopAll (best-effort, CORE-013)', () => {
|
|
26
|
-
it('stops every transport even when one stop fails, and collects the error', async () => {
|
|
27
|
-
const registry = new TransportRegistry(
|
|
28
|
-
path.join(mkdtempSync(path.join(tmpdir(), 'registry-')), 'settings.json'),
|
|
29
|
-
);
|
|
30
|
-
const stoppedSecond = vi.fn().mockResolvedValue(undefined);
|
|
31
|
-
registry.register(
|
|
32
|
-
makeTransport('failing', async () => {
|
|
33
|
-
throw new Error('stop boom');
|
|
34
|
-
}),
|
|
35
|
-
);
|
|
36
|
-
registry.register(makeTransport('healthy', stoppedSecond));
|
|
37
|
-
|
|
38
|
-
const result = await registry.stopAll();
|
|
39
|
-
|
|
40
|
-
expect(stoppedSecond).toHaveBeenCalledTimes(1);
|
|
41
|
-
expect(result.errors).toHaveLength(1);
|
|
42
|
-
expect(result.errors[0].message).toBe('stop boom');
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
it('returns an empty error list when all stops succeed', async () => {
|
|
46
|
-
const registry = new TransportRegistry(
|
|
47
|
-
path.join(mkdtempSync(path.join(tmpdir(), 'registry-')), 'settings.json'),
|
|
48
|
-
);
|
|
49
|
-
registry.register(makeTransport('a', vi.fn().mockResolvedValue(undefined)));
|
|
50
|
-
|
|
51
|
-
await expect(registry.stopAll()).resolves.toEqual({ errors: [] });
|
|
52
|
-
});
|
|
53
|
-
});
|
|
@@ -1,129 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* HeadlessInteractionChannel — owns session lifecycle for non-interactive (print) mode.
|
|
3
|
-
*
|
|
4
|
-
* Mirrors TuiInteractionChannel's ownership pattern: session creation lives here,
|
|
5
|
-
* not in the caller. print-mode.ts constructs this and calls run().
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import { execSync } from 'node:child_process';
|
|
9
|
-
|
|
10
|
-
import { InteractiveSession } from '@robota-sdk/agent-framework';
|
|
11
|
-
|
|
12
|
-
import { createHeadlessRunner, type TOutputFormat } from './headless-runner.js';
|
|
13
|
-
|
|
14
|
-
import type { IAIProvider, TPermissionMode } from '@robota-sdk/agent-core';
|
|
15
|
-
import type {
|
|
16
|
-
IBackgroundTaskRunner,
|
|
17
|
-
ICommandHostAdapters,
|
|
18
|
-
ICommandModule,
|
|
19
|
-
TSubagentRunnerFactory,
|
|
20
|
-
TShellExecFn,
|
|
21
|
-
} from '@robota-sdk/agent-framework';
|
|
22
|
-
import type { IInteractiveSessionStore } from '@robota-sdk/agent-interface-transport';
|
|
23
|
-
|
|
24
|
-
export interface IHeadlessInteractionChannelOptions {
|
|
25
|
-
cwd: string;
|
|
26
|
-
provider: IAIProvider;
|
|
27
|
-
outputFormat: TOutputFormat;
|
|
28
|
-
permissionMode?: TPermissionMode;
|
|
29
|
-
maxTurns?: number;
|
|
30
|
-
sessionStore?: IInteractiveSessionStore;
|
|
31
|
-
/** Continue/resume an existing session by id (print-mode parity with TUI). */
|
|
32
|
-
resumeSessionId?: string;
|
|
33
|
-
/** Fork the resumed session into a new independent session instead of appending. */
|
|
34
|
-
forkSession?: boolean;
|
|
35
|
-
sessionName?: string;
|
|
36
|
-
bare?: boolean;
|
|
37
|
-
allowedTools?: string[];
|
|
38
|
-
deniedTools?: string[];
|
|
39
|
-
appendSystemPrompt?: string;
|
|
40
|
-
systemPrompt?: string;
|
|
41
|
-
/** Name reported to the underlying agent config (resolved by the CLI, e.g. preset agentName). */
|
|
42
|
-
agentName?: string;
|
|
43
|
-
/** Active preset id selected at startup (PRESET-011 runtime state). Defaults to 'default'. */
|
|
44
|
-
activePresetId?: string;
|
|
45
|
-
/** Preset persona block composed as a `source: 'persona'` system-prompt section (priority 5). */
|
|
46
|
-
persona?: string;
|
|
47
|
-
/** Preset execution capability: activate agent runtime + subagent/background dispatch. */
|
|
48
|
-
enableParallelSubagents?: boolean;
|
|
49
|
-
/** Preset execution capability: run a post-task self-verification step. */
|
|
50
|
-
selfVerification?: boolean;
|
|
51
|
-
backgroundTaskRunners?: IBackgroundTaskRunner[];
|
|
52
|
-
subagentRunnerFactory?: TSubagentRunnerFactory;
|
|
53
|
-
commandModules?: readonly ICommandModule[];
|
|
54
|
-
commandHostAdapters?: ICommandHostAdapters;
|
|
55
|
-
shellExec?: TShellExecFn;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
export class HeadlessInteractionChannel {
|
|
59
|
-
private readonly opts: IHeadlessInteractionChannelOptions;
|
|
60
|
-
private exitCode = 0;
|
|
61
|
-
|
|
62
|
-
constructor(options: IHeadlessInteractionChannelOptions) {
|
|
63
|
-
this.opts = options;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
async run(prompt: string): Promise<void> {
|
|
67
|
-
const session = this.createSession();
|
|
68
|
-
const runner = createHeadlessRunner({ session, outputFormat: this.opts.outputFormat });
|
|
69
|
-
this.exitCode = await runner.run(prompt);
|
|
70
|
-
await session.shutdown({ reason: 'prompt_input_exit', message: 'Headless transport complete' });
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
/**
|
|
74
|
-
* GOAL-001: run an autonomous goal to completion (or a stop condition) in headless mode.
|
|
75
|
-
* Mirrors {@link run} but drives the framework goal loop instead of a single prompt.
|
|
76
|
-
*/
|
|
77
|
-
async runGoal(objective: string, options: { maxIterations?: number } = {}): Promise<void> {
|
|
78
|
-
const session = this.createSession();
|
|
79
|
-
const runner = createHeadlessRunner({ session, outputFormat: this.opts.outputFormat });
|
|
80
|
-
this.exitCode = await runner.runGoal(objective, options);
|
|
81
|
-
await session.shutdown({ reason: 'prompt_input_exit', message: 'Headless goal complete' });
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
private createSession(): InteractiveSession {
|
|
85
|
-
const shellExec: TShellExecFn =
|
|
86
|
-
this.opts.shellExec ??
|
|
87
|
-
((command: string) =>
|
|
88
|
-
execSync(command, { timeout: 5000, encoding: 'utf-8', stdio: 'pipe' }).trimEnd());
|
|
89
|
-
|
|
90
|
-
return new InteractiveSession({
|
|
91
|
-
cwd: this.opts.cwd,
|
|
92
|
-
provider: this.opts.provider,
|
|
93
|
-
permissionMode: this.opts.permissionMode ?? 'bypassPermissions',
|
|
94
|
-
// CMD-004: headless has no interactive renderer, so it injects no askHandler — the session's
|
|
95
|
-
// getUserInteraction() returns undefined and each command applies its explicit no-human path
|
|
96
|
-
// (e.g. /mode reports current, /exit and /clear proceed — never a silent guess).
|
|
97
|
-
maxTurns: this.opts.maxTurns,
|
|
98
|
-
sessionStore: this.opts.sessionStore,
|
|
99
|
-
resumeSessionId: this.opts.resumeSessionId,
|
|
100
|
-
forkSession: this.opts.forkSession,
|
|
101
|
-
sessionName: this.opts.sessionName,
|
|
102
|
-
bare: this.opts.bare || undefined,
|
|
103
|
-
allowedTools: this.opts.allowedTools,
|
|
104
|
-
deniedTools: this.opts.deniedTools,
|
|
105
|
-
appendSystemPrompt: this.opts.appendSystemPrompt,
|
|
106
|
-
...(this.opts.persona !== undefined ? { persona: this.opts.persona } : {}),
|
|
107
|
-
...(this.opts.systemPrompt ? { systemPrompt: this.opts.systemPrompt } : {}),
|
|
108
|
-
backgroundTaskRunners: this.opts.backgroundTaskRunners,
|
|
109
|
-
subagentRunnerFactory: this.opts.subagentRunnerFactory,
|
|
110
|
-
commandModules: this.opts.commandModules,
|
|
111
|
-
commandHostAdapters: this.opts.commandHostAdapters,
|
|
112
|
-
shellExec,
|
|
113
|
-
agentName: this.opts.agentName,
|
|
114
|
-
...(this.opts.activePresetId !== undefined
|
|
115
|
-
? { activePresetId: this.opts.activePresetId }
|
|
116
|
-
: {}),
|
|
117
|
-
...(this.opts.enableParallelSubagents !== undefined
|
|
118
|
-
? { enableParallelSubagents: this.opts.enableParallelSubagents }
|
|
119
|
-
: {}),
|
|
120
|
-
...(this.opts.selfVerification !== undefined
|
|
121
|
-
? { selfVerification: this.opts.selfVerification }
|
|
122
|
-
: {}),
|
|
123
|
-
});
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
getExitCode(): number {
|
|
127
|
-
return this.exitCode;
|
|
128
|
-
}
|
|
129
|
-
}
|
|
@@ -1,106 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
-
import type { IAIProvider } from '@robota-sdk/agent-core';
|
|
3
|
-
|
|
4
|
-
const sessionCtorSpy = vi.fn();
|
|
5
|
-
|
|
6
|
-
vi.mock('@robota-sdk/agent-framework', async (importOriginal) => {
|
|
7
|
-
const mod = await importOriginal<typeof import('@robota-sdk/agent-framework')>();
|
|
8
|
-
|
|
9
|
-
class FakeInteractiveSession {
|
|
10
|
-
private readonly listeners = new Map<string, Array<(...args: unknown[]) => void>>();
|
|
11
|
-
|
|
12
|
-
constructor(options: unknown) {
|
|
13
|
-
sessionCtorSpy(options);
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
on(event: string, handler: (...args: unknown[]) => void): void {
|
|
17
|
-
const list = this.listeners.get(event) ?? [];
|
|
18
|
-
list.push(handler);
|
|
19
|
-
this.listeners.set(event, list);
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
off(): void {}
|
|
23
|
-
|
|
24
|
-
async submit(): Promise<void> {
|
|
25
|
-
const result = {
|
|
26
|
-
response: 'ok',
|
|
27
|
-
history: [],
|
|
28
|
-
toolSummaries: [],
|
|
29
|
-
contextState: {},
|
|
30
|
-
};
|
|
31
|
-
for (const handler of this.listeners.get('complete') ?? []) {
|
|
32
|
-
handler(result);
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
getSession(): { getSessionId: () => string } {
|
|
37
|
-
return { getSessionId: () => 'test-session' };
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
async shutdown(): Promise<void> {}
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
return { ...mod, InteractiveSession: FakeInteractiveSession };
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
import { HeadlessInteractionChannel } from '../HeadlessInteractionChannel.js';
|
|
47
|
-
|
|
48
|
-
describe('HeadlessInteractionChannel session options', () => {
|
|
49
|
-
beforeEach(() => {
|
|
50
|
-
sessionCtorSpy.mockClear();
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
it('TC-01: passes deniedTools through to the InteractiveSession options', async () => {
|
|
54
|
-
const channel = new HeadlessInteractionChannel({
|
|
55
|
-
cwd: process.cwd(),
|
|
56
|
-
provider: {} as IAIProvider,
|
|
57
|
-
outputFormat: 'text',
|
|
58
|
-
allowedTools: ['Read'],
|
|
59
|
-
deniedTools: ['Bash', 'Glob'],
|
|
60
|
-
shellExec: () => '',
|
|
61
|
-
});
|
|
62
|
-
await channel.run('hello');
|
|
63
|
-
expect(sessionCtorSpy).toHaveBeenCalledTimes(1);
|
|
64
|
-
const options = sessionCtorSpy.mock.calls[0]?.[0] as {
|
|
65
|
-
allowedTools?: string[];
|
|
66
|
-
deniedTools?: string[];
|
|
67
|
-
};
|
|
68
|
-
expect(options.allowedTools).toEqual(['Read']);
|
|
69
|
-
expect(options.deniedTools).toEqual(['Bash', 'Glob']);
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
it('TC-01 (CLI-063): passes resumeSessionId and forkSession through to the InteractiveSession options', async () => {
|
|
73
|
-
const channel = new HeadlessInteractionChannel({
|
|
74
|
-
cwd: process.cwd(),
|
|
75
|
-
provider: {} as IAIProvider,
|
|
76
|
-
outputFormat: 'text',
|
|
77
|
-
resumeSessionId: 'session_prior_abc',
|
|
78
|
-
forkSession: true,
|
|
79
|
-
shellExec: () => '',
|
|
80
|
-
});
|
|
81
|
-
await channel.run('hello');
|
|
82
|
-
expect(sessionCtorSpy).toHaveBeenCalledTimes(1);
|
|
83
|
-
const options = sessionCtorSpy.mock.calls[0]?.[0] as {
|
|
84
|
-
resumeSessionId?: string;
|
|
85
|
-
forkSession?: boolean;
|
|
86
|
-
};
|
|
87
|
-
expect(options.resumeSessionId).toBe('session_prior_abc');
|
|
88
|
-
expect(options.forkSession).toBe(true);
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
it('TC-01 (CLI-063): omits resume fields when not provided', async () => {
|
|
92
|
-
const channel = new HeadlessInteractionChannel({
|
|
93
|
-
cwd: process.cwd(),
|
|
94
|
-
provider: {} as IAIProvider,
|
|
95
|
-
outputFormat: 'text',
|
|
96
|
-
shellExec: () => '',
|
|
97
|
-
});
|
|
98
|
-
await channel.run('hello');
|
|
99
|
-
const options = sessionCtorSpy.mock.calls[0]?.[0] as {
|
|
100
|
-
resumeSessionId?: string;
|
|
101
|
-
forkSession?: boolean;
|
|
102
|
-
};
|
|
103
|
-
expect(options.resumeSessionId).toBeUndefined();
|
|
104
|
-
expect(options.forkSession).toBeUndefined();
|
|
105
|
-
});
|
|
106
|
-
});
|
|
@@ -1,108 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* GOAL-001: headless autonomous goal runner. A fake session emits goal lifecycle events; the
|
|
3
|
-
* runner resolves to exit 0 when satisfied, GOAL_NOT_SATISFIED_EXIT_CODE at a bound, and 1 on error.
|
|
4
|
-
*/
|
|
5
|
-
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
6
|
-
|
|
7
|
-
import { createHeadlessRunner, GOAL_NOT_SATISFIED_EXIT_CODE } from '../headless-runner.js';
|
|
8
|
-
|
|
9
|
-
import type {
|
|
10
|
-
IGoalEvent,
|
|
11
|
-
IGoalState,
|
|
12
|
-
IInteractiveSession,
|
|
13
|
-
} from '@robota-sdk/agent-interface-transport';
|
|
14
|
-
|
|
15
|
-
type TListener = (...args: unknown[]) => void;
|
|
16
|
-
|
|
17
|
-
function goalState(overrides: Partial<IGoalState> = {}): IGoalState {
|
|
18
|
-
return {
|
|
19
|
-
id: 'g1',
|
|
20
|
-
objective: 'do it',
|
|
21
|
-
status: 'satisfied',
|
|
22
|
-
iterations: 2,
|
|
23
|
-
maxIterations: 25,
|
|
24
|
-
startedAt: '2026-06-27T00:00:00.000Z',
|
|
25
|
-
progress: [],
|
|
26
|
-
...overrides,
|
|
27
|
-
};
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
/** Minimal fake session whose setGoal drives a scripted sequence of events. */
|
|
31
|
-
function createFakeSession(
|
|
32
|
-
script: (emit: (e: string, ...a: unknown[]) => void) => void,
|
|
33
|
-
): IInteractiveSession {
|
|
34
|
-
const listeners = new Map<string, Set<TListener>>();
|
|
35
|
-
const emit = (event: string, ...args: unknown[]): void => {
|
|
36
|
-
for (const fn of listeners.get(event) ?? []) fn(...args);
|
|
37
|
-
};
|
|
38
|
-
return {
|
|
39
|
-
on: (event: string, handler: TListener) => {
|
|
40
|
-
if (!listeners.has(event)) listeners.set(event, new Set());
|
|
41
|
-
listeners.get(event)!.add(handler);
|
|
42
|
-
},
|
|
43
|
-
off: (event: string, handler: TListener) => listeners.get(event)?.delete(handler),
|
|
44
|
-
setGoal: async () => {
|
|
45
|
-
queueMicrotask(() => script(emit));
|
|
46
|
-
return goalState();
|
|
47
|
-
},
|
|
48
|
-
getSession: () => ({ getSessionId: () => 'sess' }),
|
|
49
|
-
} as unknown as IInteractiveSession;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
const stdoutChunks: string[] = [];
|
|
53
|
-
const stderrChunks: string[] = [];
|
|
54
|
-
|
|
55
|
-
beforeEach(() => {
|
|
56
|
-
stdoutChunks.length = 0;
|
|
57
|
-
stderrChunks.length = 0;
|
|
58
|
-
vi.spyOn(process.stdout, 'write').mockImplementation(((chunk: unknown) => {
|
|
59
|
-
stdoutChunks.push(String(chunk));
|
|
60
|
-
return true;
|
|
61
|
-
}) as never);
|
|
62
|
-
vi.spyOn(process.stderr, 'write').mockImplementation(((chunk: unknown) => {
|
|
63
|
-
stderrChunks.push(String(chunk));
|
|
64
|
-
return true;
|
|
65
|
-
}) as never);
|
|
66
|
-
});
|
|
67
|
-
afterEach(() => {
|
|
68
|
-
vi.restoreAllMocks();
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
describe('headless runGoal', () => {
|
|
72
|
-
it('exits 0 and prints turn responses when the goal is satisfied', async () => {
|
|
73
|
-
const session = createFakeSession((emit) => {
|
|
74
|
-
emit('complete', { response: 'did step 1' });
|
|
75
|
-
emit('goal_event', {
|
|
76
|
-
type: 'goal_started',
|
|
77
|
-
goal: goalState({ status: 'active' }),
|
|
78
|
-
} as IGoalEvent);
|
|
79
|
-
emit('goal_event', {
|
|
80
|
-
type: 'goal_stopped',
|
|
81
|
-
goal: goalState({ stopReason: 'satisfied' }),
|
|
82
|
-
} as IGoalEvent);
|
|
83
|
-
});
|
|
84
|
-
const code = await createHeadlessRunner({ session, outputFormat: 'text' }).runGoal('do it');
|
|
85
|
-
expect(code).toBe(0);
|
|
86
|
-
expect(stdoutChunks).toContain('did step 1\n');
|
|
87
|
-
expect(stdoutChunks.join('')).toContain('Goal satisfied');
|
|
88
|
-
});
|
|
89
|
-
|
|
90
|
-
it('exits GOAL_NOT_SATISFIED_EXIT_CODE when stopped at a bound', async () => {
|
|
91
|
-
const session = createFakeSession((emit) => {
|
|
92
|
-
emit('goal_event', {
|
|
93
|
-
type: 'goal_stopped',
|
|
94
|
-
goal: goalState({ status: 'stopped', stopReason: 'max-iterations' }),
|
|
95
|
-
} as IGoalEvent);
|
|
96
|
-
});
|
|
97
|
-
const code = await createHeadlessRunner({ session, outputFormat: 'text' }).runGoal('do it');
|
|
98
|
-
expect(code).toBe(GOAL_NOT_SATISFIED_EXIT_CODE);
|
|
99
|
-
});
|
|
100
|
-
|
|
101
|
-
it('exits 1 on a turn error', async () => {
|
|
102
|
-
const session = createFakeSession((emit) => {
|
|
103
|
-
emit('error', new Error('provider exploded'));
|
|
104
|
-
});
|
|
105
|
-
const code = await createHeadlessRunner({ session, outputFormat: 'text' }).runGoal('do it');
|
|
106
|
-
expect(code).toBe(1);
|
|
107
|
-
});
|
|
108
|
-
});
|
|
@@ -1,143 +0,0 @@
|
|
|
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
|
-
});
|