@phren/agent 0.0.1
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-loop.js +328 -0
- package/dist/bin.js +3 -0
- package/dist/checkpoint.js +103 -0
- package/dist/commands.js +292 -0
- package/dist/config.js +139 -0
- package/dist/context/pruner.js +62 -0
- package/dist/context/token-counter.js +28 -0
- package/dist/cost.js +71 -0
- package/dist/index.js +284 -0
- package/dist/mcp-client.js +168 -0
- package/dist/memory/anti-patterns.js +69 -0
- package/dist/memory/auto-capture.js +72 -0
- package/dist/memory/context-flush.js +24 -0
- package/dist/memory/context.js +170 -0
- package/dist/memory/error-recovery.js +58 -0
- package/dist/memory/project-context.js +77 -0
- package/dist/memory/session.js +100 -0
- package/dist/multi/agent-colors.js +41 -0
- package/dist/multi/child-entry.js +173 -0
- package/dist/multi/coordinator.js +263 -0
- package/dist/multi/diff-renderer.js +175 -0
- package/dist/multi/markdown.js +96 -0
- package/dist/multi/presets.js +107 -0
- package/dist/multi/progress.js +32 -0
- package/dist/multi/spawner.js +219 -0
- package/dist/multi/tui-multi.js +626 -0
- package/dist/multi/types.js +7 -0
- package/dist/permissions/allowlist.js +61 -0
- package/dist/permissions/checker.js +111 -0
- package/dist/permissions/prompt.js +190 -0
- package/dist/permissions/sandbox.js +95 -0
- package/dist/permissions/shell-safety.js +74 -0
- package/dist/permissions/types.js +2 -0
- package/dist/plan.js +38 -0
- package/dist/providers/anthropic.js +170 -0
- package/dist/providers/codex-auth.js +197 -0
- package/dist/providers/codex.js +265 -0
- package/dist/providers/ollama.js +142 -0
- package/dist/providers/openai-compat.js +163 -0
- package/dist/providers/openrouter.js +116 -0
- package/dist/providers/resolve.js +39 -0
- package/dist/providers/retry.js +55 -0
- package/dist/providers/types.js +2 -0
- package/dist/repl.js +180 -0
- package/dist/spinner.js +46 -0
- package/dist/system-prompt.js +31 -0
- package/dist/tools/edit-file.js +31 -0
- package/dist/tools/git.js +98 -0
- package/dist/tools/glob.js +65 -0
- package/dist/tools/grep.js +108 -0
- package/dist/tools/lint-test.js +76 -0
- package/dist/tools/phren-finding.js +35 -0
- package/dist/tools/phren-search.js +44 -0
- package/dist/tools/phren-tasks.js +71 -0
- package/dist/tools/read-file.js +44 -0
- package/dist/tools/registry.js +46 -0
- package/dist/tools/shell.js +48 -0
- package/dist/tools/types.js +2 -0
- package/dist/tools/write-file.js +27 -0
- package/dist/tui.js +451 -0
- package/package.json +39 -0
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent spawner — forks child agent processes and manages their lifecycle.
|
|
3
|
+
*
|
|
4
|
+
* Usage:
|
|
5
|
+
* const spawner = new AgentSpawner();
|
|
6
|
+
* const id = spawner.spawn({ task: "fix the bug", ... });
|
|
7
|
+
* spawner.on("done", (agentId, result) => { ... });
|
|
8
|
+
* spawner.cancel(id);
|
|
9
|
+
* await spawner.shutdown();
|
|
10
|
+
*/
|
|
11
|
+
import { fork } from "node:child_process";
|
|
12
|
+
import { fileURLToPath } from "node:url";
|
|
13
|
+
import { dirname, join } from "node:path";
|
|
14
|
+
import { EventEmitter } from "node:events";
|
|
15
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
16
|
+
const __dirname = dirname(__filename);
|
|
17
|
+
/** Resolved path to the child-entry module. */
|
|
18
|
+
const CHILD_ENTRY = join(__dirname, "child-entry.js");
|
|
19
|
+
/** Keys forwarded from the parent env into child processes. */
|
|
20
|
+
const ENV_FORWARD_KEYS = [
|
|
21
|
+
"OPENROUTER_API_KEY",
|
|
22
|
+
"ANTHROPIC_API_KEY",
|
|
23
|
+
"OPENAI_API_KEY",
|
|
24
|
+
"PHREN_AGENT_PROVIDER",
|
|
25
|
+
"PHREN_AGENT_MODEL",
|
|
26
|
+
"PHREN_OLLAMA_URL",
|
|
27
|
+
"PHREN_PATH",
|
|
28
|
+
"PHREN_PROFILE",
|
|
29
|
+
"PHREN_DEBUG",
|
|
30
|
+
"HOME",
|
|
31
|
+
"PATH",
|
|
32
|
+
"NODE_EXTRA_CA_CERTS",
|
|
33
|
+
];
|
|
34
|
+
export class AgentSpawner extends EventEmitter {
|
|
35
|
+
agents = new Map();
|
|
36
|
+
processes = new Map();
|
|
37
|
+
nextId = 1;
|
|
38
|
+
/** Spawn a new child agent. Returns the agent ID. */
|
|
39
|
+
spawn(opts) {
|
|
40
|
+
const agentId = `agent-${this.nextId++}`;
|
|
41
|
+
// Build forwarded env
|
|
42
|
+
const childEnv = {};
|
|
43
|
+
for (const key of ENV_FORWARD_KEYS) {
|
|
44
|
+
if (process.env[key])
|
|
45
|
+
childEnv[key] = process.env[key];
|
|
46
|
+
}
|
|
47
|
+
if (opts.env)
|
|
48
|
+
Object.assign(childEnv, opts.env);
|
|
49
|
+
const payload = {
|
|
50
|
+
type: "spawn",
|
|
51
|
+
agentId,
|
|
52
|
+
task: opts.task,
|
|
53
|
+
cwd: opts.cwd ?? process.cwd(),
|
|
54
|
+
provider: opts.provider,
|
|
55
|
+
model: opts.model,
|
|
56
|
+
project: opts.project,
|
|
57
|
+
permissions: opts.permissions ?? "auto-confirm",
|
|
58
|
+
maxTurns: opts.maxTurns ?? 50,
|
|
59
|
+
budget: opts.budget ?? null,
|
|
60
|
+
plan: opts.plan ?? false,
|
|
61
|
+
verbose: opts.verbose ?? false,
|
|
62
|
+
env: childEnv,
|
|
63
|
+
};
|
|
64
|
+
const entry = {
|
|
65
|
+
id: agentId,
|
|
66
|
+
task: opts.task,
|
|
67
|
+
status: "starting",
|
|
68
|
+
startedAt: Date.now(),
|
|
69
|
+
};
|
|
70
|
+
this.agents.set(agentId, entry);
|
|
71
|
+
// Fork the child process
|
|
72
|
+
const child = fork(CHILD_ENTRY, [], {
|
|
73
|
+
stdio: ["pipe", "pipe", "pipe", "ipc"],
|
|
74
|
+
env: { ...childEnv, FORCE_COLOR: "0" },
|
|
75
|
+
cwd: opts.cwd ?? process.cwd(),
|
|
76
|
+
});
|
|
77
|
+
this.processes.set(agentId, child);
|
|
78
|
+
entry.pid = child.pid;
|
|
79
|
+
entry.status = "running";
|
|
80
|
+
// Send the spawn payload
|
|
81
|
+
child.send(payload);
|
|
82
|
+
// Handle IPC messages from child
|
|
83
|
+
child.on("message", (msg) => {
|
|
84
|
+
this.handleChildMessage(msg);
|
|
85
|
+
});
|
|
86
|
+
// Handle child exit
|
|
87
|
+
child.on("exit", (code) => {
|
|
88
|
+
const agent = this.agents.get(agentId);
|
|
89
|
+
if (agent && agent.status === "running") {
|
|
90
|
+
agent.status = code === 0 ? "done" : "error";
|
|
91
|
+
agent.finishedAt = Date.now();
|
|
92
|
+
if (code !== 0 && !agent.error) {
|
|
93
|
+
agent.error = `Process exited with code ${code}`;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
this.processes.delete(agentId);
|
|
97
|
+
this.emit("exit", agentId, code);
|
|
98
|
+
});
|
|
99
|
+
// Capture stderr for error reporting
|
|
100
|
+
child.stderr?.on("data", (data) => {
|
|
101
|
+
const text = data.toString();
|
|
102
|
+
if (opts.verbose) {
|
|
103
|
+
this.emit("status", agentId, text);
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
return agentId;
|
|
107
|
+
}
|
|
108
|
+
handleChildMessage(msg) {
|
|
109
|
+
// DirectMessageEvent has from/to instead of agentId — handle before accessing msg.agentId
|
|
110
|
+
if (msg.type === "direct_message") {
|
|
111
|
+
this.routeDirectMessage(msg.from, msg.to, msg.content);
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
const agent = this.agents.get(msg.agentId);
|
|
115
|
+
switch (msg.type) {
|
|
116
|
+
case "text_delta":
|
|
117
|
+
this.emit("text_delta", msg.agentId, msg.text);
|
|
118
|
+
break;
|
|
119
|
+
case "text_block":
|
|
120
|
+
this.emit("text_block", msg.agentId, msg.text);
|
|
121
|
+
break;
|
|
122
|
+
case "tool_start":
|
|
123
|
+
this.emit("tool_start", msg.agentId, msg.toolName, msg.input, msg.count);
|
|
124
|
+
break;
|
|
125
|
+
case "tool_end":
|
|
126
|
+
this.emit("tool_end", msg.agentId, msg.toolName, msg.input, msg.output, msg.isError, msg.durationMs);
|
|
127
|
+
break;
|
|
128
|
+
case "status":
|
|
129
|
+
this.emit("status", msg.agentId, msg.message);
|
|
130
|
+
break;
|
|
131
|
+
case "done":
|
|
132
|
+
if (agent) {
|
|
133
|
+
agent.status = "done";
|
|
134
|
+
agent.finishedAt = Date.now();
|
|
135
|
+
agent.result = msg.result;
|
|
136
|
+
}
|
|
137
|
+
this.emit("done", msg.agentId, msg.result);
|
|
138
|
+
break;
|
|
139
|
+
case "error":
|
|
140
|
+
if (agent) {
|
|
141
|
+
agent.status = "error";
|
|
142
|
+
agent.finishedAt = Date.now();
|
|
143
|
+
agent.error = msg.error;
|
|
144
|
+
}
|
|
145
|
+
this.emit("error", msg.agentId, msg.error);
|
|
146
|
+
break;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
/** Route a direct message from one agent to another. */
|
|
150
|
+
routeDirectMessage(from, to, content) {
|
|
151
|
+
this.emit("message", from, to, content);
|
|
152
|
+
this.sendToAgent(to, content, from);
|
|
153
|
+
}
|
|
154
|
+
/** Cancel a running agent. */
|
|
155
|
+
cancel(agentId) {
|
|
156
|
+
const child = this.processes.get(agentId);
|
|
157
|
+
if (!child)
|
|
158
|
+
return false;
|
|
159
|
+
child.send({ type: "cancel", agentId, reason: "Cancelled by parent" });
|
|
160
|
+
// Give it a moment to clean up, then force kill
|
|
161
|
+
setTimeout(() => {
|
|
162
|
+
if (this.processes.has(agentId)) {
|
|
163
|
+
child.kill("SIGTERM");
|
|
164
|
+
}
|
|
165
|
+
}, 5000);
|
|
166
|
+
const agent = this.agents.get(agentId);
|
|
167
|
+
if (agent) {
|
|
168
|
+
agent.status = "cancelled";
|
|
169
|
+
agent.finishedAt = Date.now();
|
|
170
|
+
}
|
|
171
|
+
return true;
|
|
172
|
+
}
|
|
173
|
+
/** Send a direct message to a child agent via IPC. Returns true if delivered. */
|
|
174
|
+
sendToAgent(agentId, message, from) {
|
|
175
|
+
const child = this.processes.get(agentId);
|
|
176
|
+
if (!child)
|
|
177
|
+
return false;
|
|
178
|
+
child.send({ type: "deliver_message", from: from ?? "user", content: message });
|
|
179
|
+
return true;
|
|
180
|
+
}
|
|
181
|
+
/** Get the current state of an agent. */
|
|
182
|
+
getAgent(agentId) {
|
|
183
|
+
return this.agents.get(agentId);
|
|
184
|
+
}
|
|
185
|
+
/** List all agents. */
|
|
186
|
+
listAgents() {
|
|
187
|
+
return [...this.agents.values()];
|
|
188
|
+
}
|
|
189
|
+
/** Get agents by status. */
|
|
190
|
+
getAgentsByStatus(status) {
|
|
191
|
+
return [...this.agents.values()].filter((a) => a.status === status);
|
|
192
|
+
}
|
|
193
|
+
/** Shut down all running agents and wait for exit. */
|
|
194
|
+
async shutdown() {
|
|
195
|
+
const running = this.getAgentsByStatus("running");
|
|
196
|
+
for (const agent of running) {
|
|
197
|
+
this.cancel(agent.id);
|
|
198
|
+
}
|
|
199
|
+
// Wait for all processes to exit (max 10s)
|
|
200
|
+
if (this.processes.size > 0) {
|
|
201
|
+
await new Promise((resolve) => {
|
|
202
|
+
const check = () => {
|
|
203
|
+
if (this.processes.size === 0)
|
|
204
|
+
return resolve();
|
|
205
|
+
setTimeout(check, 100);
|
|
206
|
+
};
|
|
207
|
+
setTimeout(() => {
|
|
208
|
+
// Force kill remaining
|
|
209
|
+
for (const [id, child] of this.processes) {
|
|
210
|
+
child.kill("SIGKILL");
|
|
211
|
+
this.processes.delete(id);
|
|
212
|
+
}
|
|
213
|
+
resolve();
|
|
214
|
+
}, 10_000);
|
|
215
|
+
check();
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|