specrails-core 6.3.0 → 6.5.0

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 (102) hide show
  1. package/dist/agent-runtime/capabilities.js +22 -5
  2. package/dist/agent-runtime/cli-executor.js +8 -3
  3. package/dist/agent-runtime/cli.d.ts +1 -1
  4. package/dist/agent-runtime/cli.js +50 -3
  5. package/dist/agent-runtime/compact/environment.d.ts +49 -4
  6. package/dist/agent-runtime/compact/environment.js +191 -8
  7. package/dist/agent-runtime/config.js +68 -53
  8. package/dist/agent-runtime/engine/compiler.js +10 -2
  9. package/dist/agent-runtime/engine/contracts.d.ts +16 -0
  10. package/dist/agent-runtime/engine/pieces/control.js +11 -2
  11. package/dist/agent-runtime/engine/pieces/role-turn.d.ts +5 -0
  12. package/dist/agent-runtime/engine/pieces/role-turn.js +69 -3
  13. package/dist/agent-runtime/engine/pieces/verify.d.ts +10 -3
  14. package/dist/agent-runtime/engine/pieces/verify.js +105 -30
  15. package/dist/agent-runtime/executor-types.d.ts +2 -0
  16. package/dist/agent-runtime/graph/nodes.js +9 -55
  17. package/dist/agent-runtime/prompts.d.ts +1 -1
  18. package/dist/agent-runtime/prompts.js +18 -12
  19. package/dist/agent-runtime/session/application/active-session.d.ts +176 -0
  20. package/dist/agent-runtime/session/application/active-session.js +701 -0
  21. package/dist/agent-runtime/session/application/delegation.d.ts +73 -0
  22. package/dist/agent-runtime/session/application/delegation.js +171 -0
  23. package/dist/agent-runtime/session/application/output-buffer.d.ts +43 -0
  24. package/dist/agent-runtime/session/application/output-buffer.js +65 -0
  25. package/dist/agent-runtime/session/application/session-service.d.ts +121 -0
  26. package/dist/agent-runtime/session/application/session-service.js +256 -0
  27. package/dist/agent-runtime/session/application/timers.d.ts +15 -0
  28. package/dist/agent-runtime/session/application/timers.js +35 -0
  29. package/dist/agent-runtime/session/domain/descriptor.d.ts +6 -0
  30. package/dist/agent-runtime/session/domain/descriptor.js +32 -0
  31. package/dist/agent-runtime/session/domain/errors.d.ts +9 -0
  32. package/dist/agent-runtime/session/domain/errors.js +16 -0
  33. package/dist/agent-runtime/session/domain/events.d.ts +147 -0
  34. package/dist/agent-runtime/session/domain/events.js +24 -0
  35. package/dist/agent-runtime/session/domain/fingerprint.d.ts +2 -0
  36. package/dist/agent-runtime/session/domain/fingerprint.js +10 -0
  37. package/dist/agent-runtime/session/domain/interruption.d.ts +13 -0
  38. package/dist/agent-runtime/session/domain/interruption.js +39 -0
  39. package/dist/agent-runtime/session/domain/policy.d.ts +42 -0
  40. package/dist/agent-runtime/session/domain/policy.js +104 -0
  41. package/dist/agent-runtime/session/domain/snapshot.d.ts +100 -0
  42. package/dist/agent-runtime/session/domain/snapshot.js +234 -0
  43. package/dist/agent-runtime/session/domain/state-machines.d.ts +21 -0
  44. package/dist/agent-runtime/session/domain/state-machines.js +41 -0
  45. package/dist/agent-runtime/session/domain/types.d.ts +130 -0
  46. package/dist/agent-runtime/session/domain/types.js +15 -0
  47. package/dist/agent-runtime/session/domain/usage.d.ts +54 -0
  48. package/dist/agent-runtime/session/domain/usage.js +86 -0
  49. package/dist/agent-runtime/session/drivers/claude/argv.d.ts +19 -0
  50. package/dist/agent-runtime/session/drivers/claude/argv.js +67 -0
  51. package/dist/agent-runtime/session/drivers/claude/driver.d.ts +19 -0
  52. package/dist/agent-runtime/session/drivers/claude/driver.js +134 -0
  53. package/dist/agent-runtime/session/drivers/claude/translator.d.ts +40 -0
  54. package/dist/agent-runtime/session/drivers/claude/translator.js +247 -0
  55. package/dist/agent-runtime/session/drivers/codex/argv.d.ts +12 -0
  56. package/dist/agent-runtime/session/drivers/codex/argv.js +76 -0
  57. package/dist/agent-runtime/session/drivers/codex/driver.d.ts +19 -0
  58. package/dist/agent-runtime/session/drivers/codex/driver.js +193 -0
  59. package/dist/agent-runtime/session/drivers/codex/rpc.d.ts +28 -0
  60. package/dist/agent-runtime/session/drivers/codex/rpc.js +81 -0
  61. package/dist/agent-runtime/session/drivers/codex/translator.d.ts +53 -0
  62. package/dist/agent-runtime/session/drivers/codex/translator.js +267 -0
  63. package/dist/agent-runtime/session/drivers/common/process.d.ts +37 -0
  64. package/dist/agent-runtime/session/drivers/common/process.js +116 -0
  65. package/dist/agent-runtime/session/drivers/executor/driver.d.ts +22 -0
  66. package/dist/agent-runtime/session/drivers/executor/driver.js +163 -0
  67. package/dist/agent-runtime/session/drivers/registry.d.ts +32 -0
  68. package/dist/agent-runtime/session/drivers/registry.js +38 -0
  69. package/dist/agent-runtime/session/host/protocol.d.ts +18 -0
  70. package/dist/agent-runtime/session/host/protocol.js +88 -0
  71. package/dist/agent-runtime/session/host/server.d.ts +57 -0
  72. package/dist/agent-runtime/session/host/server.js +229 -0
  73. package/dist/agent-runtime/session/index.d.ts +45 -0
  74. package/dist/agent-runtime/session/index.js +74 -0
  75. package/dist/agent-runtime/session/journal/schema.d.ts +7 -0
  76. package/dist/agent-runtime/session/journal/schema.js +44 -0
  77. package/dist/agent-runtime/session/journal/sqlite-journal.d.ts +70 -0
  78. package/dist/agent-runtime/session/journal/sqlite-journal.js +279 -0
  79. package/dist/agent-runtime/session/ports.d.ts +192 -0
  80. package/dist/agent-runtime/session/ports.js +1 -0
  81. package/dist/agent-runtime/session/testing/driver-conformance.d.ts +21 -0
  82. package/dist/agent-runtime/session/testing/driver-conformance.js +97 -0
  83. package/dist/agent-runtime/session/testing/fake-clock.d.ts +20 -0
  84. package/dist/agent-runtime/session/testing/fake-clock.js +34 -0
  85. package/dist/agent-runtime/session/testing/fixture-replayer.d.ts +41 -0
  86. package/dist/agent-runtime/session/testing/fixture-replayer.js +172 -0
  87. package/dist/agent-runtime/session/testing/index.d.ts +9 -0
  88. package/dist/agent-runtime/session/testing/index.js +9 -0
  89. package/dist/agent-runtime/session/testing/memory-journal.d.ts +27 -0
  90. package/dist/agent-runtime/session/testing/memory-journal.js +70 -0
  91. package/dist/agent-runtime/session/testing/scripted-driver.d.ts +30 -0
  92. package/dist/agent-runtime/session/testing/scripted-driver.js +77 -0
  93. package/dist/agent-runtime/verification-proposals.d.ts +9 -0
  94. package/dist/agent-runtime/verification-proposals.js +69 -0
  95. package/dist/agent-runtime/verification-repair.d.ts +26 -0
  96. package/dist/agent-runtime/verification-repair.js +88 -0
  97. package/dist/installer/util/registry.js +3 -3
  98. package/dist/shared/specrails-home.d.ts +21 -0
  99. package/dist/shared/specrails-home.js +36 -0
  100. package/integration-contract.json +43 -2
  101. package/package.json +11 -1
  102. package/schemas/agent-runtime.schema.json +9 -7
@@ -0,0 +1,267 @@
1
+ const TOOL_ITEMS = new Set(['commandExecution', 'mcpToolCall', 'fileChange', 'dynamicToolCall', 'collabAgentToolCall', 'webSearch']);
2
+ const str = (value) => (typeof value === 'string' ? value : undefined);
3
+ const obj = (value) => (value && typeof value === 'object' && !Array.isArray(value) ? value : {});
4
+ const num = (value) => (typeof value === 'number' && Number.isFinite(value) ? value : null);
5
+ function preview(value, limit = 16_384) {
6
+ const text = typeof value === 'string' ? value : JSON.stringify(value ?? '');
7
+ return text.length > limit ? `${text.slice(0, limit)}…` : text;
8
+ }
9
+ function toolName(item) {
10
+ const type = str(item.type) ?? 'tool';
11
+ if (type === 'mcpToolCall')
12
+ return `${str(item.server) ?? 'mcp'}.${str(item.tool) ?? 'tool'}`;
13
+ if (type === 'collabAgentToolCall')
14
+ return `agent.${str(item.tool) ?? 'call'}`;
15
+ return type;
16
+ }
17
+ function toolFailed(item) {
18
+ return ['failed', 'declined'].includes(str(item.status) ?? '') || item.success === false || (typeof item.exitCode === 'number' && item.exitCode !== 0) || !!item.error;
19
+ }
20
+ /** `/root/run_tests` → `run tests` (0.160 sub-agents are named, not described). */
21
+ function describeAgentPath(agentPath) {
22
+ const name = agentPath?.split('/').filter(Boolean).pop() ?? '';
23
+ return name.replace(/[_-]+/g, ' ').trim().slice(0, 200);
24
+ }
25
+ function tokens(usage) {
26
+ return {
27
+ inputTokens: num(usage.inputTokens),
28
+ outputTokens: num(usage.outputTokens),
29
+ cacheReadTokens: num(usage.cachedInputTokens),
30
+ cacheWriteTokens: num(usage.cacheWriteInputTokens),
31
+ totalTokens: num(usage.totalTokens),
32
+ };
33
+ }
34
+ export class CodexTranslator {
35
+ rootThread = null;
36
+ rootTurn = null;
37
+ /** Inputs whose `turn/start` was acknowledged, waiting for the turn notification. */
38
+ pendingInputs = [];
39
+ rootText = '';
40
+ rootFinal = null;
41
+ streamed = new Map();
42
+ startedTools = new Set();
43
+ completedItems = new Set();
44
+ /** child thread → owner (null = root agent, else the owning child thread). */
45
+ children = new Map();
46
+ childTurn = new Map();
47
+ childPhase = new Map();
48
+ childLastText = new Map();
49
+ rootUsage = null;
50
+ model = null;
51
+ interruptRequested = false;
52
+ setRoot(threadId, model) { this.rootThread = threadId; this.model = model; }
53
+ get activeTurnId() { return this.rootTurn; }
54
+ noteTurnRequested(inputId) { this.pendingInputs.push(inputId); }
55
+ noteInterrupt() { if (this.rootTurn)
56
+ this.interruptRequested = true; }
57
+ /** Child threads with a turn in progress. */
58
+ liveChildren() {
59
+ return [...this.childTurn.entries()].filter((entry) => entry[1] !== null).map(([threadId, turnId]) => ({ threadId, turnId }));
60
+ }
61
+ /** A registered child thread (live or not). */
62
+ isChild(threadId) { return this.children.has(threadId); }
63
+ /** The turn a child is running, if any. */
64
+ childTurnOf(threadId) { return this.childTurn.get(threadId) ?? null; }
65
+ notification(method, params) {
66
+ const threadId = str(params.threadId);
67
+ if (method === 'error') {
68
+ const message = str(obj(params.error).message) ?? 'Codex reported an error';
69
+ return [{ kind: 'diagnostic', level: 'warning', code: params.willRetry === true ? 'provider.retrying' : 'provider.error', message: message.slice(0, 2_000) }];
70
+ }
71
+ if (!threadId)
72
+ return [];
73
+ if (threadId === this.rootThread)
74
+ return this.root(method, params);
75
+ if (this.children.has(threadId))
76
+ return this.child(threadId, method, params);
77
+ return [];
78
+ }
79
+ // ── root thread ───────────────────────────────────────────────────────────
80
+ root(method, params) {
81
+ switch (method) {
82
+ case 'turn/started': {
83
+ const turnId = str(obj(params.turn).id);
84
+ if (!turnId || this.rootTurn === turnId)
85
+ return [];
86
+ this.rootTurn = turnId;
87
+ this.rootText = '';
88
+ this.rootFinal = null;
89
+ const inputIds = this.pendingInputs;
90
+ this.pendingInputs = [];
91
+ return [{ kind: 'turn.started', trigger: inputIds.length > 0 ? 'input' : 'continuation', inputIds }];
92
+ }
93
+ case 'item/agentMessage/delta': {
94
+ const delta = str(params.delta);
95
+ const itemId = str(params.itemId);
96
+ if (!delta || !itemId || this.completedItems.has(itemId))
97
+ return [];
98
+ this.streamed.set(itemId, (this.streamed.get(itemId) ?? '') + delta);
99
+ return [{ kind: 'turn.output', channel: 'text', delta }];
100
+ }
101
+ case 'item/started':
102
+ case 'item/completed':
103
+ return this.item(null, obj(params.item), method === 'item/completed');
104
+ case 'thread/tokenUsage/updated':
105
+ this.rootUsage = obj(obj(params.tokenUsage).total);
106
+ return [];
107
+ case 'turn/completed': {
108
+ const turn = obj(params.turn);
109
+ if (!this.rootTurn || str(turn.id) !== this.rootTurn)
110
+ return [];
111
+ const events = [];
112
+ for (const item of Array.isArray(turn.items) ? turn.items : [])
113
+ events.push(...this.item(null, item, true));
114
+ const status = str(turn.status);
115
+ const interrupted = this.interruptRequested || status === 'interrupted';
116
+ this.interruptRequested = false;
117
+ this.rootTurn = null;
118
+ const error = str(obj(turn.error).message);
119
+ events.push({
120
+ kind: 'turn.completed',
121
+ status: interrupted ? 'stopped' : status === 'failed' ? 'failed' : 'completed',
122
+ text: this.rootFinal ?? this.rootText,
123
+ ...(status === 'failed' && !interrupted ? { error: error ?? 'Codex turn failed' } : {}),
124
+ usage: { ...(this.rootUsage ? tokens(this.rootUsage) : {}), model: this.model },
125
+ });
126
+ return events;
127
+ }
128
+ default:
129
+ return [];
130
+ }
131
+ }
132
+ // ── child threads (sub-agents) ────────────────────────────────────────────
133
+ child(threadId, method, params) {
134
+ switch (method) {
135
+ case 'turn/started': {
136
+ const turnId = str(obj(params.turn).id) ?? null;
137
+ this.childTurn.set(threadId, turnId);
138
+ const events = [];
139
+ if (this.childPhase.get(threadId) !== 'running') {
140
+ // Re-entry (the parent sent it more work): restart under the same id.
141
+ if (this.childPhase.has(threadId))
142
+ events.push({ kind: 'subagent.started', subagentId: threadId, parentId: this.children.get(threadId) ?? null, agentKind: 'background', agentType: 'codex-agent', description: 'Sub-agent' });
143
+ this.childPhase.set(threadId, 'running');
144
+ }
145
+ events.push(this.roster());
146
+ return events;
147
+ }
148
+ case 'item/agentMessage/delta': {
149
+ const delta = str(params.delta);
150
+ const itemId = str(params.itemId);
151
+ if (!delta || !itemId || this.completedItems.has(itemId))
152
+ return [];
153
+ this.streamed.set(itemId, (this.streamed.get(itemId) ?? '') + delta);
154
+ return [{ kind: 'subagent.output', subagentId: threadId, channel: 'text', delta }];
155
+ }
156
+ case 'item/started':
157
+ case 'item/completed':
158
+ return this.item(threadId, obj(params.item), method === 'item/completed');
159
+ case 'thread/tokenUsage/updated':
160
+ return [{ kind: 'subagent.usage', subagentId: threadId, usage: tokens(obj(obj(params.tokenUsage).total)) }];
161
+ case 'turn/completed': {
162
+ const turn = obj(params.turn);
163
+ this.childTurn.set(threadId, null);
164
+ const status = str(turn.status);
165
+ const phase = status === 'failed' ? 'failed' : status === 'interrupted' ? 'stopped' : 'idle';
166
+ const events = [];
167
+ for (const item of Array.isArray(turn.items) ? turn.items : [])
168
+ events.push(...this.item(threadId, item, true));
169
+ const summary = this.childLastText.get(threadId);
170
+ if (phase === 'idle' && summary)
171
+ events.push({ kind: 'subagent.result', subagentId: threadId, summary });
172
+ if (this.childPhase.get(threadId) !== phase) {
173
+ this.childPhase.set(threadId, phase);
174
+ events.push({ kind: 'subagent.phase', subagentId: threadId, phase });
175
+ }
176
+ events.push(this.roster());
177
+ return events;
178
+ }
179
+ default:
180
+ return [];
181
+ }
182
+ }
183
+ // ── items ─────────────────────────────────────────────────────────────────
184
+ item(owner, item, completed) {
185
+ const id = str(item.id);
186
+ const type = str(item.type);
187
+ if (!id || !type || this.completedItems.has(id))
188
+ return [];
189
+ const events = [];
190
+ if (type === 'agentMessage') {
191
+ if (!completed)
192
+ return [];
193
+ const text = str(item.text) ?? '';
194
+ const already = this.streamed.get(id) ?? '';
195
+ const rest = text.startsWith(already) ? text.slice(already.length) : '';
196
+ if (rest)
197
+ events.push(owner === null ? { kind: 'turn.output', channel: 'text', delta: rest } : { kind: 'subagent.output', subagentId: owner, channel: 'text', delta: rest });
198
+ if (owner === null) {
199
+ this.rootText = this.rootText ? `${this.rootText}\n\n${text}` : text;
200
+ if (item.phase === 'final_answer')
201
+ this.rootFinal = text;
202
+ }
203
+ else if (text)
204
+ this.childLastText.set(owner, text);
205
+ }
206
+ else if (type === 'subAgentActivity') {
207
+ // Child turns carry progress, results and phase; only the start registers.
208
+ if (str(item.kind) === 'started')
209
+ events.push(...this.registerChild(owner, str(item.agentThreadId), describeAgentPath(str(item.agentPath))));
210
+ }
211
+ else if (TOOL_ITEMS.has(type)) {
212
+ const name = toolName(item);
213
+ if (!this.startedTools.has(id)) {
214
+ this.startedTools.add(id);
215
+ const tool = { toolUseId: id, name, phase: 'started', input: item.command ?? item.arguments ?? item.changes ?? item.prompt ?? item.query ?? null };
216
+ events.push(owner === null ? { kind: 'turn.tool', ...tool } : { kind: 'subagent.output', subagentId: owner, channel: 'tool', tool });
217
+ }
218
+ if (completed) {
219
+ if (type === 'collabAgentToolCall')
220
+ events.push(...this.collab(owner, item));
221
+ const tool = { toolUseId: id, name, phase: 'completed', output: preview(item.aggregatedOutput ?? item.result ?? item.error ?? item.agentsStates ?? { status: item.status }), ...(toolFailed(item) ? { isError: true } : {}) };
222
+ events.push(owner === null ? { kind: 'turn.tool', ...tool } : { kind: 'subagent.output', subagentId: owner, channel: 'tool', tool });
223
+ }
224
+ }
225
+ if (completed)
226
+ this.completedItems.add(id);
227
+ return events;
228
+ }
229
+ collab(owner, item) {
230
+ const events = [];
231
+ if (str(item.tool) === 'spawnAgent') {
232
+ const prompt = str(item.prompt) ?? '';
233
+ for (const childId of Array.isArray(item.receiverThreadIds) ? item.receiverThreadIds : []) {
234
+ if (typeof childId === 'string')
235
+ events.push(...this.registerChild(owner, childId, (prompt.split('\n')[0] ?? '').slice(0, 200), prompt));
236
+ }
237
+ }
238
+ // `wait` reports the final message of finished children.
239
+ for (const [childId, state] of Object.entries(obj(item.agentsStates))) {
240
+ const message = str(obj(state).message);
241
+ if (this.children.has(childId) && obj(state).status === 'completed' && message && message !== this.childLastText.get(childId)) {
242
+ this.childLastText.set(childId, message);
243
+ events.push({ kind: 'subagent.result', subagentId: childId, summary: message });
244
+ }
245
+ }
246
+ return events;
247
+ }
248
+ registerChild(owner, childId, description, prompt = '') {
249
+ if (!childId || this.children.has(childId))
250
+ return [];
251
+ this.children.set(childId, owner);
252
+ this.childPhase.set(childId, 'running');
253
+ this.childTurn.set(childId, this.childTurn.get(childId) ?? null);
254
+ return [{
255
+ kind: 'subagent.started',
256
+ subagentId: childId,
257
+ parentId: owner,
258
+ agentKind: 'background',
259
+ agentType: 'codex-agent',
260
+ description: description || 'Sub-agent',
261
+ ...(prompt ? { prompt } : {}),
262
+ }];
263
+ }
264
+ roster() {
265
+ return { kind: 'roster', liveSubagentIds: this.liveChildren().map((child) => child.threadId) };
266
+ }
267
+ }
@@ -0,0 +1,37 @@
1
+ export interface ProcessSpec {
2
+ command: string;
3
+ args: string[];
4
+ cwd: string;
5
+ env: NodeJS.ProcessEnv;
6
+ }
7
+ export interface ProcessHandlers {
8
+ /** One complete stdout line (without the trailing newline). */
9
+ onLine(line: string): void;
10
+ /** Bounded stderr tail updates, for diagnostics only. */
11
+ onStderr?(chunk: string): void;
12
+ }
13
+ export interface ProcessExit {
14
+ exitCode: number | null;
15
+ signal: string | null;
16
+ }
17
+ /**
18
+ * A long-lived provider process with line-oriented stdout. Unlike the one-shot
19
+ * `runCliProcess`, nothing accumulates: lines are handed over as they arrive.
20
+ */
21
+ export interface ProviderProcess {
22
+ readonly pid: number | undefined;
23
+ /** Write one line to stdin; false when input is already closed. */
24
+ write(line: string): boolean;
25
+ endInput(): void;
26
+ /**
27
+ * Ask the whole process tree to stop (SIGTERM / taskkill), then force it
28
+ * (SIGKILL / taskkill /F) after `graceMs`. Resolves once the process exited.
29
+ */
30
+ terminate(graceMs: number): Promise<ProcessExit>;
31
+ readonly exited: Promise<ProcessExit>;
32
+ }
33
+ export type ProcessSpawner = (spec: ProcessSpec, handlers: ProcessHandlers) => ProviderProcess;
34
+ /** Longest stdout line accepted; longer lines are dropped and reported. */
35
+ export declare const MAX_LINE_BYTES: number;
36
+ /** Production spawner: cross-spawn, own process group on POSIX, tree-kill on every platform. */
37
+ export declare const spawnResidentProcess: ProcessSpawner;
@@ -0,0 +1,116 @@
1
+ import crossSpawn from 'cross-spawn';
2
+ import { execFile } from 'node:child_process';
3
+ import path from 'node:path';
4
+ import { StringDecoder } from 'node:string_decoder';
5
+ import { cliProcessEnvironment } from '../../../cli-process.js';
6
+ /** Longest stdout line accepted; longer lines are dropped and reported. */
7
+ export const MAX_LINE_BYTES = 16 * 1024 * 1024;
8
+ /** Production spawner: cross-spawn, own process group on POSIX, tree-kill on every platform. */
9
+ export const spawnResidentProcess = (spec, handlers) => {
10
+ const env = cliProcessEnvironment(spec.env);
11
+ const child = crossSpawn(spec.command, spec.args, {
12
+ cwd: spec.cwd,
13
+ env,
14
+ stdio: ['pipe', 'pipe', 'pipe'],
15
+ shell: false,
16
+ windowsHide: true,
17
+ detached: process.platform !== 'win32',
18
+ });
19
+ const decoder = new StringDecoder('utf8');
20
+ let pending = '';
21
+ let inputOpen = true;
22
+ let exit = null;
23
+ const exited = new Promise((resolve) => {
24
+ child.once('error', (error) => {
25
+ handlers.onStderr?.(`spawn error: ${error.message}`);
26
+ if (!exit) {
27
+ exit = { exitCode: null, signal: 'spawn-error' };
28
+ resolve(exit);
29
+ }
30
+ });
31
+ child.once('close', (code, signal) => {
32
+ const rest = pending + decoder.end();
33
+ pending = '';
34
+ if (rest.trim())
35
+ handlers.onLine(rest.replace(/\r$/, ''));
36
+ if (!exit) {
37
+ exit = { exitCode: code, signal };
38
+ resolve(exit);
39
+ }
40
+ });
41
+ });
42
+ child.stdout?.on('data', (chunk) => {
43
+ pending += decoder.write(chunk);
44
+ for (;;) {
45
+ const newline = pending.indexOf('\n');
46
+ if (newline < 0)
47
+ break;
48
+ const line = pending.slice(0, newline).replace(/\r$/, '');
49
+ pending = pending.slice(newline + 1);
50
+ if (line.length > 0)
51
+ handlers.onLine(line);
52
+ }
53
+ if (Buffer.byteLength(pending) > MAX_LINE_BYTES) {
54
+ handlers.onStderr?.(`dropped an stdout line larger than ${MAX_LINE_BYTES} bytes`);
55
+ pending = '';
56
+ }
57
+ });
58
+ child.stderr?.on('data', (chunk) => handlers.onStderr?.(chunk.toString('utf8').slice(-8_192)));
59
+ child.stdin?.on('error', () => { inputOpen = false; });
60
+ const signalTree = (force) => {
61
+ const pid = child.pid;
62
+ if (!pid || exit)
63
+ return;
64
+ if (process.platform === 'win32') {
65
+ const taskkill = path.win32.join(env.SystemRoot ?? 'C:\\Windows', 'System32', 'taskkill.exe');
66
+ execFile(taskkill, ['/PID', String(pid), '/T', ...(force ? ['/F'] : [])], { windowsHide: true, env, timeout: 10_000 }, () => {
67
+ if (force) {
68
+ try {
69
+ child.kill('SIGKILL');
70
+ }
71
+ catch { /* already exited */ }
72
+ }
73
+ });
74
+ return;
75
+ }
76
+ try {
77
+ process.kill(-pid, force ? 'SIGKILL' : 'SIGTERM');
78
+ }
79
+ catch {
80
+ try {
81
+ child.kill(force ? 'SIGKILL' : 'SIGTERM');
82
+ }
83
+ catch { /* already exited */ }
84
+ }
85
+ };
86
+ let termination = null;
87
+ return {
88
+ get pid() { return child.pid; },
89
+ exited,
90
+ write(line) {
91
+ if (!inputOpen || exit || !child.stdin || child.stdin.destroyed)
92
+ return false;
93
+ child.stdin.write(`${line}\n`, 'utf8');
94
+ return true;
95
+ },
96
+ endInput() {
97
+ if (!inputOpen)
98
+ return;
99
+ inputOpen = false;
100
+ child.stdin?.end();
101
+ },
102
+ terminate(graceMs) {
103
+ if (exit)
104
+ return Promise.resolve(exit);
105
+ termination ??= (async () => {
106
+ signalTree(false);
107
+ let timer;
108
+ const forced = new Promise((resolve) => { timer = setTimeout(() => { signalTree(true); resolve(); }, Math.max(0, graceMs)); });
109
+ const result = await Promise.race([exited, forced.then(() => exited)]);
110
+ clearTimeout(timer);
111
+ return result;
112
+ })();
113
+ return termination;
114
+ },
115
+ };
116
+ };
@@ -0,0 +1,22 @@
1
+ import { type AgentExecutor } from '../../../executor-types.js';
2
+ import type { DriverDescriptor } from '../../domain/types.js';
3
+ import type { DriverEventSink, DriverFactory, DriverOpenSpec, DriverSession } from '../../ports.js';
4
+ /**
5
+ * Adapter from the batch `AgentExecutor` contract (one request, one result) to
6
+ * the session driver contract. Each input runs as one executor invocation that
7
+ * resumes the provider session when the executor supports continuation.
8
+ * Declares exactly what it cannot do; the application adapts to it.
9
+ */
10
+ export declare function executorDescriptor(id: string, displayName: string): DriverDescriptor;
11
+ export interface ExecutorDriverOptions {
12
+ /** Per-invocation limits forwarded to the executor. */
13
+ timeoutMs?: number;
14
+ idleTimeoutMs?: number;
15
+ }
16
+ export declare class ExecutorDriverFactory implements DriverFactory {
17
+ readonly descriptor: DriverDescriptor;
18
+ private readonly executor;
19
+ private readonly options;
20
+ constructor(descriptor: DriverDescriptor, executor: AgentExecutor, options?: ExecutorDriverOptions);
21
+ open(spec: DriverOpenSpec, sink: DriverEventSink): Promise<DriverSession>;
22
+ }
@@ -0,0 +1,163 @@
1
+ import { AgentExecutionError } from '../../../executor-types.js';
2
+ /**
3
+ * Adapter from the batch `AgentExecutor` contract (one request, one result) to
4
+ * the session driver contract. Each input runs as one executor invocation that
5
+ * resumes the provider session when the executor supports continuation.
6
+ * Declares exactly what it cannot do; the application adapts to it.
7
+ */
8
+ export function executorDescriptor(id, displayName) {
9
+ return Object.freeze({
10
+ id,
11
+ displayName,
12
+ capabilities: Object.freeze({
13
+ resident: false,
14
+ nativeInputQueue: false,
15
+ subagents: 'unsupported',
16
+ subagentDisable: false,
17
+ subagentModel: false,
18
+ subagentEffort: false,
19
+ autonomousContinuation: false,
20
+ steer: false,
21
+ toolFiltering: false,
22
+ usage: Object.freeze({ costUsd: 'per-turn', tokens: 'per-turn' }),
23
+ }),
24
+ });
25
+ }
26
+ function reported(usage) {
27
+ return {
28
+ inputTokens: usage?.inputTokens ?? null,
29
+ outputTokens: usage?.outputTokens ?? null,
30
+ cacheReadTokens: usage?.cacheReadInputTokens ?? null,
31
+ cacheWriteTokens: usage?.cacheWriteInputTokens ?? null,
32
+ costUsd: usage?.costUsd ?? null,
33
+ };
34
+ }
35
+ class ExecutorDriverSession {
36
+ executor;
37
+ spec;
38
+ sink;
39
+ options;
40
+ running = null;
41
+ closed = false;
42
+ sessionRef;
43
+ toolSeq = 0;
44
+ firstTurn;
45
+ constructor(executor, spec, sink, options) {
46
+ this.executor = executor;
47
+ this.spec = spec;
48
+ this.sink = sink;
49
+ this.options = options;
50
+ this.sessionRef = spec.providerSessionRef;
51
+ this.firstTurn = spec.providerSessionRef === null;
52
+ }
53
+ async send(input) {
54
+ if (this.closed)
55
+ throw new Error('Session driver is closed');
56
+ if (this.running)
57
+ throw new Error('This provider runs one turn at a time');
58
+ const controller = new AbortController();
59
+ this.running = controller;
60
+ this.sink({ kind: 'input.receipt', inputId: input.inputId, state: 'started' });
61
+ this.sink({ kind: 'turn.started', trigger: 'input', inputIds: [input.inputId] });
62
+ // Run in the background: receipts and output arrive as events, like resident drivers.
63
+ void this.run(input, controller);
64
+ }
65
+ async run(input, controller) {
66
+ const openTools = [];
67
+ let text = '';
68
+ const files = (input.attachments ?? []).map((item) => `- ${item.path}`);
69
+ const prompt = [
70
+ this.firstTurn && this.spec.systemPrompt ? `${this.spec.systemPrompt}\n\n` : '',
71
+ input.text,
72
+ files.length ? `\n\nAttached files:\n${files.join('\n')}` : '',
73
+ ].join('');
74
+ try {
75
+ const result = await this.executor.execute({
76
+ role: 'session',
77
+ access: this.spec.policy.permissions === 'read-only' ? 'read' : 'write',
78
+ artifacts: 'none',
79
+ instructions: 'none',
80
+ prompt,
81
+ cwd: this.spec.cwd,
82
+ allowedRoots: [this.spec.cwd],
83
+ model: this.spec.model,
84
+ ...(this.spec.effort ? { effort: this.spec.effort } : {}),
85
+ ...(this.sessionRef ? { resumeSessionId: this.sessionRef } : {}),
86
+ ...(this.options.timeoutMs !== undefined ? { timeoutMs: this.options.timeoutMs } : {}),
87
+ ...(this.options.idleTimeoutMs !== undefined ? { idleTimeoutMs: this.options.idleTimeoutMs } : {}),
88
+ signal: controller.signal,
89
+ onEvent: (event) => {
90
+ if (this.closed)
91
+ return;
92
+ if (event.kind === 'session' && event.sessionId && event.sessionId !== this.sessionRef) {
93
+ this.sessionRef = event.sessionId;
94
+ this.sink({ kind: 'provider.ref', providerSessionRef: event.sessionId });
95
+ }
96
+ else if (event.kind === 'text' && event.text) {
97
+ text += event.text;
98
+ this.sink({ kind: 'turn.output', channel: 'text', delta: event.text });
99
+ }
100
+ else if (event.kind === 'tool-start') {
101
+ const id = `tool-${++this.toolSeq}`;
102
+ openTools.push(id);
103
+ this.sink({ kind: 'turn.tool', toolUseId: id, name: event.tool ?? 'tool', phase: 'started', ...(event.detail ? { input: event.detail } : {}) });
104
+ }
105
+ else if (event.kind === 'tool-end') {
106
+ const id = openTools.shift();
107
+ if (id)
108
+ this.sink({ kind: 'turn.tool', toolUseId: id, name: event.tool ?? 'tool', phase: 'completed', ...(event.detail ? { output: event.detail } : {}) });
109
+ }
110
+ },
111
+ });
112
+ this.firstTurn = false;
113
+ if (result.sessionId && result.sessionId !== this.sessionRef) {
114
+ this.sessionRef = result.sessionId;
115
+ this.sink({ kind: 'provider.ref', providerSessionRef: result.sessionId });
116
+ }
117
+ if (result.text && !text)
118
+ this.sink({ kind: 'turn.output', channel: 'text', delta: result.text });
119
+ this.sink({ kind: 'turn.completed', status: 'completed', text: result.text, usage: reported(result.usage) });
120
+ }
121
+ catch (error) {
122
+ if (this.closed)
123
+ return;
124
+ const failure = error instanceof AgentExecutionError ? error : null;
125
+ const stopped = failure?.code === 'aborted';
126
+ this.sink({
127
+ kind: 'turn.completed',
128
+ status: stopped ? 'stopped' : 'failed',
129
+ text,
130
+ ...(stopped ? {} : { error: error.message }),
131
+ usage: reported(failure?.usage),
132
+ });
133
+ }
134
+ finally {
135
+ if (this.running === controller)
136
+ this.running = null;
137
+ }
138
+ }
139
+ async interrupt() {
140
+ this.running?.abort();
141
+ }
142
+ async stopSubagents() {
143
+ return [];
144
+ }
145
+ async close(_reason) {
146
+ this.closed = true;
147
+ this.running?.abort();
148
+ }
149
+ }
150
+ export class ExecutorDriverFactory {
151
+ descriptor;
152
+ executor;
153
+ options;
154
+ constructor(descriptor, executor, options = {}) {
155
+ this.descriptor = descriptor;
156
+ this.executor = executor;
157
+ this.options = options;
158
+ }
159
+ async open(spec, sink) {
160
+ sink({ kind: 'process.started' });
161
+ return new ExecutorDriverSession(this.executor, spec, sink, this.options);
162
+ }
163
+ }
@@ -0,0 +1,32 @@
1
+ import type { AgentExecutor } from '../../executor-types.js';
2
+ import type { DriverDescriptor } from '../domain/types.js';
3
+ import type { DriverCatalog, DriverFactory } from '../ports.js';
4
+ import { type ClaudeDriverOptions } from './claude/driver.js';
5
+ import { type CodexDriverOptions } from './codex/driver.js';
6
+ import { type ExecutorDriverOptions } from './executor/driver.js';
7
+ /**
8
+ * Closed, frozen registry of session drivers (like the engine PieceRegistry).
9
+ * Adding a provider is a reviewed change: a drivers/<id>/ folder, an entry
10
+ * here, and a passing conformance run. Duplicate ids and invalid descriptors
11
+ * are rejected at construction.
12
+ */
13
+ export declare class DriverRegistry implements DriverCatalog {
14
+ private readonly factories;
15
+ constructor(factories: readonly DriverFactory[]);
16
+ get(driverId: string): DriverFactory | undefined;
17
+ descriptors(): DriverDescriptor[];
18
+ }
19
+ export interface DriverRegistryOptions {
20
+ claude?: ClaudeDriverOptions | false;
21
+ codex?: CodexDriverOptions | false;
22
+ /** Non-resident providers backed by existing batch executors (Gemini, Kimi, OpenAI-compatible connections). */
23
+ executors?: Array<{
24
+ id: string;
25
+ displayName: string;
26
+ executor: AgentExecutor;
27
+ options?: ExecutorDriverOptions;
28
+ }>;
29
+ /** Extra factories (tests, embedding hosts). */
30
+ extra?: DriverFactory[];
31
+ }
32
+ export declare function createDriverRegistry(options?: DriverRegistryOptions): DriverRegistry;
@@ -0,0 +1,38 @@
1
+ import { validateDescriptor } from '../domain/descriptor.js';
2
+ import { ClaudeDriverFactory } from './claude/driver.js';
3
+ import { CodexDriverFactory } from './codex/driver.js';
4
+ import { ExecutorDriverFactory, executorDescriptor } from './executor/driver.js';
5
+ /**
6
+ * Closed, frozen registry of session drivers (like the engine PieceRegistry).
7
+ * Adding a provider is a reviewed change: a drivers/<id>/ folder, an entry
8
+ * here, and a passing conformance run. Duplicate ids and invalid descriptors
9
+ * are rejected at construction.
10
+ */
11
+ export class DriverRegistry {
12
+ factories;
13
+ constructor(factories) {
14
+ const map = new Map();
15
+ for (const factory of factories) {
16
+ validateDescriptor(factory.descriptor);
17
+ if (map.has(factory.descriptor.id))
18
+ throw new Error(`Duplicate session driver "${factory.descriptor.id}"`);
19
+ map.set(factory.descriptor.id, factory);
20
+ }
21
+ this.factories = map;
22
+ Object.freeze(this);
23
+ }
24
+ get(driverId) {
25
+ return this.factories.get(driverId);
26
+ }
27
+ descriptors() {
28
+ return [...this.factories.values()].map((factory) => factory.descriptor);
29
+ }
30
+ }
31
+ export function createDriverRegistry(options = {}) {
32
+ return new DriverRegistry([
33
+ ...(options.claude === false ? [] : [new ClaudeDriverFactory(options.claude ?? {})]),
34
+ ...(options.codex === false ? [] : [new CodexDriverFactory(options.codex ?? {})]),
35
+ ...(options.executors ?? []).map((entry) => new ExecutorDriverFactory(executorDescriptor(entry.id, entry.displayName), entry.executor, entry.options ?? {})),
36
+ ...(options.extra ?? []),
37
+ ]);
38
+ }