diff --git a/src/agent/loop.ts b/src/agent/loop.ts index d9ecfd20..5bd3f522 100644 --- a/src/agent/loop.ts +++ b/src/agent/loop.ts @@ -210,11 +210,27 @@ export async function runAgentLoop( // Local worker pool: runs inference-driven agents in-process // as async tasks. Falls back from Conway sandbox spawning. + // Adapt parent tools for worker use. Workers get the same custom tools + // as the orchestrator so they can perform outreach, publish to GitHub, + // send emails, call external APIs, and perform any action that requires + // the parent's credentials/context. + const workerCustomTools = tools + .filter((t) => !["create_goal", "list_goals", "get_plan", "cancel_goal", + "orchestrator_status", "sleep", "modify_heartbeat", + "review_upstream_changes", "pull_upstream"].includes(t.name)) + .map((t) => ({ + name: t.name, + description: t.description, + parameters: t.parameters, + execute: async (args: Record) => t.execute(args, toolContext), + })); + const workerPool = new LocalWorkerPool({ db: db.raw, inference: workerInference, conway, workerId: `pool-${identity.name}`, + customTools: workerCustomTools, }); orchestrator = new Orchestrator({ diff --git a/src/orchestration/local-worker.ts b/src/orchestration/local-worker.ts index 1111454f..5dc0004b 100644 --- a/src/orchestration/local-worker.ts +++ b/src/orchestration/local-worker.ts @@ -72,6 +72,14 @@ interface LocalWorkerConfig { conway: ConwayClient; workerId: string; maxTurns?: number; + /** + * Additional tools from the parent agent that workers can use. + * These are merged with the built-in worker tools (exec, read_file, + * write_file, task_done) to give workers access to the parent's + * custom capabilities — e.g. GitHub, email, web search, or external + * API integrations like Obol (https://obolagents.com). + */ + customTools?: WorkerTool[]; } interface WorkerToolResult { @@ -115,6 +123,13 @@ export class LocalWorkerPool { }) .finally(() => { this.activeWorkers.delete(workerId); + // Mark child as terminated in DB so the orchestrator doesn't + // re-assign new tasks to this dead worker address. + try { + this.config.db.prepare( + "UPDATE children SET status = 'terminated' WHERE address = ?", + ).run(address); + } catch { /* ignore DB errors during cleanup */ } }); this.activeWorkers.set(workerId, { promise: workerPromise, taskId: task.id, abortController }); @@ -281,6 +296,11 @@ export class LocalWorkerPool { private buildWorkerSystemPrompt(task: TaskNode): string { const role = task.agentRole ?? "generalist"; + const customToolNames = (this.config.customTools ?? []).map(t => t.name); + const customToolSection = customToolNames.length > 0 + ? `\n- You also have custom tools from the parent agent: ${customToolNames.join(", ")}. Use them when the task requires external actions (API calls, outreach, publishing).` + : ""; + return `You are a worker agent with the role: ${role}. You have been assigned a specific task by the parent orchestrator. Your job is to @@ -290,7 +310,7 @@ RULES: - Focus ONLY on the assigned task. Do not deviate. - Use exec to run shell commands (install packages, run scripts, etc.) - Use write_file to create or modify files. -- Use read_file to inspect existing files. +- Use read_file to inspect existing files.${customToolSection} - When done, provide a clear summary of what you accomplished as your final message. - If you cannot complete the task, explain why in your final message. - Do NOT call tools after you are done. Just give your final text response. @@ -317,7 +337,7 @@ RULES: } private buildWorkerTools(): WorkerTool[] { - return [ + const builtinTools: WorkerTool[] = [ { name: "exec", description: "Execute a shell command and return stdout/stderr. Use for installing packages, running scripts, building code, etc.", @@ -418,5 +438,13 @@ RULES: }, }, ]; + + // Merge parent-provided custom tools so workers can use the same + // integrations as the orchestrator (GitHub, email, web APIs, etc.) + const custom = this.config.customTools ?? []; + if (custom.length > 0) { + logger.info(`Worker has ${custom.length} custom tool(s) from parent: ${custom.map(t => t.name).join(", ")}`); + } + return [...builtinTools, ...custom]; } } diff --git a/src/orchestration/orchestrator.ts b/src/orchestration/orchestrator.ts index eae6cc56..ca2dbf1c 100644 --- a/src/orchestration/orchestrator.ts +++ b/src/orchestration/orchestrator.ts @@ -531,6 +531,10 @@ export class Orchestrator { this.params.db.prepare( "UPDATE task_graph SET status = 'pending', assigned_to = NULL, started_at = NULL WHERE id = ?", ).run(task.id); + // Also terminate the dead child to prevent re-assignment + this.params.db.prepare( + "UPDATE children SET status = 'terminated' WHERE address = ?", + ).run(task.assignedTo); } } }