@akon-labs/nexus-cli 0.1.7 → 0.1.8
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/CHANGELOG.md +6 -0
- package/dist/cli.js +1192 -529
- package/dist/extensions/gitnexus/gateway.d.ts +2 -0
- package/dist/extensions/gitnexus/gateway.d.ts.map +1 -1
- package/dist/extensions/gitnexus/gateway.js +27 -0
- package/dist/extensions/gitnexus/gateway.js.map +1 -1
- package/dist/extensions/gitnexus/index.d.ts.map +1 -1
- package/dist/extensions/gitnexus/index.js +16 -1
- package/dist/extensions/gitnexus/index.js.map +1 -1
- package/dist/extensions/index.d.ts.map +1 -1
- package/dist/extensions/index.js +2 -0
- package/dist/extensions/index.js.map +1 -1
- package/dist/extensions/subagent/index.d.ts +110 -0
- package/dist/extensions/subagent/index.d.ts.map +1 -0
- package/dist/extensions/subagent/index.js +568 -0
- package/dist/extensions/subagent/index.js.map +1 -0
- package/dist/index.js +1236 -573
- package/dist/rpc-entry.js +1192 -529
- package/dist/vendor/ai.js +110 -15
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
|
@@ -0,0 +1,568 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Internal delegated-worker runtime.
|
|
3
|
+
*
|
|
4
|
+
* This file deliberately does not register a user-facing tool by default. The
|
|
5
|
+
* authenticated Hub policy must advertise delegation before the GitNexus
|
|
6
|
+
* extension calls registerDelegateTaskTool(). The CLI owns process isolation
|
|
7
|
+
* and streaming UX; the Hub owns one-time worker authorization and routing.
|
|
8
|
+
*/
|
|
9
|
+
import { execFile, spawn } from "node:child_process";
|
|
10
|
+
import { randomUUID } from "node:crypto";
|
|
11
|
+
import * as fs from "node:fs";
|
|
12
|
+
import * as os from "node:os";
|
|
13
|
+
import * as path from "node:path";
|
|
14
|
+
import { Text } from "@earendil-works/pi-tui";
|
|
15
|
+
import { Type } from "typebox";
|
|
16
|
+
import { AUTO_MODEL_ID, AUTO_PROVIDER_ID, isAutoModel } from "../../core/auto-model.js";
|
|
17
|
+
import { withFileMutationQueue } from "../../core/tools/index.js";
|
|
18
|
+
import { resolveGitNexusConfig } from "../gitnexus/config.js";
|
|
19
|
+
import { hubApiBase } from "../gitnexus/device-auth.js";
|
|
20
|
+
export const DELEGATE_TASK_TOOL_NAME = "delegate_task";
|
|
21
|
+
export const INTERNAL_WORKER_MARKER_ENV = "NEXUS_INTERNAL_DELEGATED_WORKER";
|
|
22
|
+
export const INTERNAL_WORKER_SESSION_ENV = "NEXUS_INTERNAL_WORKER_SESSION_ID";
|
|
23
|
+
export const INTERNAL_WORKER_TASK_ENV = "NEXUS_INTERNAL_WORKER_TASK_ID";
|
|
24
|
+
export const INTERNAL_WORKER_RUN_ENV = "NEXUS_INTERNAL_WORKER_RUN_ID";
|
|
25
|
+
export const INTERNAL_WORKER_ROOT_REQUEST_ENV = "NEXUS_INTERNAL_WORKER_ROOT_REQUEST_ID";
|
|
26
|
+
export const INTERNAL_WORKER_PARENT_REQUEST_ENV = "NEXUS_INTERNAL_WORKER_PARENT_REQUEST_ID";
|
|
27
|
+
export const INTERNAL_WORKER_GRANT_HEADER = "x-nexus-worker-grant";
|
|
28
|
+
export const INTERNAL_WORKER_TASK_HEADER = "x-nexus-worker-task-id";
|
|
29
|
+
export const SESSION_AFFINITY_HEADER = "x-session-affinity";
|
|
30
|
+
export const RUN_ID_HEADER = "x-nexus-run-id";
|
|
31
|
+
export const REQUEST_ID_HEADER = "x-nexus-request-id";
|
|
32
|
+
export const ROOT_REQUEST_ID_HEADER = "x-nexus-root-request-id";
|
|
33
|
+
export const PARENT_REQUEST_ID_HEADER = "x-nexus-parent-request-id";
|
|
34
|
+
export const DELEGATION_ID_HEADER = "x-nexus-delegation-id";
|
|
35
|
+
export const REQUEST_SCOPE_HEADER = "x-nexus-request-scope";
|
|
36
|
+
export const MAX_DELEGATED_TURNS = 12;
|
|
37
|
+
export const MAX_DELEGATIONS_PER_RUN = 3;
|
|
38
|
+
export const DELEGATED_WORKER_TIMEOUT_MS = 5 * 60 * 1000;
|
|
39
|
+
export const DELEGATED_OUTPUT_CAP_BYTES = 32 * 1024;
|
|
40
|
+
export const DELEGATED_EVENT_CAP_BYTES = 1024 * 1024;
|
|
41
|
+
export const MAX_DELEGATED_TASK_BYTES = 32 * 1024;
|
|
42
|
+
const WORKER_GRANT_TIMEOUT_MS = 10_000;
|
|
43
|
+
const INVALID_WORKER_GRANT = "unavailable";
|
|
44
|
+
const MAX_ROOT_REVIEW_TOOL_CALLS = 8;
|
|
45
|
+
const MAX_PARALLEL_MUTATING_WORKERS = 1;
|
|
46
|
+
const ROOT_REVIEW_TOOLS = new Set([
|
|
47
|
+
"read",
|
|
48
|
+
"grep",
|
|
49
|
+
"find",
|
|
50
|
+
"ls",
|
|
51
|
+
"gitnexus_query",
|
|
52
|
+
"gitnexus_context",
|
|
53
|
+
"gitnexus_impact",
|
|
54
|
+
"gitnexus_detect_changes",
|
|
55
|
+
"gitnexus_cypher",
|
|
56
|
+
"gitnexus_list_repos",
|
|
57
|
+
"web_search",
|
|
58
|
+
"web_fetch",
|
|
59
|
+
]);
|
|
60
|
+
export const DELEGATED_WORKER_TOOLS = [
|
|
61
|
+
"read",
|
|
62
|
+
"grep",
|
|
63
|
+
"find",
|
|
64
|
+
"ls",
|
|
65
|
+
"bash",
|
|
66
|
+
"write",
|
|
67
|
+
"edit",
|
|
68
|
+
"gitnexus_query",
|
|
69
|
+
"gitnexus_context",
|
|
70
|
+
"gitnexus_impact",
|
|
71
|
+
"gitnexus_detect_changes",
|
|
72
|
+
"gitnexus_cypher",
|
|
73
|
+
"gitnexus_list_repos",
|
|
74
|
+
];
|
|
75
|
+
const WORKER_SYSTEM_PROMPT = `You are an internal implementation worker for Nexus Agent.
|
|
76
|
+
|
|
77
|
+
Complete only the narrowly delegated task. Inspect the repository before editing, follow its local instructions, and verify the changed behavior. Do not delegate work or broaden scope. Do not discuss model names, providers, routing, credits, or internal architecture.
|
|
78
|
+
|
|
79
|
+
Finish with a concise report containing:
|
|
80
|
+
- changes made
|
|
81
|
+
- verification run and results
|
|
82
|
+
- blockers or remaining risks`;
|
|
83
|
+
function isRecord(value) {
|
|
84
|
+
return typeof value === "object" && value !== null;
|
|
85
|
+
}
|
|
86
|
+
function parseAssistantSnapshot(value) {
|
|
87
|
+
if (!isRecord(value) || value.role !== "assistant" || !Array.isArray(value.content))
|
|
88
|
+
return undefined;
|
|
89
|
+
const text = value.content
|
|
90
|
+
.filter(isRecord)
|
|
91
|
+
.filter((part) => part.type === "text" && typeof part.text === "string")
|
|
92
|
+
.map((part) => part.text)
|
|
93
|
+
.join("\n");
|
|
94
|
+
return {
|
|
95
|
+
text,
|
|
96
|
+
...(typeof value.stopReason === "string" ? { stopReason: value.stopReason } : {}),
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
export function composeDelegatedTask(task, acceptanceCriteria, relevantPaths) {
|
|
100
|
+
const sections = [`Task:\n${task.trim()}`];
|
|
101
|
+
if (acceptanceCriteria.length > 0) {
|
|
102
|
+
sections.push(`Acceptance criteria:\n${acceptanceCriteria.map((criterion) => `- ${criterion}`).join("\n")}`);
|
|
103
|
+
}
|
|
104
|
+
if (relevantPaths.length > 0) {
|
|
105
|
+
sections.push(`Relevant paths:\n${relevantPaths.map((filePath) => `- ${filePath}`).join("\n")}`);
|
|
106
|
+
}
|
|
107
|
+
return sections.join("\n\n");
|
|
108
|
+
}
|
|
109
|
+
export function buildDelegatedWorkerArgs(task, promptPath) {
|
|
110
|
+
return [
|
|
111
|
+
"--mode",
|
|
112
|
+
"json",
|
|
113
|
+
"-p",
|
|
114
|
+
"--no-session",
|
|
115
|
+
"--model",
|
|
116
|
+
`${AUTO_PROVIDER_ID}/${AUTO_MODEL_ID}`,
|
|
117
|
+
"--tools",
|
|
118
|
+
DELEGATED_WORKER_TOOLS.join(","),
|
|
119
|
+
"--exclude-tools",
|
|
120
|
+
`${DELEGATE_TASK_TOOL_NAME},subagent`,
|
|
121
|
+
"--append-system-prompt",
|
|
122
|
+
promptPath,
|
|
123
|
+
task,
|
|
124
|
+
];
|
|
125
|
+
}
|
|
126
|
+
function getNexusInvocation(args) {
|
|
127
|
+
const currentScript = process.argv[1];
|
|
128
|
+
const isBunVirtualScript = currentScript?.startsWith("/$bunfs/root/");
|
|
129
|
+
if (currentScript && !isBunVirtualScript && fs.existsSync(currentScript)) {
|
|
130
|
+
return { command: process.execPath, args: [currentScript, ...args] };
|
|
131
|
+
}
|
|
132
|
+
const execName = path.basename(process.execPath).toLowerCase();
|
|
133
|
+
const isGenericRuntime = /^(node|bun)(\.exe)?$/.test(execName);
|
|
134
|
+
return isGenericRuntime ? { command: "nexus", args } : { command: process.execPath, args };
|
|
135
|
+
}
|
|
136
|
+
async function writeWorkerPrompt() {
|
|
137
|
+
const dir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "nexus-worker-"));
|
|
138
|
+
const filePath = path.join(dir, "system.md");
|
|
139
|
+
await withFileMutationQueue(filePath, async () => {
|
|
140
|
+
await fs.promises.writeFile(filePath, WORKER_SYSTEM_PROMPT, { encoding: "utf8", mode: 0o600 });
|
|
141
|
+
});
|
|
142
|
+
return { dir, filePath };
|
|
143
|
+
}
|
|
144
|
+
function truncateUtf8(value, cap) {
|
|
145
|
+
if (Buffer.byteLength(value, "utf8") <= cap)
|
|
146
|
+
return { value, truncated: false };
|
|
147
|
+
const suffix = "\n\n[Report truncated]";
|
|
148
|
+
let truncated = value.slice(0, Math.max(0, cap - Buffer.byteLength(suffix, "utf8")));
|
|
149
|
+
while (Buffer.byteLength(`${truncated}${suffix}`, "utf8") > cap)
|
|
150
|
+
truncated = truncated.slice(0, -1);
|
|
151
|
+
return { value: `${truncated}${suffix}`, truncated: true };
|
|
152
|
+
}
|
|
153
|
+
export function createHubDelegationController(config, options = {}) {
|
|
154
|
+
const fetchImpl = options.fetchImpl ?? fetch;
|
|
155
|
+
return {
|
|
156
|
+
async createWorkerGrant(request) {
|
|
157
|
+
const controller = new AbortController();
|
|
158
|
+
const onAbort = () => controller.abort();
|
|
159
|
+
if (request.signal?.aborted)
|
|
160
|
+
controller.abort();
|
|
161
|
+
else
|
|
162
|
+
request.signal?.addEventListener("abort", onAbort, { once: true });
|
|
163
|
+
const timeout = setTimeout(() => controller.abort(), options.timeoutMs ?? WORKER_GRANT_TIMEOUT_MS);
|
|
164
|
+
try {
|
|
165
|
+
const response = await fetchImpl(`${hubApiBase(config.hubUrl)}/llm/worker-grants`, {
|
|
166
|
+
method: "POST",
|
|
167
|
+
headers: {
|
|
168
|
+
"content-type": "application/json",
|
|
169
|
+
accept: "application/json",
|
|
170
|
+
authorization: `Bearer ${config.token}`,
|
|
171
|
+
[RUN_ID_HEADER]: request.runId,
|
|
172
|
+
[REQUEST_ID_HEADER]: request.requestId,
|
|
173
|
+
[ROOT_REQUEST_ID_HEADER]: request.rootRequestId,
|
|
174
|
+
[PARENT_REQUEST_ID_HEADER]: request.parentRequestId,
|
|
175
|
+
[DELEGATION_ID_HEADER]: request.taskId,
|
|
176
|
+
[REQUEST_SCOPE_HEADER]: "worker",
|
|
177
|
+
...(config.deviceFingerprint ? { "x-device-fingerprint": config.deviceFingerprint } : {}),
|
|
178
|
+
...(config.deviceName ? { "x-device-name": config.deviceName } : {}),
|
|
179
|
+
},
|
|
180
|
+
body: JSON.stringify({ sessionId: request.sessionId, taskId: request.taskId }),
|
|
181
|
+
signal: controller.signal,
|
|
182
|
+
});
|
|
183
|
+
if (!response.ok) {
|
|
184
|
+
await response.body?.cancel().catch(() => { });
|
|
185
|
+
throw new Error("Worker authorization unavailable");
|
|
186
|
+
}
|
|
187
|
+
const body = await response.json();
|
|
188
|
+
if (!isRecord(body) ||
|
|
189
|
+
typeof body.grant !== "string" ||
|
|
190
|
+
!body.grant.trim() ||
|
|
191
|
+
typeof body.expiresAt !== "string" ||
|
|
192
|
+
!body.expiresAt.trim() ||
|
|
193
|
+
!Number.isFinite(Date.parse(body.expiresAt))) {
|
|
194
|
+
throw new Error("Invalid worker authorization response");
|
|
195
|
+
}
|
|
196
|
+
return { token: body.grant, expiresAt: body.expiresAt };
|
|
197
|
+
}
|
|
198
|
+
finally {
|
|
199
|
+
clearTimeout(timeout);
|
|
200
|
+
request.signal?.removeEventListener("abort", onAbort);
|
|
201
|
+
}
|
|
202
|
+
},
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
export function isDelegatedWorkerProcess(env = process.env) {
|
|
206
|
+
return env[INTERNAL_WORKER_MARKER_ENV] === "1";
|
|
207
|
+
}
|
|
208
|
+
/** Capture task routing once, then remove it before worker tools can inherit it. */
|
|
209
|
+
export function consumeWorkerContext(env = process.env) {
|
|
210
|
+
if (!isDelegatedWorkerProcess(env))
|
|
211
|
+
return undefined;
|
|
212
|
+
const sessionId = env[INTERNAL_WORKER_SESSION_ENV]?.trim();
|
|
213
|
+
const taskId = env[INTERNAL_WORKER_TASK_ENV]?.trim();
|
|
214
|
+
const runId = env[INTERNAL_WORKER_RUN_ENV]?.trim();
|
|
215
|
+
const rootRequestId = env[INTERNAL_WORKER_ROOT_REQUEST_ENV]?.trim();
|
|
216
|
+
const parentRequestId = env[INTERNAL_WORKER_PARENT_REQUEST_ENV]?.trim();
|
|
217
|
+
delete env[INTERNAL_WORKER_SESSION_ENV];
|
|
218
|
+
delete env[INTERNAL_WORKER_TASK_ENV];
|
|
219
|
+
delete env[INTERNAL_WORKER_RUN_ENV];
|
|
220
|
+
delete env[INTERNAL_WORKER_ROOT_REQUEST_ENV];
|
|
221
|
+
delete env[INTERNAL_WORKER_PARENT_REQUEST_ENV];
|
|
222
|
+
if (!sessionId || !taskId || !runId || !rootRequestId || !parentRequestId)
|
|
223
|
+
return undefined;
|
|
224
|
+
return { sessionId, taskId, runId, rootRequestId, parentRequestId };
|
|
225
|
+
}
|
|
226
|
+
function killProcessTree(pid, signal) {
|
|
227
|
+
if (pid === undefined)
|
|
228
|
+
return;
|
|
229
|
+
if (process.platform === "win32") {
|
|
230
|
+
execFile("taskkill", ["/T", "/F", "/PID", String(pid)], () => undefined);
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
try {
|
|
234
|
+
process.kill(-pid, signal);
|
|
235
|
+
}
|
|
236
|
+
catch {
|
|
237
|
+
// The detached process group may already have exited.
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
export async function runDelegatedWorker(options) {
|
|
241
|
+
const prompt = await writeWorkerPrompt();
|
|
242
|
+
const task = composeDelegatedTask(options.task, options.acceptanceCriteria, options.relevantPaths);
|
|
243
|
+
const args = buildDelegatedWorkerArgs(task, prompt.filePath);
|
|
244
|
+
let output = "";
|
|
245
|
+
let turns = 0;
|
|
246
|
+
let terminalStatus;
|
|
247
|
+
let assistantFailed = false;
|
|
248
|
+
let eventBytes = 0;
|
|
249
|
+
try {
|
|
250
|
+
const exitCode = await new Promise((resolve) => {
|
|
251
|
+
const invocation = getNexusInvocation(args);
|
|
252
|
+
const child = spawn(invocation.command, invocation.args, {
|
|
253
|
+
cwd: options.cwd,
|
|
254
|
+
detached: true,
|
|
255
|
+
env: {
|
|
256
|
+
...process.env,
|
|
257
|
+
[INTERNAL_WORKER_MARKER_ENV]: "1",
|
|
258
|
+
[INTERNAL_WORKER_SESSION_ENV]: options.workerContext.sessionId,
|
|
259
|
+
[INTERNAL_WORKER_TASK_ENV]: options.workerContext.taskId,
|
|
260
|
+
[INTERNAL_WORKER_RUN_ENV]: options.workerContext.runId,
|
|
261
|
+
[INTERNAL_WORKER_ROOT_REQUEST_ENV]: options.workerContext.rootRequestId,
|
|
262
|
+
[INTERNAL_WORKER_PARENT_REQUEST_ENV]: options.workerContext.parentRequestId,
|
|
263
|
+
},
|
|
264
|
+
shell: false,
|
|
265
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
266
|
+
windowsHide: true,
|
|
267
|
+
});
|
|
268
|
+
let buffer = "";
|
|
269
|
+
let settled = false;
|
|
270
|
+
let stopped = false;
|
|
271
|
+
let forceKillTimer;
|
|
272
|
+
const finish = (code) => {
|
|
273
|
+
if (settled)
|
|
274
|
+
return;
|
|
275
|
+
settled = true;
|
|
276
|
+
clearTimeout(timeoutTimer);
|
|
277
|
+
clearTimeout(forceKillTimer);
|
|
278
|
+
options.signal?.removeEventListener("abort", abortHandler);
|
|
279
|
+
resolve(code);
|
|
280
|
+
};
|
|
281
|
+
const stop = (status) => {
|
|
282
|
+
if (stopped)
|
|
283
|
+
return;
|
|
284
|
+
stopped = true;
|
|
285
|
+
terminalStatus = status;
|
|
286
|
+
killProcessTree(child.pid, "SIGTERM");
|
|
287
|
+
forceKillTimer = setTimeout(() => {
|
|
288
|
+
if (process.platform !== "win32")
|
|
289
|
+
killProcessTree(child.pid, "SIGKILL");
|
|
290
|
+
finish(1);
|
|
291
|
+
}, 5000);
|
|
292
|
+
};
|
|
293
|
+
const emitProgress = () => {
|
|
294
|
+
options.onUpdate?.({
|
|
295
|
+
content: [{ type: "text", text: "Delegated work is in progress." }],
|
|
296
|
+
details: { status: "running", turns },
|
|
297
|
+
});
|
|
298
|
+
};
|
|
299
|
+
const processLine = (line) => {
|
|
300
|
+
if (!line.trim())
|
|
301
|
+
return;
|
|
302
|
+
let parsed;
|
|
303
|
+
try {
|
|
304
|
+
parsed = JSON.parse(line);
|
|
305
|
+
}
|
|
306
|
+
catch {
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
if (!isRecord(parsed) || parsed.type !== "message_end")
|
|
310
|
+
return;
|
|
311
|
+
const snapshot = parseAssistantSnapshot(parsed.message);
|
|
312
|
+
if (!snapshot)
|
|
313
|
+
return;
|
|
314
|
+
turns += 1;
|
|
315
|
+
if (snapshot.text)
|
|
316
|
+
output = snapshot.text;
|
|
317
|
+
if (snapshot.stopReason === "error" || snapshot.stopReason === "aborted")
|
|
318
|
+
assistantFailed = true;
|
|
319
|
+
emitProgress();
|
|
320
|
+
if (turns >= MAX_DELEGATED_TURNS)
|
|
321
|
+
stop("turn_limit");
|
|
322
|
+
};
|
|
323
|
+
const abortHandler = () => stop("aborted");
|
|
324
|
+
const timeoutTimer = setTimeout(() => stop("timeout"), Math.min(Math.max(1_000, options.timeoutMs ?? DELEGATED_WORKER_TIMEOUT_MS), DELEGATED_WORKER_TIMEOUT_MS));
|
|
325
|
+
child.stdout.on("data", (chunk) => {
|
|
326
|
+
eventBytes += Buffer.byteLength(chunk);
|
|
327
|
+
if (eventBytes > DELEGATED_EVENT_CAP_BYTES) {
|
|
328
|
+
assistantFailed = true;
|
|
329
|
+
stop("event_limit");
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
buffer += chunk.toString();
|
|
333
|
+
if (Buffer.byteLength(buffer, "utf8") > DELEGATED_EVENT_CAP_BYTES) {
|
|
334
|
+
buffer = "";
|
|
335
|
+
assistantFailed = true;
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
const lines = buffer.split("\n");
|
|
339
|
+
buffer = lines.pop() ?? "";
|
|
340
|
+
for (const line of lines)
|
|
341
|
+
processLine(line);
|
|
342
|
+
});
|
|
343
|
+
// Consume stderr so a noisy child cannot block. Raw provider/process output
|
|
344
|
+
// is intentionally not retained or exposed to the orchestrator or user.
|
|
345
|
+
child.stderr.on("data", () => undefined);
|
|
346
|
+
child.on("close", (code) => {
|
|
347
|
+
if (buffer.trim())
|
|
348
|
+
processLine(buffer);
|
|
349
|
+
finish(code ?? (terminalStatus ? 1 : 0));
|
|
350
|
+
});
|
|
351
|
+
child.on("error", () => finish(1));
|
|
352
|
+
if (options.signal?.aborted)
|
|
353
|
+
abortHandler();
|
|
354
|
+
else
|
|
355
|
+
options.signal?.addEventListener("abort", abortHandler, { once: true });
|
|
356
|
+
});
|
|
357
|
+
const limitedOutput = truncateUtf8(output, DELEGATED_OUTPUT_CAP_BYTES);
|
|
358
|
+
const status = terminalStatus ?? (exitCode === 0 && !assistantFailed ? "completed" : "failed");
|
|
359
|
+
return {
|
|
360
|
+
status,
|
|
361
|
+
turns,
|
|
362
|
+
exitCode,
|
|
363
|
+
output: limitedOutput.value,
|
|
364
|
+
outputTruncated: limitedOutput.truncated,
|
|
365
|
+
};
|
|
366
|
+
}
|
|
367
|
+
finally {
|
|
368
|
+
await fs.promises.rm(prompt.dir, { recursive: true, force: true });
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
const DelegateTaskParameters = Type.Object({
|
|
372
|
+
task: Type.String({ description: "A narrow implementation task with explicit boundaries", maxLength: 32_768 }),
|
|
373
|
+
acceptanceCriteria: Type.Optional(Type.Array(Type.String({ maxLength: 2000 }), { maxItems: 10 })),
|
|
374
|
+
relevantPaths: Type.Optional(Type.Array(Type.String({ maxLength: 1000 }), { maxItems: 20 })),
|
|
375
|
+
});
|
|
376
|
+
function resultContent(result) {
|
|
377
|
+
const report = result.output.trim() || "No implementation report was returned.";
|
|
378
|
+
if (result.status === "completed")
|
|
379
|
+
return `Delegated implementation completed.\n\n${report}`;
|
|
380
|
+
return `Delegated implementation did not complete (${result.status}).\n\nPartial report:\n${report}`;
|
|
381
|
+
}
|
|
382
|
+
/** Register the tool only after the Hub explicitly advertises delegation. */
|
|
383
|
+
export function registerDelegateTaskTool(pi, capability) {
|
|
384
|
+
const maxTaskBytes = Math.min(Math.max(1, Math.floor(capability.maxTaskBytes)), MAX_DELEGATED_TASK_BYTES);
|
|
385
|
+
const workerTimeoutMs = Math.min(Math.max(1_000, Math.floor(capability.workerTimeoutMs)), DELEGATED_WORKER_TIMEOUT_MS);
|
|
386
|
+
const runId = randomUUID();
|
|
387
|
+
let rootRequestId;
|
|
388
|
+
let latestRootRequestId;
|
|
389
|
+
let activeWorkers = 0;
|
|
390
|
+
let invocationCount = 0;
|
|
391
|
+
let rootReviewToolCalls = 0;
|
|
392
|
+
pi.on("before_agent_start", (event) => {
|
|
393
|
+
invocationCount = 0;
|
|
394
|
+
rootReviewToolCalls = 0;
|
|
395
|
+
return {
|
|
396
|
+
systemPrompt: `${event.systemPrompt}\n\nImplementation policy: perform implementation, mutation, shell, and repeated tool loops through delegate_task. Keep this parent conversation focused on planning and final review. Use only bounded read-only tools to inspect worker changes, verify graph impact, and reach the final answer.`,
|
|
397
|
+
};
|
|
398
|
+
});
|
|
399
|
+
pi.on("before_provider_headers", (event, ctx) => {
|
|
400
|
+
if (!isAutoModel(ctx.model) || isDelegatedWorkerProcess())
|
|
401
|
+
return;
|
|
402
|
+
const requestId = randomUUID();
|
|
403
|
+
rootRequestId ??= requestId;
|
|
404
|
+
latestRootRequestId = requestId;
|
|
405
|
+
event.headers[RUN_ID_HEADER] = runId;
|
|
406
|
+
event.headers[REQUEST_ID_HEADER] = requestId;
|
|
407
|
+
event.headers[ROOT_REQUEST_ID_HEADER] = rootRequestId;
|
|
408
|
+
event.headers[REQUEST_SCOPE_HEADER] = "root";
|
|
409
|
+
});
|
|
410
|
+
pi.on("tool_call", (event, ctx) => {
|
|
411
|
+
if (!isAutoModel(ctx.model) || isDelegatedWorkerProcess() || event.toolName === DELEGATE_TASK_TOOL_NAME) {
|
|
412
|
+
return;
|
|
413
|
+
}
|
|
414
|
+
if (!ROOT_REVIEW_TOOLS.has(event.toolName)) {
|
|
415
|
+
return {
|
|
416
|
+
block: true,
|
|
417
|
+
reason: "This parent run is review-only. Use delegate_task for implementation, mutation, shell commands, tests, or other execution.",
|
|
418
|
+
};
|
|
419
|
+
}
|
|
420
|
+
if (rootReviewToolCalls >= MAX_ROOT_REVIEW_TOOL_CALLS) {
|
|
421
|
+
return {
|
|
422
|
+
block: true,
|
|
423
|
+
reason: "The bounded parent review budget is exhausted. Delegate further investigation or finalize the review.",
|
|
424
|
+
};
|
|
425
|
+
}
|
|
426
|
+
rootReviewToolCalls += 1;
|
|
427
|
+
});
|
|
428
|
+
pi.registerTool({
|
|
429
|
+
name: DELEGATE_TASK_TOOL_NAME,
|
|
430
|
+
label: "Work",
|
|
431
|
+
description: "Delegate one narrow implementation task to an isolated internal worker. Keep planning, user communication, and final verification in this conversation.",
|
|
432
|
+
promptGuidelines: [
|
|
433
|
+
"Use delegate_task for bounded implementation, mutation, command execution, tests, and repeated tool work after you have formed a plan.",
|
|
434
|
+
"You remain responsible for reviewing the returned report, verifying the result, and communicating with the user.",
|
|
435
|
+
],
|
|
436
|
+
parameters: DelegateTaskParameters,
|
|
437
|
+
executionMode: "sequential",
|
|
438
|
+
async execute(_toolCallId, params, signal, onUpdate, ctx) {
|
|
439
|
+
if (!isAutoModel(ctx.model)) {
|
|
440
|
+
return {
|
|
441
|
+
content: [{ type: "text", text: "Delegated work is unavailable for the active mode." }],
|
|
442
|
+
details: { status: "failed", turns: 0 },
|
|
443
|
+
};
|
|
444
|
+
}
|
|
445
|
+
if (!ctx.isProjectTrusted()) {
|
|
446
|
+
return {
|
|
447
|
+
content: [{ type: "text", text: "Delegated work requires a trusted project." }],
|
|
448
|
+
details: { status: "failed", turns: 0 },
|
|
449
|
+
};
|
|
450
|
+
}
|
|
451
|
+
if (activeWorkers >= MAX_PARALLEL_MUTATING_WORKERS || invocationCount >= MAX_DELEGATIONS_PER_RUN) {
|
|
452
|
+
return {
|
|
453
|
+
content: [{ type: "text", text: "Delegated work is temporarily unavailable for this run." }],
|
|
454
|
+
details: { status: "failed", turns: 0 },
|
|
455
|
+
};
|
|
456
|
+
}
|
|
457
|
+
const request = {
|
|
458
|
+
task: params.task,
|
|
459
|
+
acceptanceCriteria: params.acceptanceCriteria ?? [],
|
|
460
|
+
relevantPaths: params.relevantPaths ?? [],
|
|
461
|
+
cwd: ctx.cwd,
|
|
462
|
+
signal,
|
|
463
|
+
};
|
|
464
|
+
if (Buffer.byteLength(composeDelegatedTask(request.task, request.acceptanceCriteria, request.relevantPaths), "utf8") > maxTaskBytes) {
|
|
465
|
+
return {
|
|
466
|
+
content: [{ type: "text", text: "Delegated work is too large for one bounded task." }],
|
|
467
|
+
details: { status: "failed", turns: 0 },
|
|
468
|
+
};
|
|
469
|
+
}
|
|
470
|
+
activeWorkers += 1;
|
|
471
|
+
invocationCount += 1;
|
|
472
|
+
try {
|
|
473
|
+
const delegationId = randomUUID();
|
|
474
|
+
rootRequestId ??= randomUUID();
|
|
475
|
+
const result = await runDelegatedWorker({
|
|
476
|
+
...request,
|
|
477
|
+
workerContext: {
|
|
478
|
+
sessionId: ctx.sessionManager.getSessionId(),
|
|
479
|
+
taskId: delegationId,
|
|
480
|
+
runId,
|
|
481
|
+
rootRequestId,
|
|
482
|
+
parentRequestId: latestRootRequestId ?? rootRequestId,
|
|
483
|
+
},
|
|
484
|
+
timeoutMs: workerTimeoutMs,
|
|
485
|
+
onUpdate,
|
|
486
|
+
});
|
|
487
|
+
return {
|
|
488
|
+
content: [{ type: "text", text: resultContent(result) }],
|
|
489
|
+
details: {
|
|
490
|
+
status: result.status,
|
|
491
|
+
turns: result.turns,
|
|
492
|
+
exitCode: result.exitCode,
|
|
493
|
+
outputTruncated: result.outputTruncated,
|
|
494
|
+
},
|
|
495
|
+
};
|
|
496
|
+
}
|
|
497
|
+
catch {
|
|
498
|
+
return {
|
|
499
|
+
content: [{ type: "text", text: "Delegated work could not be started." }],
|
|
500
|
+
details: { status: "failed", turns: 0 },
|
|
501
|
+
};
|
|
502
|
+
}
|
|
503
|
+
finally {
|
|
504
|
+
activeWorkers -= 1;
|
|
505
|
+
}
|
|
506
|
+
},
|
|
507
|
+
renderCall(_args, theme) {
|
|
508
|
+
return new Text(theme.fg("toolTitle", theme.bold("Working…")), 0, 0);
|
|
509
|
+
},
|
|
510
|
+
renderResult(result, _options, theme) {
|
|
511
|
+
const details = result.details;
|
|
512
|
+
if (details?.status === "running")
|
|
513
|
+
return new Text(theme.fg("muted", "Working…"), 0, 0);
|
|
514
|
+
if (details?.status === "completed")
|
|
515
|
+
return new Text(theme.fg("success", "Work completed"), 0, 0);
|
|
516
|
+
return new Text(theme.fg("error", "Work needs attention"), 0, 0);
|
|
517
|
+
},
|
|
518
|
+
});
|
|
519
|
+
}
|
|
520
|
+
/**
|
|
521
|
+
* Install a fail-closed header hook for one worker task. A fresh one-time grant
|
|
522
|
+
* is minted for each model turn; no grant is inherited by worker tools.
|
|
523
|
+
*/
|
|
524
|
+
export function registerWorkerGrantHeaders(pi, workerContext, controller) {
|
|
525
|
+
pi.on("before_provider_headers", async (event, ctx) => {
|
|
526
|
+
if (!isAutoModel(ctx.model)) {
|
|
527
|
+
ctx.abort();
|
|
528
|
+
return;
|
|
529
|
+
}
|
|
530
|
+
// Set an invalid sentinel first. Extension handler errors are reported and
|
|
531
|
+
// swallowed by the runner, so this prevents a mint failure from silently
|
|
532
|
+
// falling back to an ungranted root request.
|
|
533
|
+
const requestId = randomUUID();
|
|
534
|
+
event.headers[INTERNAL_WORKER_GRANT_HEADER] = INVALID_WORKER_GRANT;
|
|
535
|
+
event.headers[INTERNAL_WORKER_TASK_HEADER] = workerContext.taskId;
|
|
536
|
+
event.headers[SESSION_AFFINITY_HEADER] = workerContext.sessionId;
|
|
537
|
+
event.headers[RUN_ID_HEADER] = workerContext.runId;
|
|
538
|
+
event.headers[REQUEST_ID_HEADER] = requestId;
|
|
539
|
+
event.headers[ROOT_REQUEST_ID_HEADER] = workerContext.rootRequestId;
|
|
540
|
+
event.headers[PARENT_REQUEST_ID_HEADER] = workerContext.parentRequestId;
|
|
541
|
+
event.headers[DELEGATION_ID_HEADER] = workerContext.taskId;
|
|
542
|
+
event.headers[REQUEST_SCOPE_HEADER] = "worker";
|
|
543
|
+
if (!controller)
|
|
544
|
+
return;
|
|
545
|
+
const grant = await controller.createWorkerGrant({
|
|
546
|
+
...workerContext,
|
|
547
|
+
requestId,
|
|
548
|
+
signal: ctx.signal,
|
|
549
|
+
});
|
|
550
|
+
event.headers[INTERNAL_WORKER_GRANT_HEADER] = grant.token;
|
|
551
|
+
});
|
|
552
|
+
}
|
|
553
|
+
export default function delegatedWorkerRuntime(pi) {
|
|
554
|
+
const isWorker = isDelegatedWorkerProcess();
|
|
555
|
+
const workerContext = consumeWorkerContext();
|
|
556
|
+
if (!isWorker)
|
|
557
|
+
return;
|
|
558
|
+
const config = resolveGitNexusConfig();
|
|
559
|
+
const failClosedContext = {
|
|
560
|
+
sessionId: "unavailable",
|
|
561
|
+
taskId: "unavailable",
|
|
562
|
+
runId: "unavailable",
|
|
563
|
+
rootRequestId: "unavailable",
|
|
564
|
+
parentRequestId: "unavailable",
|
|
565
|
+
};
|
|
566
|
+
registerWorkerGrantHeaders(pi, workerContext ?? failClosedContext, workerContext && config ? createHubDelegationController(config) : undefined);
|
|
567
|
+
}
|
|
568
|
+
//# sourceMappingURL=index.js.map
|