klyro 1.0.0 → 1.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent/anthropic-adapter.d.ts +49 -5
- package/dist/agent/anthropic-adapter.js +86 -17
- package/dist/agent/capabilities.d.ts +23 -0
- package/dist/agent/capabilities.js +53 -6
- package/dist/agent/child-worker.d.ts +104 -0
- package/dist/agent/child-worker.js +250 -0
- package/dist/agent/orchestrator.d.ts +124 -6
- package/dist/agent/orchestrator.js +425 -58
- package/dist/agent/provider-adapter.d.ts +8 -0
- package/dist/agent/provider-adapter.js +12 -3
- package/dist/agent/retry.d.ts +1 -1
- package/dist/agent/retry.js +54 -12
- package/dist/agent/runtime.d.ts +84 -8
- package/dist/agent/runtime.js +352 -39
- package/dist/agent/stream-budget.d.ts +36 -0
- package/dist/agent/stream-budget.js +121 -0
- package/dist/agent/worktree-manager.d.ts +74 -0
- package/dist/agent/worktree-manager.js +189 -0
- package/dist/checkpoints/store.d.ts +9 -0
- package/dist/checkpoints/store.js +56 -5
- package/dist/cli/auth.js +16 -1
- package/dist/cli/commit.d.ts +31 -0
- package/dist/cli/commit.js +142 -0
- package/dist/cli/config.d.ts +54 -3
- package/dist/cli/config.js +146 -3
- package/dist/cli/doctor.d.ts +1 -0
- package/dist/cli/doctor.js +71 -6
- package/dist/cli/eval.d.ts +6 -1
- package/dist/cli/eval.js +9 -0
- package/dist/cli/hooks.d.ts +47 -0
- package/dist/cli/hooks.js +181 -0
- package/dist/cli/repl.js +196 -29
- package/dist/cli/run.d.ts +13 -11
- package/dist/cli/run.js +144 -20
- package/dist/cli/update.d.ts +5 -0
- package/dist/cli/update.js +62 -10
- package/dist/context/import-graph.d.ts +2 -0
- package/dist/context/import-graph.js +31 -3
- package/dist/context/klyro-md.js +4 -1
- package/dist/context/memory.d.ts +8 -0
- package/dist/context/memory.js +50 -2
- package/dist/context/project-map.d.ts +6 -0
- package/dist/context/project-map.js +50 -2
- package/dist/context/repo-map.d.ts +2 -0
- package/dist/context/repo-map.js +31 -1
- package/dist/events/catalog.d.ts +37 -0
- package/dist/events/catalog.js +9 -0
- package/dist/index.js +177 -8
- package/dist/mcp/client.d.ts +6 -4
- package/dist/mcp/client.js +83 -14
- package/dist/mcp/config.d.ts +10 -0
- package/dist/mcp/config.js +18 -1
- package/dist/mcp/registry.d.ts +23 -19
- package/dist/mcp/registry.js +127 -8
- package/dist/mcp/schema.d.ts +11 -4
- package/dist/mcp/schema.js +27 -16
- package/dist/mcp/trust.d.ts +20 -0
- package/dist/mcp/trust.js +74 -0
- package/dist/persistence/audit.d.ts +28 -0
- package/dist/persistence/audit.js +101 -1
- package/dist/persistence/store.d.ts +26 -2
- package/dist/persistence/store.js +140 -13
- package/dist/policy/approval.d.ts +14 -0
- package/dist/policy/approval.js +44 -2
- package/dist/policy/engine.d.ts +17 -0
- package/dist/policy/engine.js +162 -9
- package/dist/policy/path-guard.d.ts +24 -0
- package/dist/policy/path-guard.js +46 -0
- package/dist/policy/secret-redactor.js +4 -0
- package/dist/providers/model-info.d.ts +23 -0
- package/dist/providers/model-info.js +43 -2
- package/dist/repl.d.ts +6 -0
- package/dist/repl.js +12 -7
- package/dist/tools/agent/spawn-agent.js +5 -5
- package/dist/tools/agent/task-apply.d.ts +4 -0
- package/dist/tools/agent/task-apply.js +44 -0
- package/dist/tools/agent/task-stop.d.ts +6 -0
- package/dist/tools/agent/task-stop.js +39 -0
- package/dist/tools/agent/task-wait.d.ts +17 -0
- package/dist/tools/agent/task-wait.js +79 -0
- package/dist/tools/fs/apply-patch.js +77 -1
- package/dist/tools/fs/edit-file.js +69 -1
- package/dist/tools/fs/multi-edit.d.ts +4 -0
- package/dist/tools/fs/multi-edit.js +70 -1
- package/dist/tools/fs/write-file.js +83 -6
- package/dist/tools/plan/todo-write.js +1 -1
- package/dist/tools/registry.js +6 -0
- package/dist/tools/shell/background.js +6 -3
- package/dist/tools/shell/sandbox.d.ts +51 -0
- package/dist/tools/shell/sandbox.js +143 -0
- package/dist/tools/shell/shell-exec.d.ts +29 -0
- package/dist/tools/shell/shell-exec.js +170 -12
- package/dist/tools/shell/worker-entry.d.ts +12 -0
- package/dist/tools/shell/worker-entry.js +43 -0
- package/dist/tools/types.d.ts +6 -0
- package/dist/tools/verify/run-verify.js +3 -1
- package/dist/trace/writer.d.ts +20 -0
- package/dist/trace/writer.js +62 -4
- package/dist/tui/app.js +1 -1
- package/dist/tui/approval.js +20 -21
- package/dist/util.d.ts +1 -0
- package/dist/util.js +1 -0
- package/dist/verification/baseline.js +17 -3
- package/dist/verification/classify.js +27 -15
- package/dist/verification/engine.d.ts +8 -0
- package/dist/verification/engine.js +28 -1
- package/dist/verification/registry.d.ts +2 -0
- package/dist/verification/registry.js +44 -0
- package/dist/verification/scoped.js +64 -11
- package/package.json +1 -1
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* G2 — Process isolation for sub-agents (V1: headless/CLI children).
|
|
3
|
+
*
|
|
4
|
+
* This module is the *child-process* end of spawn_agent. The orchestrator
|
|
5
|
+
* forks this file with `node` (never shares a process), passing a
|
|
6
|
+
* JSON-serializable payload on STDIN (not argv — the system prompt + memory
|
|
7
|
+
* can exceed argv/command-line length limits, which would fail the spawn with
|
|
8
|
+
* E2BIG). The child rebuilds a minimal but self-sufficient `RuntimeDeps`,
|
|
9
|
+
* runs the task through `run()`, emits ONE JSON `ChildResult` line on stdout,
|
|
10
|
+
* and exits 0/1.
|
|
11
|
+
*
|
|
12
|
+
* Isolation contract: a child that OOMs, segfaults, or enters an infinite
|
|
13
|
+
* loop fails ONLY this process — the parent harness survives and reports
|
|
14
|
+
* `CHILD_CRASH` exactly as it does for an in-process throw.
|
|
15
|
+
*
|
|
16
|
+
* V1 scope (documented honestly):
|
|
17
|
+
* - Headless children only. TUI children stay in-process because the Ink
|
|
18
|
+
* approval bridge is inherently tied to the parent terminal.
|
|
19
|
+
* - The child runs a system-prompt STRING (already assembled by the parent,
|
|
20
|
+
* so KLYRO.md + memory + trust decisions match) with a file-system tool
|
|
21
|
+
* subset and DenyAll approval — no MCP server forwarding, no live prompt.
|
|
22
|
+
* - Grandchild spawning is not forwarded: a process-isolated child rebuilds
|
|
23
|
+
* its own agent bridge-less deps, so its tools see NO_ORCHESTRATOR (same
|
|
24
|
+
* as a pre-bridge child). V2 may thread a task handle back via a socket.
|
|
25
|
+
*
|
|
26
|
+
* Secrets never cross the process boundary via argv/env leaks: the child
|
|
27
|
+
* reads its provider key/baseURL from the SAME env the parent CLI uses
|
|
28
|
+
* (KLYRO_API_KEY, KLYRO_BASE_URL), so credentials stay in-process/env,
|
|
29
|
+
* never in the payload.
|
|
30
|
+
*/
|
|
31
|
+
import * as path from 'node:path';
|
|
32
|
+
import { spawn } from 'node:child_process';
|
|
33
|
+
import { fileURLToPath } from 'node:url';
|
|
34
|
+
import { existsSync } from 'node:fs';
|
|
35
|
+
import { run } from './runtime.js';
|
|
36
|
+
/**
|
|
37
|
+
* Differently-phrased crash, distinct from an in-process throw the parent
|
|
38
|
+
* would catch. The child either exited non-zero without a parseable result
|
|
39
|
+
* line, or produced a malformed result. Carries the raw stdout/stderr tail.
|
|
40
|
+
*/
|
|
41
|
+
export class ChildCrashError extends Error {
|
|
42
|
+
likelyCause;
|
|
43
|
+
stderrTail;
|
|
44
|
+
constructor(message, likelyCause, stderrTail) {
|
|
45
|
+
super(message);
|
|
46
|
+
this.likelyCause = likelyCause;
|
|
47
|
+
this.stderrTail = stderrTail;
|
|
48
|
+
this.name = 'ChildCrashError';
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Resolve the compiled child-worker entry that *this* module's neighboring
|
|
53
|
+
* build (src or dist) produced. Both dev (tsx, .ts) and packaged (dist, .js)
|
|
54
|
+
* layouts live beside this file, so we probe for whichever exists.
|
|
55
|
+
*/
|
|
56
|
+
export function workerEntryPath() {
|
|
57
|
+
const here = fileURLToPath(import.meta.url);
|
|
58
|
+
const dir = path.dirname(here);
|
|
59
|
+
for (const ext of ['js', 'ts']) {
|
|
60
|
+
const candidate = path.join(dir, `child-worker.${ext}`);
|
|
61
|
+
if (existsSync(candidate))
|
|
62
|
+
return candidate;
|
|
63
|
+
}
|
|
64
|
+
// Fall back to the compiled name — the load path both layouts converge on.
|
|
65
|
+
return path.join(dir, 'child-worker.js');
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Fork the child-worker as a real OS process and await its single JSON
|
|
69
|
+
* ChildResult line. Secrets never reach argv: the payload rides stdin, the
|
|
70
|
+
* provider key/baseURL come from the inherited env. Non-zero exit / missing
|
|
71
|
+
* line → rejects with ChildCrashError so the orchestrator maps it to
|
|
72
|
+
* CHILD_CRASH exactly as an in-process throw does.
|
|
73
|
+
*/
|
|
74
|
+
export async function forkChild(entry, payload, opts = {}) {
|
|
75
|
+
const isTs = entry.endsWith('.ts');
|
|
76
|
+
// tsx dev: run under the tsx loader; prod dist: plain node.
|
|
77
|
+
const args = isTs ? ['--import', 'tsx/esm', entry] : [entry];
|
|
78
|
+
const child = spawn(process.execPath, args, {
|
|
79
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
80
|
+
env: process.env,
|
|
81
|
+
windowsHide: true,
|
|
82
|
+
shell: false,
|
|
83
|
+
});
|
|
84
|
+
let stdout = '';
|
|
85
|
+
let stderr = '';
|
|
86
|
+
child.stdout.setEncoding('utf8');
|
|
87
|
+
child.stderr.setEncoding('utf8');
|
|
88
|
+
child.stdout.on('data', (c) => { stdout += c; });
|
|
89
|
+
child.stderr.on('data', (c) => { opts.onStderr?.(c); stderr += c; });
|
|
90
|
+
// Write the payload to stdin, then signal EOF so the child knows it has
|
|
91
|
+
// the whole payload before it starts running.
|
|
92
|
+
child.stdin.on('error', () => { });
|
|
93
|
+
child.stdin.write(JSON.stringify(payload));
|
|
94
|
+
child.stdin.end();
|
|
95
|
+
const timeout = opts.timeoutMs ?? 10 * 60_000;
|
|
96
|
+
let killedForTimeout = false;
|
|
97
|
+
const timer = setTimeout(() => {
|
|
98
|
+
killedForTimeout = true;
|
|
99
|
+
try {
|
|
100
|
+
child.kill();
|
|
101
|
+
}
|
|
102
|
+
catch { /* already gone */ }
|
|
103
|
+
}, timeout);
|
|
104
|
+
const abortListener = () => { try {
|
|
105
|
+
child.kill();
|
|
106
|
+
}
|
|
107
|
+
catch { /* already gone */ } };
|
|
108
|
+
if (opts.signal?.aborted)
|
|
109
|
+
abortListener();
|
|
110
|
+
opts.signal?.addEventListener('abort', abortListener, { once: true });
|
|
111
|
+
return new Promise((resolve, reject) => {
|
|
112
|
+
const cleanup = () => {
|
|
113
|
+
clearTimeout(timer);
|
|
114
|
+
opts.signal?.removeEventListener('abort', abortListener);
|
|
115
|
+
};
|
|
116
|
+
child.on('error', (err) => {
|
|
117
|
+
cleanup();
|
|
118
|
+
reject(new ChildCrashError(`child spawn failed: ${err.message}`, 'exit'));
|
|
119
|
+
});
|
|
120
|
+
child.on('close', (code, signal) => {
|
|
121
|
+
cleanup();
|
|
122
|
+
const first = stdout.split('\n').find((l) => l.trim().length > 0);
|
|
123
|
+
if (code === 0 && first) {
|
|
124
|
+
try {
|
|
125
|
+
resolve(JSON.parse(first));
|
|
126
|
+
}
|
|
127
|
+
catch {
|
|
128
|
+
reject(new ChildCrashError('child exited 0 but emitted malformed ChildResult', 'exit', stderr.slice(-1000)));
|
|
129
|
+
}
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
const cause = signal === 'SIGKILL' || signal === 'SIGTERM'
|
|
133
|
+
? (killedForTimeout ? 'timeout' : 'oom')
|
|
134
|
+
: 'exit';
|
|
135
|
+
reject(new ChildCrashError(`child exited ${code ?? signal ?? '??'} without a ChildResult` + (stderr ? `: ${stderr.slice(-500)}` : ''), cause, stderr.slice(-1000)));
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Rebuild a minimal RuntimeDeps from the payload + inherited env. Centralized
|
|
141
|
+
* so tests can call it directly. Errors on missing provider config.
|
|
142
|
+
*/
|
|
143
|
+
export async function buildChildDeps(payload) {
|
|
144
|
+
const provider = process.env.KLYRO_PROVIDER ?? 'openai';
|
|
145
|
+
const baseUrl = process.env.KLYRO_BASE_URL;
|
|
146
|
+
const apiKey = process.env.KLYRO_API_KEY;
|
|
147
|
+
if (!apiKey)
|
|
148
|
+
throw new Error('KLYRO_API_KEY is not set in the child environment');
|
|
149
|
+
let adapter;
|
|
150
|
+
if (provider === 'anthropic') {
|
|
151
|
+
const { anthropicAdapter } = await import('./anthropic-adapter.js');
|
|
152
|
+
adapter = anthropicAdapter({ baseURL: baseUrl, apiKey, timeoutMs: 240_000 });
|
|
153
|
+
}
|
|
154
|
+
else {
|
|
155
|
+
if (!baseUrl) {
|
|
156
|
+
throw new Error(`KLYRO_BASE_URL is not set for provider "${provider}" in the child environment`);
|
|
157
|
+
}
|
|
158
|
+
const { httpChatAdapter } = await import('./provider-adapter.js');
|
|
159
|
+
adapter = httpChatAdapter({ baseURL: baseUrl, apiKey, timeoutMs: 240_000 });
|
|
160
|
+
}
|
|
161
|
+
const { builtinRegistry } = await import('../tools/registry.js');
|
|
162
|
+
const { builtinRules, clonePolicyConfig, PolicyEngine } = await import('../policy/engine.js');
|
|
163
|
+
const { DenyAllApprovalPrompt } = await import('../policy/approval.js');
|
|
164
|
+
// File-system-only tool subset is the policy node's concern, handled by the
|
|
165
|
+
// parent's scoping; the child just needs a working registry. DenyAll keeps
|
|
166
|
+
// any tool that genuinely needs a user-prompt from stalling the child.
|
|
167
|
+
const deps = {
|
|
168
|
+
adapter,
|
|
169
|
+
registry: builtinRegistry(),
|
|
170
|
+
policy: new PolicyEngine(builtinRules(), clonePolicyConfig()),
|
|
171
|
+
approval: new DenyAllApprovalPrompt(),
|
|
172
|
+
systemPrompt: () => payload.systemPrompt,
|
|
173
|
+
};
|
|
174
|
+
const options = {
|
|
175
|
+
task: payload.task,
|
|
176
|
+
cwd: path.resolve(payload.cwd),
|
|
177
|
+
model: payload.model,
|
|
178
|
+
maxSteps: payload.maxSteps,
|
|
179
|
+
maxCost: payload.maxCost,
|
|
180
|
+
maxTimeMs: payload.maxTimeMs,
|
|
181
|
+
...(payload.maxTokens !== undefined ? { maxTokens: payload.maxTokens } : {}),
|
|
182
|
+
nonInteractive: true,
|
|
183
|
+
};
|
|
184
|
+
return { deps, options };
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* Read the JSON payload. Payloads arrive on stdin (the parent writes the JSON
|
|
188
|
+
* then ends stdin), never argv — large system prompts would otherwise fail
|
|
189
|
+
* the spawn with E2BIG on both Windows (32K cmdline) and Linux (MAX_ARG_STRLEN).
|
|
190
|
+
* Callers who must use argv (tests) can route through `runChildFromString`.
|
|
191
|
+
*/
|
|
192
|
+
export function readPayloadFromStdin() {
|
|
193
|
+
return new Promise((resolve, reject) => {
|
|
194
|
+
const chunks = [];
|
|
195
|
+
process.stdin.on('data', (c) => chunks.push(c));
|
|
196
|
+
process.stdin.on('end', () => resolve(Buffer.concat(chunks).toString('utf8')));
|
|
197
|
+
process.stdin.on('error', reject);
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* Main entry. Reads payload from stdin, runs, prints ONE JSON ChildResult
|
|
202
|
+
* line, exits 0 on complete, 1 on failure/crash (so the parent knows to mark
|
|
203
|
+
* CHILD_CRASH). Intended to be the sole behavior of this file when invoked as
|
|
204
|
+
* a real process.
|
|
205
|
+
*/
|
|
206
|
+
export async function main() {
|
|
207
|
+
let raw;
|
|
208
|
+
try {
|
|
209
|
+
raw = await readPayloadFromStdin();
|
|
210
|
+
}
|
|
211
|
+
catch (e) {
|
|
212
|
+
process.stderr.write(`child-worker: failed reading payload: ${e}\n`);
|
|
213
|
+
process.exit(2);
|
|
214
|
+
}
|
|
215
|
+
if (!raw) {
|
|
216
|
+
process.stderr.write('child-worker: missing payload on stdin\n');
|
|
217
|
+
process.exit(2);
|
|
218
|
+
}
|
|
219
|
+
let payload;
|
|
220
|
+
try {
|
|
221
|
+
payload = JSON.parse(raw);
|
|
222
|
+
}
|
|
223
|
+
catch (e) {
|
|
224
|
+
process.stderr.write(`child-worker: invalid payload: ${e}\n`);
|
|
225
|
+
process.exit(2);
|
|
226
|
+
}
|
|
227
|
+
try {
|
|
228
|
+
const { deps, options } = await buildChildDeps(payload);
|
|
229
|
+
const result = await run(options, deps);
|
|
230
|
+
const out = {
|
|
231
|
+
status: result.status,
|
|
232
|
+
steps: result.steps,
|
|
233
|
+
toolCalls: result.toolCalls,
|
|
234
|
+
finalText: result.finalText,
|
|
235
|
+
hasEdits: result.hasEdits,
|
|
236
|
+
usage: result.usage,
|
|
237
|
+
};
|
|
238
|
+
process.stdout.write(JSON.stringify(out));
|
|
239
|
+
process.exit(result.status === 'complete' ? 0 : 1);
|
|
240
|
+
}
|
|
241
|
+
catch (err) {
|
|
242
|
+
process.stderr.write(`child-worker: ${err instanceof Error ? err.stack ?? err.message : String(err)}\n`);
|
|
243
|
+
process.exit(1);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
// Run directly only when executed as a child process (not imported by tests).
|
|
247
|
+
// Match both bare "child-worker" and the compiled "child-worker.js".
|
|
248
|
+
if (process.argv[1] && /child-worker(?:\.js)?$/.test(process.argv[1])) {
|
|
249
|
+
void main();
|
|
250
|
+
}
|
|
@@ -12,11 +12,15 @@
|
|
|
12
12
|
* The compact result is a `ChildSummary` — a `ToolResult` the parent model
|
|
13
13
|
* can act on — never the full child transcript.
|
|
14
14
|
*/
|
|
15
|
-
import type { RuntimeDeps } from './runtime.js';
|
|
15
|
+
import type { RuntimeDeps, RuntimeEvent } from './runtime.js';
|
|
16
16
|
import type { ToolResult } from '../tools/types.js';
|
|
17
|
-
import { TaskManager, type TaskStatus, type TaskSummary } from './task-manager.js';
|
|
17
|
+
import { TaskManager, type TaskRecord, type TaskStatus, type TaskSummary } from './task-manager.js';
|
|
18
18
|
import { WorkerSpawner } from './worker-spawner.js';
|
|
19
19
|
import { type DropReason } from './capabilities.js';
|
|
20
|
+
/** Concurrency budgets enforced in `spawnAgent` (CONCURRENCY_LIMIT on exceed). */
|
|
21
|
+
export declare const MAX_CONCURRENT_TASKS = 4;
|
|
22
|
+
export declare const MAX_TASKS_PER_PARENT = 8;
|
|
23
|
+
export declare const MAX_TOTAL_TASKS_PER_SESSION = 32;
|
|
20
24
|
/** An agent a model can delegate to via `spawn_agent`. */
|
|
21
25
|
export interface AgentDefinition {
|
|
22
26
|
id: string;
|
|
@@ -32,6 +36,13 @@ export interface AgentDefinition {
|
|
|
32
36
|
maxSteps?: number;
|
|
33
37
|
maxCost?: number;
|
|
34
38
|
maxTimeMs?: number;
|
|
39
|
+
/** Token budget forwarded to the child's run options. */
|
|
40
|
+
maxTokens?: number;
|
|
41
|
+
/**
|
|
42
|
+
* Filesystem allow-list for this agent; intersected with the parent's at
|
|
43
|
+
* spawn time (`undefined` = no additional constraint).
|
|
44
|
+
*/
|
|
45
|
+
allowedPaths?: string[];
|
|
35
46
|
}
|
|
36
47
|
/** Default agents a model can delegate to. */
|
|
37
48
|
export declare const BUILTIN_AGENTS: readonly AgentDefinition[];
|
|
@@ -68,6 +79,21 @@ export interface ParentContextRef {
|
|
|
68
79
|
maxDepth: number;
|
|
69
80
|
allowedTools: ReadonlySet<string> | null;
|
|
70
81
|
model?: string;
|
|
82
|
+
/**
|
|
83
|
+
* Effective filesystem allow-list threaded through to the child (sibling C
|
|
84
|
+
* enforces it in fs tools). `undefined` = unconstrained.
|
|
85
|
+
*/
|
|
86
|
+
allowedPaths?: string[];
|
|
87
|
+
}
|
|
88
|
+
/** One awaited task entry returned by `waitForTasks`. */
|
|
89
|
+
export interface WaitedTask {
|
|
90
|
+
taskId: string;
|
|
91
|
+
status: TaskStatus;
|
|
92
|
+
summary?: ChildSummary;
|
|
93
|
+
error?: {
|
|
94
|
+
code: string;
|
|
95
|
+
message: string;
|
|
96
|
+
};
|
|
71
97
|
}
|
|
72
98
|
/** Interface exposed to the runtime/tools for a child spawn request. */
|
|
73
99
|
export interface AgentSpawnBridge {
|
|
@@ -91,6 +117,32 @@ export interface AgentSpawnBridge {
|
|
|
91
117
|
message: string;
|
|
92
118
|
};
|
|
93
119
|
} | undefined;
|
|
120
|
+
/**
|
|
121
|
+
* Return completed-then-undrained child summaries. Each completed task is
|
|
122
|
+
* drained exactly once — subsequent calls omit it. Optional so older
|
|
123
|
+
* (sibling A) bridge fakes still satisfy the type.
|
|
124
|
+
*/
|
|
125
|
+
drainCompletions?(): ChildSummary[];
|
|
126
|
+
/**
|
|
127
|
+
* Await each task's `record.done` up to `timeoutMs` (per task). Entries
|
|
128
|
+
* that do not settle in time are returned with status `'running'` — the
|
|
129
|
+
* underlying task is NOT cancelled. Never throws for unknown ids: they
|
|
130
|
+
* come back as `{ status: 'failed', error: NOT_FOUND }`.
|
|
131
|
+
*/
|
|
132
|
+
waitForTasks?(taskIds: string[], timeoutMs?: number): Promise<{
|
|
133
|
+
tasks: WaitedTask[];
|
|
134
|
+
}>;
|
|
135
|
+
/** Signal abort for a live task (in-process workers; no subprocesses yet — no process tree to kill). Idempotent on terminal tasks. */
|
|
136
|
+
cancelTask?(taskId: string): ToolResult<{
|
|
137
|
+
taskId: string;
|
|
138
|
+
status: TaskStatus;
|
|
139
|
+
}>;
|
|
140
|
+
/**
|
|
141
|
+
* Merge a succeeded task's worktree branch into the parent tree (or
|
|
142
|
+
* acknowledge a shared-cwd task) and emit `subtask.merged`. Errors with
|
|
143
|
+
* NOT_FOUND / NOT_READY / MERGE_CONFLICT.
|
|
144
|
+
*/
|
|
145
|
+
applyTask?(taskId: string): Promise<ToolResult<ChildSummary>>;
|
|
94
146
|
}
|
|
95
147
|
/** Constructor options for the orchestrator — the parent runtime's deps. */
|
|
96
148
|
export interface OrchestratorOpts {
|
|
@@ -98,12 +150,40 @@ export interface OrchestratorOpts {
|
|
|
98
150
|
deps: RuntimeDeps;
|
|
99
151
|
taskManager?: TaskManager;
|
|
100
152
|
workerSpawner?: WorkerSpawner;
|
|
153
|
+
/**
|
|
154
|
+
* True when the parent is the interactive TUI. TUI children stay in-process
|
|
155
|
+
* (V1 limitation — the Ink approval bridge is tied to the parent terminal),
|
|
156
|
+
* while headless/CLI children run process-isolated. Defaults to false.
|
|
157
|
+
*/
|
|
158
|
+
isTui?: boolean;
|
|
101
159
|
}
|
|
160
|
+
/**
|
|
161
|
+
* Build a `subtask.progress` note for one finished tool call.
|
|
162
|
+
* Pure — unit-tested directly (see agent-tools.test.ts).
|
|
163
|
+
*/
|
|
164
|
+
export declare function progressNote(step: number, tool: string, isError: boolean): string;
|
|
165
|
+
/**
|
|
166
|
+
* Build the `RunOptions.onEvent` handler the orchestrator passes into each
|
|
167
|
+
* child's run options. Emits at most one `subtask.progress` per tool call:
|
|
168
|
+
* a `tool_result` is only mirrored when its `tool_call_end` was observed
|
|
169
|
+
* first, so duplicate/late results can never double-emit. (The note needs
|
|
170
|
+
* the ok/ERR outcome, which only `tool_result` carries — `tool_call_end`
|
|
171
|
+
* alone cannot build it — hence the end-gated result throttle.)
|
|
172
|
+
*/
|
|
173
|
+
export declare function createSubtaskProgressEmitter(opts: {
|
|
174
|
+
taskId: string;
|
|
175
|
+
sessionId: string;
|
|
176
|
+
}): (ev: RuntimeEvent) => void;
|
|
102
177
|
export declare class AgentOrchestrator {
|
|
103
178
|
readonly sessionId: string;
|
|
104
179
|
readonly deps: RuntimeDeps;
|
|
105
180
|
readonly taskManager: TaskManager;
|
|
106
181
|
readonly workerSpawner: WorkerSpawner;
|
|
182
|
+
readonly isTui: boolean;
|
|
183
|
+
/** Per-task spawn metadata: capability drops + worktree placement. */
|
|
184
|
+
private readonly taskMeta;
|
|
185
|
+
/** Finished summaries not yet drained via `drainCompletions`. */
|
|
186
|
+
private readonly undrained;
|
|
107
187
|
constructor(opts: OrchestratorOpts);
|
|
108
188
|
listAgents(): AgentDefinition[];
|
|
109
189
|
getAgent(id: string): AgentDefinition | undefined;
|
|
@@ -112,9 +192,10 @@ export declare class AgentOrchestrator {
|
|
|
112
192
|
/** Compute a child's effective capabilities from the parent's own. */
|
|
113
193
|
private resolveChild;
|
|
114
194
|
/**
|
|
115
|
-
* Spawn a child agent
|
|
116
|
-
*
|
|
117
|
-
*
|
|
195
|
+
* Spawn a child agent asynchronously: start the child worker and return
|
|
196
|
+
* IMMEDIATELY with `{ status: 'running' }`. Completion/failure bus emits
|
|
197
|
+
* and `taskManager.finish` happen in the worker closure; the parent
|
|
198
|
+
* observes them via `task_wait` / `task_get` / `drainCompletions`.
|
|
118
199
|
*/
|
|
119
200
|
spawnAgent(input: {
|
|
120
201
|
agent: string;
|
|
@@ -123,7 +204,44 @@ export declare class AgentOrchestrator {
|
|
|
123
204
|
model?: string;
|
|
124
205
|
timeoutMs?: number;
|
|
125
206
|
}, parent: ParentContextRef): Promise<ToolResult<ChildSummary>>;
|
|
126
|
-
|
|
207
|
+
/**
|
|
208
|
+
* Settle a child in the worker closure: finish the task (idempotent —
|
|
209
|
+
* a TaskManager timeout/cancel that fired first wins), emit the terminal
|
|
210
|
+
* bus event, stash the summary for `drainCompletions`, and best-effort
|
|
211
|
+
* remove the worktree unless the child succeeded (merge happens later in
|
|
212
|
+
* `task_apply`).
|
|
213
|
+
*/
|
|
214
|
+
private settleChild;
|
|
215
|
+
/** Build and stash the finished summary for `drainCompletions`. */
|
|
216
|
+
private stashSummary;
|
|
217
|
+
/** Completed-then-undrained child summaries; each drained exactly once. */
|
|
218
|
+
drainCompletions(): ChildSummary[];
|
|
219
|
+
/**
|
|
220
|
+
* Await each task's `done` up to `timeoutMs` per task. Unsettled entries
|
|
221
|
+
* come back with status `'running'` and a running summary — the underlying
|
|
222
|
+
* task is NOT cancelled.
|
|
223
|
+
*/
|
|
224
|
+
waitForTasks(taskIds: string[], timeoutMs?: number): Promise<{
|
|
225
|
+
tasks: WaitedTask[];
|
|
226
|
+
}>;
|
|
227
|
+
private awaitDone;
|
|
228
|
+
/** Signal abort for a live task via `taskManager.cancel` (in-process workers; no subprocesses yet). Idempotent on terminal tasks. */
|
|
229
|
+
cancelTask(taskId: string): ToolResult<{
|
|
230
|
+
taskId: string;
|
|
231
|
+
status: TaskStatus;
|
|
232
|
+
}>;
|
|
233
|
+
/**
|
|
234
|
+
* Apply a succeeded task: merge its worktree branch into the parent tree
|
|
235
|
+
* (or acknowledge a shared-cwd task whose edits already landed) and emit
|
|
236
|
+
* `subtask.merged`. Non-succeeded tasks error with NOT_READY; merge
|
|
237
|
+
* conflicts error with MERGE_CONFLICT (merge already aborted, branch kept).
|
|
238
|
+
*/
|
|
239
|
+
applyTask(taskId: string): Promise<ToolResult<ChildSummary>>;
|
|
240
|
+
/** Compact summary for a task record (also used by task_wait). */
|
|
241
|
+
toChildSummary(r: TaskRecord, def: AgentDefinition, dropped: {
|
|
242
|
+
tool: string;
|
|
243
|
+
reason: DropReason;
|
|
244
|
+
}[]): ChildSummary;
|
|
127
245
|
}
|
|
128
246
|
/** Module-scoped holder the orchestrator sets so children inherit the parent's signal. */
|
|
129
247
|
export declare const parentAbortSignalRef: {
|