@akon-labs/nexus-cli 0.1.6 → 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 +12 -0
- package/README.md +2 -3
- package/dist/cli.js +1192 -649
- package/dist/core/slash-commands.d.ts.map +1 -1
- package/dist/core/slash-commands.js +0 -1
- package/dist/core/slash-commands.js.map +1 -1
- 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 -693
- package/dist/modes/interactive/interactive-mode.d.ts +0 -5
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +0 -122
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/dist/rpc-entry.js +1192 -649
- package/dist/vendor/ai.js +110 -15
- package/docs/rpc.md +1 -1
- package/docs/usage.md +0 -1
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/extensions/subagent/index.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Internal delegated-worker runtime.\n *\n * This file deliberately does not register a user-facing tool by default. The\n * authenticated Hub policy must advertise delegation before the GitNexus\n * extension calls registerDelegateTaskTool(). The CLI owns process isolation\n * and streaming UX; the Hub owns one-time worker authorization and routing.\n */\n\nimport { execFile, spawn } from \"node:child_process\";\nimport { randomUUID } from \"node:crypto\";\nimport * as fs from \"node:fs\";\nimport * as os from \"node:os\";\nimport * as path from \"node:path\";\nimport type { AgentToolResult } from \"@earendil-works/pi-agent-core\";\nimport { Text } from \"@earendil-works/pi-tui\";\nimport { Type } from \"typebox\";\nimport { AUTO_MODEL_ID, AUTO_PROVIDER_ID, isAutoModel } from \"../../core/auto-model.ts\";\nimport type { ExtensionAPI } from \"../../core/extensions/types.ts\";\nimport { withFileMutationQueue } from \"../../core/tools/index.ts\";\nimport type { GitNexusConfig } from \"../gitnexus/config.ts\";\nimport { resolveGitNexusConfig } from \"../gitnexus/config.ts\";\nimport { hubApiBase } from \"../gitnexus/device-auth.ts\";\n\nexport const DELEGATE_TASK_TOOL_NAME = \"delegate_task\";\nexport const INTERNAL_WORKER_MARKER_ENV = \"NEXUS_INTERNAL_DELEGATED_WORKER\";\nexport const INTERNAL_WORKER_SESSION_ENV = \"NEXUS_INTERNAL_WORKER_SESSION_ID\";\nexport const INTERNAL_WORKER_TASK_ENV = \"NEXUS_INTERNAL_WORKER_TASK_ID\";\nexport const INTERNAL_WORKER_RUN_ENV = \"NEXUS_INTERNAL_WORKER_RUN_ID\";\nexport const INTERNAL_WORKER_ROOT_REQUEST_ENV = \"NEXUS_INTERNAL_WORKER_ROOT_REQUEST_ID\";\nexport const INTERNAL_WORKER_PARENT_REQUEST_ENV = \"NEXUS_INTERNAL_WORKER_PARENT_REQUEST_ID\";\nexport const INTERNAL_WORKER_GRANT_HEADER = \"x-nexus-worker-grant\";\nexport const INTERNAL_WORKER_TASK_HEADER = \"x-nexus-worker-task-id\";\nexport const SESSION_AFFINITY_HEADER = \"x-session-affinity\";\nexport const RUN_ID_HEADER = \"x-nexus-run-id\";\nexport const REQUEST_ID_HEADER = \"x-nexus-request-id\";\nexport const ROOT_REQUEST_ID_HEADER = \"x-nexus-root-request-id\";\nexport const PARENT_REQUEST_ID_HEADER = \"x-nexus-parent-request-id\";\nexport const DELEGATION_ID_HEADER = \"x-nexus-delegation-id\";\nexport const REQUEST_SCOPE_HEADER = \"x-nexus-request-scope\";\nexport const MAX_DELEGATED_TURNS = 12;\nexport const MAX_DELEGATIONS_PER_RUN = 3;\nexport const DELEGATED_WORKER_TIMEOUT_MS = 5 * 60 * 1000;\nexport const DELEGATED_OUTPUT_CAP_BYTES = 32 * 1024;\nexport const DELEGATED_EVENT_CAP_BYTES = 1024 * 1024;\nexport const MAX_DELEGATED_TASK_BYTES = 32 * 1024;\n\nconst WORKER_GRANT_TIMEOUT_MS = 10_000;\nconst INVALID_WORKER_GRANT = \"unavailable\";\nconst MAX_ROOT_REVIEW_TOOL_CALLS = 8;\nconst MAX_PARALLEL_MUTATING_WORKERS = 1;\n\nconst ROOT_REVIEW_TOOLS = new Set([\n\t\"read\",\n\t\"grep\",\n\t\"find\",\n\t\"ls\",\n\t\"gitnexus_query\",\n\t\"gitnexus_context\",\n\t\"gitnexus_impact\",\n\t\"gitnexus_detect_changes\",\n\t\"gitnexus_cypher\",\n\t\"gitnexus_list_repos\",\n\t\"web_search\",\n\t\"web_fetch\",\n]);\n\nexport const DELEGATED_WORKER_TOOLS = [\n\t\"read\",\n\t\"grep\",\n\t\"find\",\n\t\"ls\",\n\t\"bash\",\n\t\"write\",\n\t\"edit\",\n\t\"gitnexus_query\",\n\t\"gitnexus_context\",\n\t\"gitnexus_impact\",\n\t\"gitnexus_detect_changes\",\n\t\"gitnexus_cypher\",\n\t\"gitnexus_list_repos\",\n] as const;\n\nconst WORKER_SYSTEM_PROMPT = `You are an internal implementation worker for Nexus Agent.\n\nComplete 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.\n\nFinish with a concise report containing:\n- changes made\n- verification run and results\n- blockers or remaining risks`;\n\nexport type DelegationStatus =\n\t| \"running\"\n\t| \"completed\"\n\t| \"failed\"\n\t| \"aborted\"\n\t| \"timeout\"\n\t| \"turn_limit\"\n\t| \"event_limit\";\n\nexport interface DelegationDetails {\n\tstatus: DelegationStatus;\n\tturns: number;\n\texitCode?: number;\n\toutputTruncated?: boolean;\n}\n\nexport interface WorkerGrant {\n\t/** Opaque, short-lived grant minted by the Hub for one worker completion. */\n\ttoken: string;\n\texpiresAt: string;\n}\n\nexport interface DelegationCapability {\n\tenabled: true;\n\tversion: 1;\n\tmaxParallel: number;\n\tmaxTaskBytes: number;\n\tworkerTimeoutMs: number;\n}\n\nexport interface WorkerGrantRequest {\n\tsessionId: string;\n\ttaskId: string;\n\trunId: string;\n\trootRequestId: string;\n\tparentRequestId: string;\n\trequestId: string;\n\tsignal: AbortSignal | undefined;\n}\n\nexport interface DelegationController {\n\tcreateWorkerGrant(request: WorkerGrantRequest): Promise<WorkerGrant>;\n}\n\nexport interface DelegationRequest {\n\ttask: string;\n\tacceptanceCriteria: string[];\n\trelevantPaths: string[];\n\tcwd: string;\n\tsignal: AbortSignal | undefined;\n}\n\nexport interface WorkerContext {\n\tsessionId: string;\n\ttaskId: string;\n\trunId: string;\n\trootRequestId: string;\n\tparentRequestId: string;\n}\n\nexport interface DelegatedWorkerOptions extends DelegationRequest {\n\tworkerContext: WorkerContext;\n\ttimeoutMs?: number;\n\tonUpdate?: (result: AgentToolResult<DelegationDetails>) => void;\n}\n\nexport interface DelegatedWorkerResult {\n\tstatus: Exclude<DelegationStatus, \"running\">;\n\tturns: number;\n\texitCode: number;\n\toutput: string;\n\toutputTruncated: boolean;\n}\n\ninterface AssistantSnapshot {\n\ttext: string;\n\tstopReason?: string;\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n\treturn typeof value === \"object\" && value !== null;\n}\n\nfunction parseAssistantSnapshot(value: unknown): AssistantSnapshot | undefined {\n\tif (!isRecord(value) || value.role !== \"assistant\" || !Array.isArray(value.content)) return undefined;\n\tconst text = value.content\n\t\t.filter(isRecord)\n\t\t.filter((part) => part.type === \"text\" && typeof part.text === \"string\")\n\t\t.map((part) => part.text as string)\n\t\t.join(\"\\n\");\n\treturn {\n\t\ttext,\n\t\t...(typeof value.stopReason === \"string\" ? { stopReason: value.stopReason } : {}),\n\t};\n}\n\nexport function composeDelegatedTask(\n\ttask: string,\n\tacceptanceCriteria: readonly string[],\n\trelevantPaths: readonly string[],\n): string {\n\tconst sections = [`Task:\\n${task.trim()}`];\n\tif (acceptanceCriteria.length > 0) {\n\t\tsections.push(`Acceptance criteria:\\n${acceptanceCriteria.map((criterion) => `- ${criterion}`).join(\"\\n\")}`);\n\t}\n\tif (relevantPaths.length > 0) {\n\t\tsections.push(`Relevant paths:\\n${relevantPaths.map((filePath) => `- ${filePath}`).join(\"\\n\")}`);\n\t}\n\treturn sections.join(\"\\n\\n\");\n}\n\nexport function buildDelegatedWorkerArgs(task: string, promptPath: string): string[] {\n\treturn [\n\t\t\"--mode\",\n\t\t\"json\",\n\t\t\"-p\",\n\t\t\"--no-session\",\n\t\t\"--model\",\n\t\t`${AUTO_PROVIDER_ID}/${AUTO_MODEL_ID}`,\n\t\t\"--tools\",\n\t\tDELEGATED_WORKER_TOOLS.join(\",\"),\n\t\t\"--exclude-tools\",\n\t\t`${DELEGATE_TASK_TOOL_NAME},subagent`,\n\t\t\"--append-system-prompt\",\n\t\tpromptPath,\n\t\ttask,\n\t];\n}\n\nfunction getNexusInvocation(args: string[]): { command: string; args: string[] } {\n\tconst currentScript = process.argv[1];\n\tconst isBunVirtualScript = currentScript?.startsWith(\"/$bunfs/root/\");\n\tif (currentScript && !isBunVirtualScript && fs.existsSync(currentScript)) {\n\t\treturn { command: process.execPath, args: [currentScript, ...args] };\n\t}\n\n\tconst execName = path.basename(process.execPath).toLowerCase();\n\tconst isGenericRuntime = /^(node|bun)(\\.exe)?$/.test(execName);\n\treturn isGenericRuntime ? { command: \"nexus\", args } : { command: process.execPath, args };\n}\n\nasync function writeWorkerPrompt(): Promise<{ dir: string; filePath: string }> {\n\tconst dir = await fs.promises.mkdtemp(path.join(os.tmpdir(), \"nexus-worker-\"));\n\tconst filePath = path.join(dir, \"system.md\");\n\tawait withFileMutationQueue(filePath, async () => {\n\t\tawait fs.promises.writeFile(filePath, WORKER_SYSTEM_PROMPT, { encoding: \"utf8\", mode: 0o600 });\n\t});\n\treturn { dir, filePath };\n}\n\nfunction truncateUtf8(value: string, cap: number): { value: string; truncated: boolean } {\n\tif (Buffer.byteLength(value, \"utf8\") <= cap) return { value, truncated: false };\n\tconst suffix = \"\\n\\n[Report truncated]\";\n\tlet truncated = value.slice(0, Math.max(0, cap - Buffer.byteLength(suffix, \"utf8\")));\n\twhile (Buffer.byteLength(`${truncated}${suffix}`, \"utf8\") > cap) truncated = truncated.slice(0, -1);\n\treturn { value: `${truncated}${suffix}`, truncated: true };\n}\n\nexport function createHubDelegationController(\n\tconfig: GitNexusConfig,\n\toptions: { fetchImpl?: typeof fetch; timeoutMs?: number } = {},\n): DelegationController {\n\tconst fetchImpl = options.fetchImpl ?? fetch;\n\treturn {\n\t\tasync createWorkerGrant(request) {\n\t\t\tconst controller = new AbortController();\n\t\t\tconst onAbort = (): void => controller.abort();\n\t\t\tif (request.signal?.aborted) controller.abort();\n\t\t\telse request.signal?.addEventListener(\"abort\", onAbort, { once: true });\n\t\t\tconst timeout = setTimeout(() => controller.abort(), options.timeoutMs ?? WORKER_GRANT_TIMEOUT_MS);\n\t\t\ttry {\n\t\t\t\tconst response = await fetchImpl(`${hubApiBase(config.hubUrl)}/llm/worker-grants`, {\n\t\t\t\t\tmethod: \"POST\",\n\t\t\t\t\theaders: {\n\t\t\t\t\t\t\"content-type\": \"application/json\",\n\t\t\t\t\t\taccept: \"application/json\",\n\t\t\t\t\t\tauthorization: `Bearer ${config.token}`,\n\t\t\t\t\t\t[RUN_ID_HEADER]: request.runId,\n\t\t\t\t\t\t[REQUEST_ID_HEADER]: request.requestId,\n\t\t\t\t\t\t[ROOT_REQUEST_ID_HEADER]: request.rootRequestId,\n\t\t\t\t\t\t[PARENT_REQUEST_ID_HEADER]: request.parentRequestId,\n\t\t\t\t\t\t[DELEGATION_ID_HEADER]: request.taskId,\n\t\t\t\t\t\t[REQUEST_SCOPE_HEADER]: \"worker\",\n\t\t\t\t\t\t...(config.deviceFingerprint ? { \"x-device-fingerprint\": config.deviceFingerprint } : {}),\n\t\t\t\t\t\t...(config.deviceName ? { \"x-device-name\": config.deviceName } : {}),\n\t\t\t\t\t},\n\t\t\t\t\tbody: JSON.stringify({ sessionId: request.sessionId, taskId: request.taskId }),\n\t\t\t\t\tsignal: controller.signal,\n\t\t\t\t});\n\t\t\t\tif (!response.ok) {\n\t\t\t\t\tawait response.body?.cancel().catch(() => {});\n\t\t\t\t\tthrow new Error(\"Worker authorization unavailable\");\n\t\t\t\t}\n\t\t\t\tconst body: unknown = await response.json();\n\t\t\t\tif (\n\t\t\t\t\t!isRecord(body) ||\n\t\t\t\t\ttypeof body.grant !== \"string\" ||\n\t\t\t\t\t!body.grant.trim() ||\n\t\t\t\t\ttypeof body.expiresAt !== \"string\" ||\n\t\t\t\t\t!body.expiresAt.trim() ||\n\t\t\t\t\t!Number.isFinite(Date.parse(body.expiresAt))\n\t\t\t\t) {\n\t\t\t\t\tthrow new Error(\"Invalid worker authorization response\");\n\t\t\t\t}\n\t\t\t\treturn { token: body.grant, expiresAt: body.expiresAt };\n\t\t\t} finally {\n\t\t\t\tclearTimeout(timeout);\n\t\t\t\trequest.signal?.removeEventListener(\"abort\", onAbort);\n\t\t\t}\n\t\t},\n\t};\n}\n\nexport function isDelegatedWorkerProcess(env: NodeJS.ProcessEnv = process.env): boolean {\n\treturn env[INTERNAL_WORKER_MARKER_ENV] === \"1\";\n}\n\n/** Capture task routing once, then remove it before worker tools can inherit it. */\nexport function consumeWorkerContext(env: NodeJS.ProcessEnv = process.env): WorkerContext | undefined {\n\tif (!isDelegatedWorkerProcess(env)) return undefined;\n\tconst sessionId = env[INTERNAL_WORKER_SESSION_ENV]?.trim();\n\tconst taskId = env[INTERNAL_WORKER_TASK_ENV]?.trim();\n\tconst runId = env[INTERNAL_WORKER_RUN_ENV]?.trim();\n\tconst rootRequestId = env[INTERNAL_WORKER_ROOT_REQUEST_ENV]?.trim();\n\tconst parentRequestId = env[INTERNAL_WORKER_PARENT_REQUEST_ENV]?.trim();\n\tdelete env[INTERNAL_WORKER_SESSION_ENV];\n\tdelete env[INTERNAL_WORKER_TASK_ENV];\n\tdelete env[INTERNAL_WORKER_RUN_ENV];\n\tdelete env[INTERNAL_WORKER_ROOT_REQUEST_ENV];\n\tdelete env[INTERNAL_WORKER_PARENT_REQUEST_ENV];\n\tif (!sessionId || !taskId || !runId || !rootRequestId || !parentRequestId) return undefined;\n\treturn { sessionId, taskId, runId, rootRequestId, parentRequestId };\n}\n\nfunction killProcessTree(pid: number | undefined, signal: NodeJS.Signals): void {\n\tif (pid === undefined) return;\n\tif (process.platform === \"win32\") {\n\t\texecFile(\"taskkill\", [\"/T\", \"/F\", \"/PID\", String(pid)], () => undefined);\n\t\treturn;\n\t}\n\ttry {\n\t\tprocess.kill(-pid, signal);\n\t} catch {\n\t\t// The detached process group may already have exited.\n\t}\n}\n\nexport async function runDelegatedWorker(options: DelegatedWorkerOptions): Promise<DelegatedWorkerResult> {\n\tconst prompt = await writeWorkerPrompt();\n\tconst task = composeDelegatedTask(options.task, options.acceptanceCriteria, options.relevantPaths);\n\tconst args = buildDelegatedWorkerArgs(task, prompt.filePath);\n\tlet output = \"\";\n\tlet turns = 0;\n\tlet terminalStatus: Exclude<DelegationStatus, \"running\" | \"completed\" | \"failed\"> | undefined;\n\tlet assistantFailed = false;\n\tlet eventBytes = 0;\n\n\ttry {\n\t\tconst exitCode = await new Promise<number>((resolve) => {\n\t\t\tconst invocation = getNexusInvocation(args);\n\t\t\tconst child = spawn(invocation.command, invocation.args, {\n\t\t\t\tcwd: options.cwd,\n\t\t\t\tdetached: true,\n\t\t\t\tenv: {\n\t\t\t\t\t...process.env,\n\t\t\t\t\t[INTERNAL_WORKER_MARKER_ENV]: \"1\",\n\t\t\t\t\t[INTERNAL_WORKER_SESSION_ENV]: options.workerContext.sessionId,\n\t\t\t\t\t[INTERNAL_WORKER_TASK_ENV]: options.workerContext.taskId,\n\t\t\t\t\t[INTERNAL_WORKER_RUN_ENV]: options.workerContext.runId,\n\t\t\t\t\t[INTERNAL_WORKER_ROOT_REQUEST_ENV]: options.workerContext.rootRequestId,\n\t\t\t\t\t[INTERNAL_WORKER_PARENT_REQUEST_ENV]: options.workerContext.parentRequestId,\n\t\t\t\t},\n\t\t\t\tshell: false,\n\t\t\t\tstdio: [\"ignore\", \"pipe\", \"pipe\"],\n\t\t\t\twindowsHide: true,\n\t\t\t});\n\t\t\tlet buffer = \"\";\n\t\t\tlet settled = false;\n\t\t\tlet stopped = false;\n\t\t\tlet forceKillTimer: NodeJS.Timeout | undefined;\n\n\t\t\tconst finish = (code: number): void => {\n\t\t\t\tif (settled) return;\n\t\t\t\tsettled = true;\n\t\t\t\tclearTimeout(timeoutTimer);\n\t\t\t\tclearTimeout(forceKillTimer);\n\t\t\t\toptions.signal?.removeEventListener(\"abort\", abortHandler);\n\t\t\t\tresolve(code);\n\t\t\t};\n\n\t\t\tconst stop = (status: \"aborted\" | \"timeout\" | \"turn_limit\" | \"event_limit\"): void => {\n\t\t\t\tif (stopped) return;\n\t\t\t\tstopped = true;\n\t\t\t\tterminalStatus = status;\n\t\t\t\tkillProcessTree(child.pid, \"SIGTERM\");\n\t\t\t\tforceKillTimer = setTimeout(() => {\n\t\t\t\t\tif (process.platform !== \"win32\") killProcessTree(child.pid, \"SIGKILL\");\n\t\t\t\t\tfinish(1);\n\t\t\t\t}, 5000);\n\t\t\t};\n\n\t\t\tconst emitProgress = (): void => {\n\t\t\t\toptions.onUpdate?.({\n\t\t\t\t\tcontent: [{ type: \"text\", text: \"Delegated work is in progress.\" }],\n\t\t\t\t\tdetails: { status: \"running\", turns },\n\t\t\t\t});\n\t\t\t};\n\n\t\t\tconst processLine = (line: string): void => {\n\t\t\t\tif (!line.trim()) return;\n\t\t\t\tlet parsed: unknown;\n\t\t\t\ttry {\n\t\t\t\t\tparsed = JSON.parse(line);\n\t\t\t\t} catch {\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tif (!isRecord(parsed) || parsed.type !== \"message_end\") return;\n\t\t\t\tconst snapshot = parseAssistantSnapshot(parsed.message);\n\t\t\t\tif (!snapshot) return;\n\t\t\t\tturns += 1;\n\t\t\t\tif (snapshot.text) output = snapshot.text;\n\t\t\t\tif (snapshot.stopReason === \"error\" || snapshot.stopReason === \"aborted\") assistantFailed = true;\n\t\t\t\temitProgress();\n\t\t\t\tif (turns >= MAX_DELEGATED_TURNS) stop(\"turn_limit\");\n\t\t\t};\n\n\t\t\tconst abortHandler = (): void => stop(\"aborted\");\n\t\t\tconst timeoutTimer = setTimeout(\n\t\t\t\t() => stop(\"timeout\"),\n\t\t\t\tMath.min(Math.max(1_000, options.timeoutMs ?? DELEGATED_WORKER_TIMEOUT_MS), DELEGATED_WORKER_TIMEOUT_MS),\n\t\t\t);\n\t\t\tchild.stdout.on(\"data\", (chunk: Buffer | string) => {\n\t\t\t\teventBytes += Buffer.byteLength(chunk);\n\t\t\t\tif (eventBytes > DELEGATED_EVENT_CAP_BYTES) {\n\t\t\t\t\tassistantFailed = true;\n\t\t\t\t\tstop(\"event_limit\");\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tbuffer += chunk.toString();\n\t\t\t\tif (Buffer.byteLength(buffer, \"utf8\") > DELEGATED_EVENT_CAP_BYTES) {\n\t\t\t\t\tbuffer = \"\";\n\t\t\t\t\tassistantFailed = true;\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tconst lines = buffer.split(\"\\n\");\n\t\t\t\tbuffer = lines.pop() ?? \"\";\n\t\t\t\tfor (const line of lines) processLine(line);\n\t\t\t});\n\t\t\t// Consume stderr so a noisy child cannot block. Raw provider/process output\n\t\t\t// is intentionally not retained or exposed to the orchestrator or user.\n\t\t\tchild.stderr.on(\"data\", () => undefined);\n\t\t\tchild.on(\"close\", (code) => {\n\t\t\t\tif (buffer.trim()) processLine(buffer);\n\t\t\t\tfinish(code ?? (terminalStatus ? 1 : 0));\n\t\t\t});\n\t\t\tchild.on(\"error\", () => finish(1));\n\t\t\tif (options.signal?.aborted) abortHandler();\n\t\t\telse options.signal?.addEventListener(\"abort\", abortHandler, { once: true });\n\t\t});\n\n\t\tconst limitedOutput = truncateUtf8(output, DELEGATED_OUTPUT_CAP_BYTES);\n\t\tconst status = terminalStatus ?? (exitCode === 0 && !assistantFailed ? \"completed\" : \"failed\");\n\t\treturn {\n\t\t\tstatus,\n\t\t\tturns,\n\t\t\texitCode,\n\t\t\toutput: limitedOutput.value,\n\t\t\toutputTruncated: limitedOutput.truncated,\n\t\t};\n\t} finally {\n\t\tawait fs.promises.rm(prompt.dir, { recursive: true, force: true });\n\t}\n}\n\nconst DelegateTaskParameters = Type.Object({\n\ttask: Type.String({ description: \"A narrow implementation task with explicit boundaries\", maxLength: 32_768 }),\n\tacceptanceCriteria: Type.Optional(Type.Array(Type.String({ maxLength: 2000 }), { maxItems: 10 })),\n\trelevantPaths: Type.Optional(Type.Array(Type.String({ maxLength: 1000 }), { maxItems: 20 })),\n});\n\nfunction resultContent(result: DelegatedWorkerResult): string {\n\tconst report = result.output.trim() || \"No implementation report was returned.\";\n\tif (result.status === \"completed\") return `Delegated implementation completed.\\n\\n${report}`;\n\treturn `Delegated implementation did not complete (${result.status}).\\n\\nPartial report:\\n${report}`;\n}\n\n/** Register the tool only after the Hub explicitly advertises delegation. */\nexport function registerDelegateTaskTool(pi: ExtensionAPI, capability: DelegationCapability): void {\n\tconst maxTaskBytes = Math.min(Math.max(1, Math.floor(capability.maxTaskBytes)), MAX_DELEGATED_TASK_BYTES);\n\tconst workerTimeoutMs = Math.min(\n\t\tMath.max(1_000, Math.floor(capability.workerTimeoutMs)),\n\t\tDELEGATED_WORKER_TIMEOUT_MS,\n\t);\n\tconst runId = randomUUID();\n\tlet rootRequestId: string | undefined;\n\tlet latestRootRequestId: string | undefined;\n\tlet activeWorkers = 0;\n\tlet invocationCount = 0;\n\tlet rootReviewToolCalls = 0;\n\tpi.on(\"before_agent_start\", (event) => {\n\t\tinvocationCount = 0;\n\t\trootReviewToolCalls = 0;\n\t\treturn {\n\t\t\tsystemPrompt: `${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.`,\n\t\t};\n\t});\n\tpi.on(\"before_provider_headers\", (event, ctx) => {\n\t\tif (!isAutoModel(ctx.model) || isDelegatedWorkerProcess()) return;\n\t\tconst requestId = randomUUID();\n\t\trootRequestId ??= requestId;\n\t\tlatestRootRequestId = requestId;\n\t\tevent.headers[RUN_ID_HEADER] = runId;\n\t\tevent.headers[REQUEST_ID_HEADER] = requestId;\n\t\tevent.headers[ROOT_REQUEST_ID_HEADER] = rootRequestId;\n\t\tevent.headers[REQUEST_SCOPE_HEADER] = \"root\";\n\t});\n\tpi.on(\"tool_call\", (event, ctx) => {\n\t\tif (!isAutoModel(ctx.model) || isDelegatedWorkerProcess() || event.toolName === DELEGATE_TASK_TOOL_NAME) {\n\t\t\treturn;\n\t\t}\n\t\tif (!ROOT_REVIEW_TOOLS.has(event.toolName)) {\n\t\t\treturn {\n\t\t\t\tblock: true,\n\t\t\t\treason:\n\t\t\t\t\t\"This parent run is review-only. Use delegate_task for implementation, mutation, shell commands, tests, or other execution.\",\n\t\t\t};\n\t\t}\n\t\tif (rootReviewToolCalls >= MAX_ROOT_REVIEW_TOOL_CALLS) {\n\t\t\treturn {\n\t\t\t\tblock: true,\n\t\t\t\treason:\n\t\t\t\t\t\"The bounded parent review budget is exhausted. Delegate further investigation or finalize the review.\",\n\t\t\t};\n\t\t}\n\t\trootReviewToolCalls += 1;\n\t});\n\n\tpi.registerTool({\n\t\tname: DELEGATE_TASK_TOOL_NAME,\n\t\tlabel: \"Work\",\n\t\tdescription:\n\t\t\t\"Delegate one narrow implementation task to an isolated internal worker. Keep planning, user communication, and final verification in this conversation.\",\n\t\tpromptGuidelines: [\n\t\t\t\"Use delegate_task for bounded implementation, mutation, command execution, tests, and repeated tool work after you have formed a plan.\",\n\t\t\t\"You remain responsible for reviewing the returned report, verifying the result, and communicating with the user.\",\n\t\t],\n\t\tparameters: DelegateTaskParameters,\n\t\texecutionMode: \"sequential\",\n\t\tasync execute(_toolCallId, params, signal, onUpdate, ctx) {\n\t\t\tif (!isAutoModel(ctx.model)) {\n\t\t\t\treturn {\n\t\t\t\t\tcontent: [{ type: \"text\", text: \"Delegated work is unavailable for the active mode.\" }],\n\t\t\t\t\tdetails: { status: \"failed\", turns: 0 } satisfies DelegationDetails,\n\t\t\t\t};\n\t\t\t}\n\t\t\tif (!ctx.isProjectTrusted()) {\n\t\t\t\treturn {\n\t\t\t\t\tcontent: [{ type: \"text\", text: \"Delegated work requires a trusted project.\" }],\n\t\t\t\t\tdetails: { status: \"failed\", turns: 0 } satisfies DelegationDetails,\n\t\t\t\t};\n\t\t\t}\n\t\t\tif (activeWorkers >= MAX_PARALLEL_MUTATING_WORKERS || invocationCount >= MAX_DELEGATIONS_PER_RUN) {\n\t\t\t\treturn {\n\t\t\t\t\tcontent: [{ type: \"text\", text: \"Delegated work is temporarily unavailable for this run.\" }],\n\t\t\t\t\tdetails: { status: \"failed\", turns: 0 } satisfies DelegationDetails,\n\t\t\t\t};\n\t\t\t}\n\n\t\t\tconst request: DelegationRequest = {\n\t\t\t\ttask: params.task,\n\t\t\t\tacceptanceCriteria: params.acceptanceCriteria ?? [],\n\t\t\t\trelevantPaths: params.relevantPaths ?? [],\n\t\t\t\tcwd: ctx.cwd,\n\t\t\t\tsignal,\n\t\t\t};\n\t\t\tif (\n\t\t\t\tBuffer.byteLength(\n\t\t\t\t\tcomposeDelegatedTask(request.task, request.acceptanceCriteria, request.relevantPaths),\n\t\t\t\t\t\"utf8\",\n\t\t\t\t) > maxTaskBytes\n\t\t\t) {\n\t\t\t\treturn {\n\t\t\t\t\tcontent: [{ type: \"text\", text: \"Delegated work is too large for one bounded task.\" }],\n\t\t\t\t\tdetails: { status: \"failed\", turns: 0 } satisfies DelegationDetails,\n\t\t\t\t};\n\t\t\t}\n\n\t\t\tactiveWorkers += 1;\n\t\t\tinvocationCount += 1;\n\t\t\ttry {\n\t\t\t\tconst delegationId = randomUUID();\n\t\t\t\trootRequestId ??= randomUUID();\n\t\t\t\tconst result = await runDelegatedWorker({\n\t\t\t\t\t...request,\n\t\t\t\t\tworkerContext: {\n\t\t\t\t\t\tsessionId: ctx.sessionManager.getSessionId(),\n\t\t\t\t\t\ttaskId: delegationId,\n\t\t\t\t\t\trunId,\n\t\t\t\t\t\trootRequestId,\n\t\t\t\t\t\tparentRequestId: latestRootRequestId ?? rootRequestId,\n\t\t\t\t\t},\n\t\t\t\t\ttimeoutMs: workerTimeoutMs,\n\t\t\t\t\tonUpdate,\n\t\t\t\t});\n\t\t\t\treturn {\n\t\t\t\t\tcontent: [{ type: \"text\", text: resultContent(result) }],\n\t\t\t\t\tdetails: {\n\t\t\t\t\t\tstatus: result.status,\n\t\t\t\t\t\tturns: result.turns,\n\t\t\t\t\t\texitCode: result.exitCode,\n\t\t\t\t\t\toutputTruncated: result.outputTruncated,\n\t\t\t\t\t} satisfies DelegationDetails,\n\t\t\t\t};\n\t\t\t} catch {\n\t\t\t\treturn {\n\t\t\t\t\tcontent: [{ type: \"text\", text: \"Delegated work could not be started.\" }],\n\t\t\t\t\tdetails: { status: \"failed\", turns: 0 } satisfies DelegationDetails,\n\t\t\t\t};\n\t\t\t} finally {\n\t\t\t\tactiveWorkers -= 1;\n\t\t\t}\n\t\t},\n\t\trenderCall(_args, theme) {\n\t\t\treturn new Text(theme.fg(\"toolTitle\", theme.bold(\"Working…\")), 0, 0);\n\t\t},\n\t\trenderResult(result, _options, theme) {\n\t\t\tconst details = result.details as DelegationDetails | undefined;\n\t\t\tif (details?.status === \"running\") return new Text(theme.fg(\"muted\", \"Working…\"), 0, 0);\n\t\t\tif (details?.status === \"completed\") return new Text(theme.fg(\"success\", \"Work completed\"), 0, 0);\n\t\t\treturn new Text(theme.fg(\"error\", \"Work needs attention\"), 0, 0);\n\t\t},\n\t});\n}\n\n/**\n * Install a fail-closed header hook for one worker task. A fresh one-time grant\n * is minted for each model turn; no grant is inherited by worker tools.\n */\nexport function registerWorkerGrantHeaders(\n\tpi: ExtensionAPI,\n\tworkerContext: WorkerContext,\n\tcontroller: DelegationController | undefined,\n): void {\n\tpi.on(\"before_provider_headers\", async (event, ctx) => {\n\t\tif (!isAutoModel(ctx.model)) {\n\t\t\tctx.abort();\n\t\t\treturn;\n\t\t}\n\n\t\t// Set an invalid sentinel first. Extension handler errors are reported and\n\t\t// swallowed by the runner, so this prevents a mint failure from silently\n\t\t// falling back to an ungranted root request.\n\t\tconst requestId = randomUUID();\n\t\tevent.headers[INTERNAL_WORKER_GRANT_HEADER] = INVALID_WORKER_GRANT;\n\t\tevent.headers[INTERNAL_WORKER_TASK_HEADER] = workerContext.taskId;\n\t\tevent.headers[SESSION_AFFINITY_HEADER] = workerContext.sessionId;\n\t\tevent.headers[RUN_ID_HEADER] = workerContext.runId;\n\t\tevent.headers[REQUEST_ID_HEADER] = requestId;\n\t\tevent.headers[ROOT_REQUEST_ID_HEADER] = workerContext.rootRequestId;\n\t\tevent.headers[PARENT_REQUEST_ID_HEADER] = workerContext.parentRequestId;\n\t\tevent.headers[DELEGATION_ID_HEADER] = workerContext.taskId;\n\t\tevent.headers[REQUEST_SCOPE_HEADER] = \"worker\";\n\t\tif (!controller) return;\n\n\t\tconst grant = await controller.createWorkerGrant({\n\t\t\t...workerContext,\n\t\t\trequestId,\n\t\t\tsignal: ctx.signal,\n\t\t});\n\t\tevent.headers[INTERNAL_WORKER_GRANT_HEADER] = grant.token;\n\t});\n}\n\nexport default function delegatedWorkerRuntime(pi: ExtensionAPI): void {\n\tconst isWorker = isDelegatedWorkerProcess();\n\tconst workerContext = consumeWorkerContext();\n\tif (!isWorker) return;\n\tconst config = resolveGitNexusConfig();\n\tconst failClosedContext: WorkerContext = {\n\t\tsessionId: \"unavailable\",\n\t\ttaskId: \"unavailable\",\n\t\trunId: \"unavailable\",\n\t\trootRequestId: \"unavailable\",\n\t\tparentRequestId: \"unavailable\",\n\t};\n\tregisterWorkerGrantHeaders(\n\t\tpi,\n\t\tworkerContext ?? failClosedContext,\n\t\tworkerContext && config ? createHubDelegationController(config) : undefined,\n\t);\n}\n"]}
|
|
@@ -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
|