multiagents 0.1.0
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/.mcp.json +12 -0
- package/README.md +184 -0
- package/adapters/base-adapter.ts +1493 -0
- package/adapters/claude-adapter.ts +66 -0
- package/adapters/codex-adapter.ts +135 -0
- package/adapters/gemini-adapter.ts +129 -0
- package/broker.ts +1263 -0
- package/cli/commands.ts +194 -0
- package/cli/dashboard.ts +988 -0
- package/cli/session.ts +278 -0
- package/cli/setup.ts +257 -0
- package/cli.ts +17 -0
- package/index.ts +41 -0
- package/noop-mcp.ts +63 -0
- package/orchestrator/guardrails.ts +243 -0
- package/orchestrator/launcher.ts +433 -0
- package/orchestrator/monitor.ts +285 -0
- package/orchestrator/orchestrator-server.ts +1000 -0
- package/orchestrator/progress.ts +214 -0
- package/orchestrator/recovery.ts +176 -0
- package/orchestrator/session-control.ts +343 -0
- package/package.json +70 -0
- package/scripts/postinstall.ts +84 -0
- package/scripts/version.ts +62 -0
- package/server.ts +52 -0
- package/shared/broker-client.ts +243 -0
- package/shared/constants.ts +148 -0
- package/shared/summarize.ts +97 -0
- package/shared/types.ts +419 -0
- package/shared/utils.ts +121 -0
- package/tsconfig.json +29 -0
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// multiagents — Process Monitor
|
|
3
|
+
// ============================================================================
|
|
4
|
+
// Monitors agent process lifecycle: stdout parsing, status updates, crash
|
|
5
|
+
// detection.
|
|
6
|
+
// ============================================================================
|
|
7
|
+
|
|
8
|
+
import type { Subprocess } from "bun";
|
|
9
|
+
import type { BrokerClient } from "../shared/broker-client.ts";
|
|
10
|
+
import { log } from "../shared/utils.ts";
|
|
11
|
+
|
|
12
|
+
const LOG_PREFIX = "monitor";
|
|
13
|
+
|
|
14
|
+
// Track last-seen cumulative tokens per slot (Codex sends cumulative totals)
|
|
15
|
+
const lastTokenTotals = new Map<number, { input: number; output: number; cacheRead: number }>();
|
|
16
|
+
|
|
17
|
+
/** Update token usage for a slot — handles both delta and cumulative formats. */
|
|
18
|
+
async function updateTokenUsage(
|
|
19
|
+
slotId: number,
|
|
20
|
+
tokens: { input: number; output: number; cacheRead: number },
|
|
21
|
+
brokerClient: BrokerClient,
|
|
22
|
+
): Promise<void> {
|
|
23
|
+
// Codex sends cumulative totals; Claude sends per-result totals.
|
|
24
|
+
// For Codex, we compute the delta from the last seen value.
|
|
25
|
+
// For Claude, each "result" contains the full session usage, so we also delta.
|
|
26
|
+
const last = lastTokenTotals.get(slotId) ?? { input: 0, output: 0, cacheRead: 0 };
|
|
27
|
+
const deltaInput = Math.max(0, tokens.input - last.input);
|
|
28
|
+
const deltaOutput = Math.max(0, tokens.output - last.output);
|
|
29
|
+
const deltaCacheRead = Math.max(0, tokens.cacheRead - last.cacheRead);
|
|
30
|
+
|
|
31
|
+
lastTokenTotals.set(slotId, tokens);
|
|
32
|
+
|
|
33
|
+
if (deltaInput === 0 && deltaOutput === 0 && deltaCacheRead === 0) return;
|
|
34
|
+
|
|
35
|
+
try {
|
|
36
|
+
await brokerClient.updateSlot({
|
|
37
|
+
id: slotId,
|
|
38
|
+
input_tokens: deltaInput,
|
|
39
|
+
output_tokens: deltaOutput,
|
|
40
|
+
cache_read_tokens: deltaCacheRead,
|
|
41
|
+
});
|
|
42
|
+
} catch {
|
|
43
|
+
// Best-effort token tracking
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Event emitted by the process monitor. */
|
|
48
|
+
export interface AgentEvent {
|
|
49
|
+
type: string;
|
|
50
|
+
severity: "info" | "warning" | "critical";
|
|
51
|
+
slotId: number;
|
|
52
|
+
sessionId: string;
|
|
53
|
+
message: string;
|
|
54
|
+
data?: Record<string, unknown>;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Monitor an agent subprocess: read stdout for progress signals,
|
|
59
|
+
* update slot status in the broker, and fire events on exit.
|
|
60
|
+
*
|
|
61
|
+
* This function is non-blocking — it starts async readers and returns
|
|
62
|
+
* immediately.
|
|
63
|
+
*/
|
|
64
|
+
export function monitorProcess(
|
|
65
|
+
proc: Subprocess,
|
|
66
|
+
slotId: number,
|
|
67
|
+
sessionId: string,
|
|
68
|
+
brokerClient: BrokerClient,
|
|
69
|
+
onEvent: (event: AgentEvent) => void,
|
|
70
|
+
): void {
|
|
71
|
+
// Read stdout for JSON progress lines
|
|
72
|
+
if (proc.stdout) {
|
|
73
|
+
readStream(proc.stdout, slotId, sessionId, brokerClient, onEvent);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Read stderr for error output
|
|
77
|
+
if (proc.stderr) {
|
|
78
|
+
readStderr(proc.stderr, slotId, sessionId, onEvent);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Monitor process exit
|
|
82
|
+
proc.exited.then((exitCode) => {
|
|
83
|
+
handleExit(exitCode, slotId, sessionId, brokerClient, onEvent);
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Read stdout stream, parse JSON lines for progress signals. */
|
|
88
|
+
async function readStream(
|
|
89
|
+
stdout: ReadableStream<Uint8Array>,
|
|
90
|
+
slotId: number,
|
|
91
|
+
sessionId: string,
|
|
92
|
+
brokerClient: BrokerClient,
|
|
93
|
+
onEvent: (event: AgentEvent) => void,
|
|
94
|
+
): Promise<void> {
|
|
95
|
+
const decoder = new TextDecoder();
|
|
96
|
+
let buffer = "";
|
|
97
|
+
|
|
98
|
+
try {
|
|
99
|
+
const reader = stdout.getReader();
|
|
100
|
+
while (true) {
|
|
101
|
+
const { done, value } = await reader.read();
|
|
102
|
+
if (done) break;
|
|
103
|
+
|
|
104
|
+
buffer += decoder.decode(value, { stream: true });
|
|
105
|
+
const lines = buffer.split("\n");
|
|
106
|
+
buffer = lines.pop() ?? "";
|
|
107
|
+
|
|
108
|
+
for (const line of lines) {
|
|
109
|
+
const trimmed = line.trim();
|
|
110
|
+
if (!trimmed) continue;
|
|
111
|
+
await processLine(trimmed, slotId, sessionId, brokerClient, onEvent);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
} catch (err) {
|
|
115
|
+
log(LOG_PREFIX, `stdout reader error for slot ${slotId}: ${err}`);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** Process a single stdout line, attempting JSON parse for structured signals. */
|
|
120
|
+
async function processLine(
|
|
121
|
+
line: string,
|
|
122
|
+
slotId: number,
|
|
123
|
+
sessionId: string,
|
|
124
|
+
brokerClient: BrokerClient,
|
|
125
|
+
onEvent: (event: AgentEvent) => void,
|
|
126
|
+
): Promise<void> {
|
|
127
|
+
// Try parsing as JSON (Claude stream-json format)
|
|
128
|
+
try {
|
|
129
|
+
const parsed = JSON.parse(line);
|
|
130
|
+
|
|
131
|
+
// Claude stream-json result message (includes final token usage)
|
|
132
|
+
if (parsed.type === "result" && parsed.result) {
|
|
133
|
+
onEvent({
|
|
134
|
+
type: "agent_output",
|
|
135
|
+
severity: "info",
|
|
136
|
+
slotId,
|
|
137
|
+
sessionId,
|
|
138
|
+
message: `Agent produced result`,
|
|
139
|
+
data: { result: parsed.result },
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
// Extract token usage from Claude result
|
|
143
|
+
const usage = parsed.result?.usage;
|
|
144
|
+
if (usage) {
|
|
145
|
+
await updateTokenUsage(slotId, {
|
|
146
|
+
input: usage.input_tokens ?? 0,
|
|
147
|
+
output: usage.output_tokens ?? 0,
|
|
148
|
+
cacheRead: usage.cache_read_input_tokens ?? usage.cache_creation_input_tokens ?? 0,
|
|
149
|
+
}, brokerClient);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Codex token_count message
|
|
154
|
+
if (parsed.msg?.type === "token_count" && parsed.msg?.info?.total_token_usage) {
|
|
155
|
+
const tu = parsed.msg.info.total_token_usage;
|
|
156
|
+
await updateTokenUsage(slotId, {
|
|
157
|
+
input: tu.input_tokens ?? 0,
|
|
158
|
+
output: tu.output_tokens ?? 0,
|
|
159
|
+
cacheRead: tu.cached_input_tokens ?? 0,
|
|
160
|
+
}, brokerClient);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Claude stream-json assistant message with content
|
|
164
|
+
if (parsed.type === "assistant" && parsed.message?.content) {
|
|
165
|
+
// Update the slot's context snapshot with latest output
|
|
166
|
+
try {
|
|
167
|
+
const contentText = Array.isArray(parsed.message.content)
|
|
168
|
+
? parsed.message.content
|
|
169
|
+
.filter((c: any) => c.type === "text")
|
|
170
|
+
.map((c: any) => c.text)
|
|
171
|
+
.join("")
|
|
172
|
+
: String(parsed.message.content);
|
|
173
|
+
|
|
174
|
+
const summary = contentText.slice(0, 200);
|
|
175
|
+
await brokerClient.updateSlot({
|
|
176
|
+
id: slotId,
|
|
177
|
+
context_snapshot: JSON.stringify({
|
|
178
|
+
last_summary: summary,
|
|
179
|
+
last_status: "working",
|
|
180
|
+
updated_at: Date.now(),
|
|
181
|
+
}),
|
|
182
|
+
});
|
|
183
|
+
} catch {
|
|
184
|
+
// Best-effort snapshot update
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Tool use signals progress
|
|
189
|
+
if (parsed.type === "assistant" && parsed.message?.stop_reason === "tool_use") {
|
|
190
|
+
onEvent({
|
|
191
|
+
type: "agent_progress",
|
|
192
|
+
severity: "info",
|
|
193
|
+
slotId,
|
|
194
|
+
sessionId,
|
|
195
|
+
message: "Agent is using tools",
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
return;
|
|
200
|
+
} catch {
|
|
201
|
+
// Not JSON — treat as plain text output, ignore
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/** Read stderr for error messages. */
|
|
206
|
+
async function readStderr(
|
|
207
|
+
stderr: ReadableStream<Uint8Array>,
|
|
208
|
+
slotId: number,
|
|
209
|
+
sessionId: string,
|
|
210
|
+
onEvent: (event: AgentEvent) => void,
|
|
211
|
+
): Promise<void> {
|
|
212
|
+
const decoder = new TextDecoder();
|
|
213
|
+
let buffer = "";
|
|
214
|
+
|
|
215
|
+
try {
|
|
216
|
+
const reader = stderr.getReader();
|
|
217
|
+
while (true) {
|
|
218
|
+
const { done, value } = await reader.read();
|
|
219
|
+
if (done) break;
|
|
220
|
+
|
|
221
|
+
buffer += decoder.decode(value, { stream: true });
|
|
222
|
+
const lines = buffer.split("\n");
|
|
223
|
+
buffer = lines.pop() ?? "";
|
|
224
|
+
|
|
225
|
+
for (const line of lines) {
|
|
226
|
+
const trimmed = line.trim();
|
|
227
|
+
if (!trimmed) continue;
|
|
228
|
+
|
|
229
|
+
// Check for error patterns
|
|
230
|
+
if (/error|fatal|panic|exception/i.test(trimmed)) {
|
|
231
|
+
onEvent({
|
|
232
|
+
type: "agent_error",
|
|
233
|
+
severity: "warning",
|
|
234
|
+
slotId,
|
|
235
|
+
sessionId,
|
|
236
|
+
message: `Agent stderr: ${trimmed.slice(0, 200)}`,
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
} catch (err) {
|
|
242
|
+
log(LOG_PREFIX, `stderr reader error for slot ${slotId}: ${err}`);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/** Handle process exit — update slot and emit event. */
|
|
247
|
+
async function handleExit(
|
|
248
|
+
exitCode: number,
|
|
249
|
+
slotId: number,
|
|
250
|
+
sessionId: string,
|
|
251
|
+
brokerClient: BrokerClient,
|
|
252
|
+
onEvent: (event: AgentEvent) => void,
|
|
253
|
+
): Promise<void> {
|
|
254
|
+
// Update slot status to disconnected
|
|
255
|
+
try {
|
|
256
|
+
await brokerClient.updateSlot({
|
|
257
|
+
id: slotId,
|
|
258
|
+
status: "disconnected",
|
|
259
|
+
});
|
|
260
|
+
} catch (err) {
|
|
261
|
+
log(LOG_PREFIX, `Failed to update slot ${slotId} on exit: ${err}`);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
if (exitCode === 0) {
|
|
265
|
+
onEvent({
|
|
266
|
+
type: "agent_completed",
|
|
267
|
+
severity: "info",
|
|
268
|
+
slotId,
|
|
269
|
+
sessionId,
|
|
270
|
+
message: `Agent in slot ${slotId} completed successfully`,
|
|
271
|
+
data: { exit_code: exitCode },
|
|
272
|
+
});
|
|
273
|
+
} else {
|
|
274
|
+
onEvent({
|
|
275
|
+
type: "agent_crashed",
|
|
276
|
+
severity: "critical",
|
|
277
|
+
slotId,
|
|
278
|
+
sessionId,
|
|
279
|
+
message: `Agent in slot ${slotId} exited with code ${exitCode}`,
|
|
280
|
+
data: { exit_code: exitCode },
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
log(LOG_PREFIX, `Slot ${slotId} process exited with code ${exitCode}`);
|
|
285
|
+
}
|