@heretyc/subagent-mcp 2.8.2 → 2.8.5
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/LICENSE +201 -201
- package/NOTICE +5 -5
- package/README.md +136 -135
- package/directives/carryover-claude.md +6 -10
- package/directives/carryover-codex.md +6 -10
- package/directives/orchestration-claude.md +22 -14
- package/directives/orchestration-codex.md +22 -13
- package/directives/reminder-off-claude.md +6 -7
- package/directives/reminder-off-codex.md +6 -7
- package/directives/reminder-on.md +5 -12
- package/directives/short-off.md +1 -0
- package/directives/short-on.md +1 -0
- package/dist/advanced-ruleset.py +67 -67
- package/dist/drivers.js +478 -0
- package/dist/effort.js +7 -16
- package/dist/hooks/orchestration-claude.js +2 -1
- package/dist/hooks/orchestration-codex.js +2 -1
- package/dist/index.js +118 -84
- package/dist/orchestration/hook-core.js +4 -4
- package/dist/output-helpers.js +26 -3
- package/dist/routing-table.json +4149 -4149
- package/dist/ruleset-scaffold.js +1 -1
- package/dist/setup.js +2 -1
- package/dist/status-helpers.js +4 -3
- package/dist/stream-helpers.js +51 -6
- package/package.json +53 -51
- package/scripts/postinstall.mjs +101 -101
- package/directives/off-turn-reminder.md +0 -1
package/dist/index.js
CHANGED
|
@@ -9,11 +9,12 @@ import { isAbsolute, basename, join, dirname } from "node:path";
|
|
|
9
9
|
import { tmpdir } from "node:os";
|
|
10
10
|
import { pathToFileURL } from "url";
|
|
11
11
|
import { buildCommand } from "./effort.js";
|
|
12
|
+
import { createProviderDriver } from "./drivers.js";
|
|
12
13
|
import { resolveExeFor } from "./platform.js";
|
|
13
14
|
import { formatLocalIso, selectUnreported } from "./wait-helpers.js";
|
|
14
15
|
import { computeStatusTransition, buildLivenessFields, } from "./status-helpers.js";
|
|
15
16
|
import { extractFinalTurn } from "./output-helpers.js";
|
|
16
|
-
import { consumeStreamChunk, flushStream, retainLastN, } from "./stream-helpers.js";
|
|
17
|
+
import { consumeStreamChunk, flushStream, isTurnCompletedLine, retainLastN, } from "./stream-helpers.js";
|
|
17
18
|
import { loadRoutingTable, buildCandidates, validatePresence, TASK_CATEGORIES, AUTO_HINT, SPLIT_HINT, } from "./routing.js";
|
|
18
19
|
import { createDeadlockWindow } from "./deadlock.js";
|
|
19
20
|
import { createRulesetGate, RULESET_HARD_FAIL_MSG, } from "./ruleset.js";
|
|
@@ -27,14 +28,13 @@ const deadlockWindow = createDeadlockWindow();
|
|
|
27
28
|
// scoping. The env-check runs lazily at the FIRST launch_agent call; success
|
|
28
29
|
// latches enabled/disabled for the process lifetime, failure never latches.
|
|
29
30
|
const rulesetGate = createRulesetGate();
|
|
30
|
-
// Post-spawn grace window (ms). A
|
|
31
|
-
// successful
|
|
32
|
-
//
|
|
33
|
-
//
|
|
34
|
-
//
|
|
35
|
-
//
|
|
36
|
-
//
|
|
37
|
-
// sets it.
|
|
31
|
+
// Post-spawn grace window (ms). A provider driver that exits within this window
|
|
32
|
+
// after a successful driver start never launched (not logged in, expired auth, instant
|
|
33
|
+
// crash) — the attempt loop silently advances instead of falsely reporting
|
|
34
|
+
// success. ANY exit within the window counts, even code 0, EXCEPT a driver
|
|
35
|
+
// already finalized by its turn-completed marker (legitimate fast completion).
|
|
36
|
+
// SUBAGENT_SPAWN_GRACE_MS overrides (non-negative int; 0 disables only this
|
|
37
|
+
// post-start early-exit detection) — a test seam; production never sets it.
|
|
38
38
|
const SPAWN_GRACE_MS = (() => {
|
|
39
39
|
const raw = process.env.SUBAGENT_SPAWN_GRACE_MS;
|
|
40
40
|
if (raw === undefined || raw === "")
|
|
@@ -120,8 +120,8 @@ const ORCHESTRATION_INSTRUCTIONS = "BINDING IN BOTH MODES - SOLE CHANNEL: while
|
|
|
120
120
|
const SUBAGENT_INSTRUCTIONS = "SUB-AGENT SESSION: you are a child process launched by subagent-mcp. Follow the parent prompt. Do not treat yourself as the orchestrator, do not re-trigger orchestration carryover, and do not launch further sub-agents unless the parent prompt explicitly assigns that.";
|
|
121
121
|
const server = new McpServer({
|
|
122
122
|
name: "subagent-mcp",
|
|
123
|
-
version: "2.
|
|
124
|
-
description: "
|
|
123
|
+
version: "2.8.5",
|
|
124
|
+
description: "Launches always-interactive local Claude and Codex sub-agent sessions. Claude uses the Claude Agent SDK over the local Claude Code executable; Codex uses `codex app-server` over stdio. The server does not call Anthropic or OpenAI HTTP APIs directly.",
|
|
125
125
|
}, {
|
|
126
126
|
instructions: process.env.SUBAGENT_MCP_SUBAGENT === "1"
|
|
127
127
|
? SUBAGENT_INSTRUCTIONS
|
|
@@ -140,7 +140,7 @@ function cleanupUcSettingsPath(ucSettingsPath) {
|
|
|
140
140
|
catch { }
|
|
141
141
|
}
|
|
142
142
|
// Attempt to spawn + register a single candidate. Resolves to the agent_id on a
|
|
143
|
-
// successful
|
|
143
|
+
// successful driver start, or a launch-time failure reason string (never throws/rejects).
|
|
144
144
|
//
|
|
145
145
|
// spawn() failures (ENOENT/EACCES) are ASYNC: a missing/broken CLI emits the
|
|
146
146
|
// child 'error' event AFTER spawn() returns, so a try/catch around spawn cannot
|
|
@@ -165,7 +165,7 @@ async function tryLaunchCandidate(candidate, prompt, agentCwd, routingTier, rule
|
|
|
165
165
|
let buildResult;
|
|
166
166
|
let cmd;
|
|
167
167
|
try {
|
|
168
|
-
buildResult = buildCommand(candidate.provider, candidate.model, effortForBuild,
|
|
168
|
+
buildResult = buildCommand(candidate.provider, candidate.model, effortForBuild, agentCwd);
|
|
169
169
|
cmd = resolveExe(candidate.provider);
|
|
170
170
|
}
|
|
171
171
|
catch (e) {
|
|
@@ -177,14 +177,17 @@ async function tryLaunchCandidate(candidate, prompt, agentCwd, routingTier, rule
|
|
|
177
177
|
cleanupUcSettingsPath(buildResult.ucSettingsPath);
|
|
178
178
|
return { reason: `CLI executable not found: ${cmd}` };
|
|
179
179
|
}
|
|
180
|
-
|
|
181
|
-
let childProcess;
|
|
180
|
+
let driver;
|
|
182
181
|
try {
|
|
183
|
-
|
|
182
|
+
driver = await createProviderDriver({
|
|
183
|
+
provider: candidate.provider,
|
|
184
|
+
command: cmd,
|
|
185
|
+
args: buildResult.args,
|
|
184
186
|
cwd: agentCwd,
|
|
185
187
|
env: { ...process.env, SUBAGENT_MCP_SUBAGENT: "1" },
|
|
186
|
-
|
|
187
|
-
|
|
188
|
+
model: candidate.model,
|
|
189
|
+
effort: candidate.effort,
|
|
190
|
+
ucSettingsPath: buildResult.ucSettingsPath,
|
|
188
191
|
});
|
|
189
192
|
}
|
|
190
193
|
catch (error) {
|
|
@@ -192,6 +195,7 @@ async function tryLaunchCandidate(candidate, prompt, agentCwd, routingTier, rule
|
|
|
192
195
|
cleanupUcSettingsPath(buildResult.ucSettingsPath);
|
|
193
196
|
return { reason: error instanceof Error ? error.message : String(error) };
|
|
194
197
|
}
|
|
198
|
+
const childProcess = driver.process;
|
|
195
199
|
// Await the one-shot spawn/error race. The 'error' handler is attached BEFORE
|
|
196
200
|
// we await so an async ENOENT cannot escape as an unhandled event.
|
|
197
201
|
try {
|
|
@@ -208,7 +212,7 @@ async function tryLaunchCandidate(candidate, prompt, agentCwd, routingTier, rule
|
|
|
208
212
|
// Launch-time failure (ENOENT/EACCES/etc.) — kill if somehow alive, clean up
|
|
209
213
|
// the settings file, and report so the attempt loop advances.
|
|
210
214
|
try {
|
|
211
|
-
|
|
215
|
+
driver.kill();
|
|
212
216
|
}
|
|
213
217
|
catch { }
|
|
214
218
|
cleanupUcSettingsPath(buildResult.ucSettingsPath);
|
|
@@ -229,6 +233,7 @@ async function tryLaunchCandidate(candidate, prompt, agentCwd, routingTier, rule
|
|
|
229
233
|
: {}),
|
|
230
234
|
status: "processing",
|
|
231
235
|
process: childProcess,
|
|
236
|
+
driver,
|
|
232
237
|
stdout: "",
|
|
233
238
|
stderr: "",
|
|
234
239
|
exitCode: null,
|
|
@@ -250,15 +255,6 @@ async function tryLaunchCandidate(candidate, prompt, agentCwd, routingTier, rule
|
|
|
250
255
|
// item, so it does NOT refresh the heartbeat.
|
|
251
256
|
agentState.stderr += `\n[process error] ${err instanceof Error ? err.message : String(err)}`;
|
|
252
257
|
});
|
|
253
|
-
if (candidate.provider === "claude" && childProcess.stdin) {
|
|
254
|
-
// EPIPE if the child dies before draining the prompt (the grace-window
|
|
255
|
-
// early-exit class) — fold into stderr, never crash the server.
|
|
256
|
-
childProcess.stdin.on("error", (err) => {
|
|
257
|
-
agentState.stderr += `\n[stdin error] ${err instanceof Error ? err.message : String(err)}`;
|
|
258
|
-
});
|
|
259
|
-
childProcess.stdin.write(prompt);
|
|
260
|
-
childProcess.stdin.end();
|
|
261
|
-
}
|
|
262
258
|
if (childProcess.stdout) {
|
|
263
259
|
childProcess.stdout.on("data", (data) => {
|
|
264
260
|
const chunk = data.toString();
|
|
@@ -278,17 +274,15 @@ async function tryLaunchCandidate(candidate, prompt, agentCwd, routingTier, rule
|
|
|
278
274
|
agentState.lastActivity = at;
|
|
279
275
|
agentState.visibleStream = retainLastN(agentState.visibleStream, items.map((it) => ({ ...it, at })), 3);
|
|
280
276
|
}
|
|
281
|
-
//
|
|
282
|
-
//
|
|
283
|
-
//
|
|
284
|
-
|
|
285
|
-
|
|
277
|
+
// Provider completion events mark the current turn finished while the
|
|
278
|
+
// logical interactive session can remain available for later messages.
|
|
279
|
+
// Scan COMPLETE lines only so a marker split across chunks is matched
|
|
280
|
+
// once fully assembled (never on a partial fragment).
|
|
281
|
+
if (lines.some((l) => isTurnCompletedLine(agentState.provider, l))) {
|
|
286
282
|
agentState.turnCompleted = true;
|
|
287
283
|
agentState.status = "finished";
|
|
288
|
-
agentState.exitCode = 0;
|
|
289
284
|
if (agentState.exitedAt === null)
|
|
290
285
|
agentState.exitedAt = at;
|
|
291
|
-
childProcess.kill();
|
|
292
286
|
}
|
|
293
287
|
});
|
|
294
288
|
}
|
|
@@ -309,11 +303,13 @@ async function tryLaunchCandidate(candidate, prompt, agentCwd, routingTier, rule
|
|
|
309
303
|
for (const line of lines) {
|
|
310
304
|
agentState.stdout += line + "\n";
|
|
311
305
|
}
|
|
312
|
-
// A
|
|
313
|
-
//
|
|
314
|
-
if (agentState.provider
|
|
315
|
-
lines.some((l) => l.includes('"type":"turn.completed"'))) {
|
|
306
|
+
// A completion marker may arrive only in this final flush (no trailing
|
|
307
|
+
// newline) — the grace window's success exception needs it.
|
|
308
|
+
if (lines.some((l) => isTurnCompletedLine(agentState.provider, l))) {
|
|
316
309
|
agentState.turnCompleted = true;
|
|
310
|
+
agentState.status = "finished";
|
|
311
|
+
if (agentState.exitedAt === null)
|
|
312
|
+
agentState.exitedAt = at;
|
|
317
313
|
}
|
|
318
314
|
if (items.length > 0) {
|
|
319
315
|
agentState.lastActivity = at;
|
|
@@ -332,7 +328,12 @@ async function tryLaunchCandidate(candidate, prompt, agentCwd, routingTier, rule
|
|
|
332
328
|
return;
|
|
333
329
|
}
|
|
334
330
|
if (agentState.status === "finished") {
|
|
335
|
-
// Already finalized by turn.completed;
|
|
331
|
+
// Already finalized by turn.completed; record that the interactive
|
|
332
|
+
// driver is no longer live so poll/list stop advertising alive=true.
|
|
333
|
+
if (agentState.exitCode === null)
|
|
334
|
+
agentState.exitCode = code !== null ? code : -1;
|
|
335
|
+
if (code !== 0)
|
|
336
|
+
agentState.status = "errored";
|
|
336
337
|
return;
|
|
337
338
|
}
|
|
338
339
|
// Normal exit: set exit code and derive status
|
|
@@ -346,28 +347,51 @@ async function tryLaunchCandidate(candidate, prompt, agentCwd, routingTier, rule
|
|
|
346
347
|
const closedAfterFlush = new Promise((resolve) => {
|
|
347
348
|
childProcess.once("close", () => resolve());
|
|
348
349
|
});
|
|
349
|
-
//
|
|
350
|
-
//
|
|
351
|
-
//
|
|
352
|
-
//
|
|
353
|
-
//
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
//
|
|
350
|
+
// POSIX seam: a child can die and deliver 'exit' DURING the startup write
|
|
351
|
+
// below (the write then rejects with EPIPE) — before the grace block attaches
|
|
352
|
+
// its own listener. Capture that exit here so the grace window still sees it
|
|
353
|
+
// and reports the exit code, instead of registering a corpse or surfacing the
|
|
354
|
+
// raw EPIPE as the failure reason.
|
|
355
|
+
let earlyExitInfo = null;
|
|
356
|
+
childProcess.once("exit", (code, signal) => {
|
|
357
|
+
earlyExitInfo = { code, signal };
|
|
358
|
+
});
|
|
359
|
+
// The startup write is best-effort. On POSIX, writing to an already-dead
|
|
360
|
+
// child's stdin rejects with EPIPE; that is NOT itself a launch failure.
|
|
361
|
+
// Record it and fall through to the grace window, which reports the real exit
|
|
362
|
+
// code (grace>0) or the legacy startup-write seam registers it (grace=0).
|
|
363
|
+
let startError = null;
|
|
364
|
+
try {
|
|
365
|
+
await driver.start(prompt);
|
|
366
|
+
}
|
|
367
|
+
catch (err) {
|
|
368
|
+
startError = err instanceof Error ? err : new Error(String(err));
|
|
369
|
+
}
|
|
370
|
+
// Post-spawn grace window: a 'spawn' win alone is NOT success — a provider
|
|
371
|
+
// driver that starts then dies immediately must advance the attempt loop, not
|
|
372
|
+
// falsely conclude it. AgentState is fully wired and the initial turn already
|
|
373
|
+
// submitted above, so a surviving driver loses no stream output during the
|
|
374
|
+
// wait. Exception: a driver already finalized by its turn-completion marker
|
|
375
|
+
// completed the task legitimately fast — that is a success, never a launch
|
|
376
|
+
// failure. The close handler above cleans up a condemned driver (uc settings,
|
|
377
|
+
// stream flush); the agent is simply never registered.
|
|
359
378
|
if (SPAWN_GRACE_MS > 0) {
|
|
360
|
-
const earlyExit =
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
379
|
+
const earlyExit = earlyExitInfo ??
|
|
380
|
+
(await new Promise((resolve) => {
|
|
381
|
+
if (earlyExitInfo) {
|
|
382
|
+
resolve(earlyExitInfo);
|
|
383
|
+
return;
|
|
384
|
+
}
|
|
385
|
+
const timer = setTimeout(() => {
|
|
386
|
+
childProcess.removeListener("exit", onExit);
|
|
387
|
+
resolve(null);
|
|
388
|
+
}, SPAWN_GRACE_MS);
|
|
389
|
+
const onExit = (code, signal) => {
|
|
390
|
+
clearTimeout(timer);
|
|
391
|
+
resolve({ code, signal });
|
|
392
|
+
};
|
|
393
|
+
childProcess.once("exit", onExit);
|
|
394
|
+
}));
|
|
371
395
|
if (earlyExit) {
|
|
372
396
|
// 'exit' can be delivered before the final stdout chunk, so wait for
|
|
373
397
|
// 'close' (streams drained, flush scanned) before deciding — a
|
|
@@ -381,6 +405,17 @@ async function tryLaunchCandidate(candidate, prompt, agentCwd, routingTier, rule
|
|
|
381
405
|
};
|
|
382
406
|
}
|
|
383
407
|
}
|
|
408
|
+
else if (startError && !agentState.turnCompleted) {
|
|
409
|
+
// Lived past the grace window but the startup write failed: the child is
|
|
410
|
+
// not accepting input, so advance the loop rather than register an agent
|
|
411
|
+
// that never received its prompt.
|
|
412
|
+
try {
|
|
413
|
+
driver.kill();
|
|
414
|
+
}
|
|
415
|
+
catch { }
|
|
416
|
+
cleanupUcSettings(agentState);
|
|
417
|
+
return { reason: startError.message };
|
|
418
|
+
}
|
|
384
419
|
}
|
|
385
420
|
agents.set(agentId, agentState);
|
|
386
421
|
return { agentId };
|
|
@@ -394,7 +429,7 @@ function sameTriples(a, b) {
|
|
|
394
429
|
return a.every((c, i) => c.provider === b[i].provider && c.model === b[i].model && c.effort === b[i].effort);
|
|
395
430
|
}
|
|
396
431
|
// Tool 1: launch_agent
|
|
397
|
-
server.tool("launch_agent", "Spawn a sub-agent. AUTO MODE (mandatory first attempt unless an override is licensed below): pass only `prompt` + `task_category` and NO overrides; the server picks the best provider/model/effort for that category from its routing table, launches the top candidate, and silently falls back to the next-best on launch failure. `provider`/`model`/`effort` are overrides — licensed on 1st/2nd attempts ONLY when the task verifiably requires a specific capability; STATE that capability when overriding; if you pass `model` you must also pass `provider`, and if you pass `effort` you must pass both `provider` and `model`. SOLE CHANNEL: while this server is connected this tool is the ONLY sanctioned way to spawn sub-agents, in BOTH orchestration states — harness-native Task/Agent tools are FORBIDDEN for sub-agent launches. PROMPT RULE: the FIRST line of every `prompt` MUST be \"<this is a request from a parent process>\" (sub-agent self-identification). Unsure which task_category fits? Don't submit one amorphous task — SPLIT into atomic steps that each map to a single category, one agent per step. ultracode effort is Opus
|
|
432
|
+
server.tool("launch_agent", "Spawn a sub-agent session. AUTO MODE (mandatory first attempt unless an override is licensed below): pass only `prompt` + `task_category` and NO overrides; the server picks the best provider/model/effort for that category from its routing table, launches the top candidate, and silently falls back to the next-best on launch failure. `provider`/`model`/`effort` are overrides — licensed on 1st/2nd attempts ONLY when the task verifiably requires a specific capability; STATE that capability when overriding; if you pass `model` you must also pass `provider`, and if you pass `effort` you must pass both `provider` and `model`. SOLE CHANNEL: while this server is connected this tool is the ONLY sanctioned way to spawn sub-agents, in BOTH orchestration states — harness-native Task/Agent tools are FORBIDDEN for sub-agent launches. PROMPT RULE: the FIRST line of every `prompt` MUST be \"<this is a request from a parent process>\" (sub-agent self-identification). Unsure which task_category fits? Don't submit one amorphous task — SPLIT into atomic steps that each map to a single category, one agent per step. ultracode effort is Opus 4.8+ only. Claude uses a Claude Agent SDK logical session over the local Claude executable; Codex uses a `codex app-server` child. Children run with env SUBAGENT_MCP_SUBAGENT=1 so the orchestration hooks skip them (they are not orchestrators and don't re-trigger carryover). Launch returns status `processing` (alive); a later `stalled` is alive-but-quiet (thinking or awaiting a temp-file handoff), NOT dead — wait or re-poll, don't kill (see poll_agent). DEADLOCK RULE: you MUST ALWAYS set `deadlock=true` when 2 launch attempts for the SAME atomic task have already failed or been unsatisfactory (the 3rd attempt onward; re-wording or re-splitting the prompt does NOT make it a different task), and NEVER otherwise — from the 3rd attempt deadlock outranks any capability override: drop provider/model/effort.", {
|
|
398
433
|
task_category: z.enum(TASK_CATEGORIES).describe(TASK_CATEGORY_GLOSS),
|
|
399
434
|
prompt: z.string().min(1),
|
|
400
435
|
provider: z.enum(["claude", "codex"]).optional(),
|
|
@@ -491,7 +526,7 @@ server.tool("launch_agent", "Spawn a sub-agent. AUTO MODE (mandatory first attem
|
|
|
491
526
|
}
|
|
492
527
|
candidates = applied.candidates;
|
|
493
528
|
}
|
|
494
|
-
// 6. Attempt loop: best→worst. Register on first successful
|
|
529
|
+
// 6. Attempt loop: best→worst. Register on first successful driver start; silently
|
|
495
530
|
// advance on launch-time failure. Sub-agent task outcome is NEVER a trigger.
|
|
496
531
|
const skipped = [];
|
|
497
532
|
for (const candidate of candidates) {
|
|
@@ -608,7 +643,7 @@ server.tool("poll_agent", "Get an agent's current status and output. Status `pro
|
|
|
608
643
|
};
|
|
609
644
|
});
|
|
610
645
|
// Tool 3: kill_agent
|
|
611
|
-
server.tool("kill_agent", "Terminate a live agent (status `processing` or
|
|
646
|
+
server.tool("kill_agent", "Terminate a live agent/session (status `processing`, `stalled`, or turn-finished but still interactive) by immediately force-killing its managed driver. No-op for already-terminal closed agents.", {
|
|
612
647
|
agent_id: z.string(),
|
|
613
648
|
}, async (params) => {
|
|
614
649
|
const agent = agents.get(params.agent_id);
|
|
@@ -623,9 +658,11 @@ server.tool("kill_agent", "Terminate a live agent (status `processing` or `stall
|
|
|
623
658
|
isError: true,
|
|
624
659
|
};
|
|
625
660
|
}
|
|
626
|
-
// Kill applies to ALL live states (processing
|
|
627
|
-
//
|
|
628
|
-
|
|
661
|
+
// Kill applies to ALL live driver states (processing, stalled, or finished
|
|
662
|
+
// current turn with an open interactive session). Closed terminal agents are
|
|
663
|
+
// a no-op.
|
|
664
|
+
const isLive = (agent.status === "processing" || agent.status === "stalled" || agent.status === "finished") &&
|
|
665
|
+
!agent.driver.closed;
|
|
629
666
|
if (!isLive) {
|
|
630
667
|
return {
|
|
631
668
|
content: [
|
|
@@ -645,6 +682,7 @@ server.tool("kill_agent", "Terminate a live agent (status `processing` or `stall
|
|
|
645
682
|
// grace period. On Windows, taskkill /t /f tears down the whole tree; on
|
|
646
683
|
// POSIX, SIGKILL the process (close handler records the real exit code).
|
|
647
684
|
agent.status = "stopped";
|
|
685
|
+
agent.driver.kill();
|
|
648
686
|
if (isWindows && agent.process.pid) {
|
|
649
687
|
spawn("taskkill", ["/pid", String(agent.process.pid), "/t", "/f"], {
|
|
650
688
|
windowsHide: true,
|
|
@@ -683,7 +721,7 @@ server.tool("kill_agent", "Terminate a live agent (status `processing` or `stall
|
|
|
683
721
|
}
|
|
684
722
|
});
|
|
685
723
|
// Tool 4: send_message
|
|
686
|
-
server.tool("send_message", "
|
|
724
|
+
server.tool("send_message", "Enqueue a user message for an open interactive agent session. Observe output with poll_agent or wait.", {
|
|
687
725
|
agent_id: z.string(),
|
|
688
726
|
message: z.string().min(1),
|
|
689
727
|
}, async (params) => {
|
|
@@ -699,7 +737,8 @@ server.tool("send_message", "Send a message to a running agent's stdin", {
|
|
|
699
737
|
isError: true,
|
|
700
738
|
};
|
|
701
739
|
}
|
|
702
|
-
const isLive = agent.status === "processing" || agent.status === "stalled"
|
|
740
|
+
const isLive = (agent.status === "processing" || agent.status === "stalled" || agent.status === "finished") &&
|
|
741
|
+
!agent.driver.closed;
|
|
703
742
|
if (!isLive) {
|
|
704
743
|
return {
|
|
705
744
|
content: [
|
|
@@ -711,20 +750,15 @@ server.tool("send_message", "Send a message to a running agent's stdin", {
|
|
|
711
750
|
isError: true,
|
|
712
751
|
};
|
|
713
752
|
}
|
|
714
|
-
if (!agent.process.stdin) {
|
|
715
|
-
return {
|
|
716
|
-
content: [
|
|
717
|
-
{
|
|
718
|
-
type: "text",
|
|
719
|
-
text: `Error: Agent stdin is not available`,
|
|
720
|
-
},
|
|
721
|
-
],
|
|
722
|
-
isError: true,
|
|
723
|
-
};
|
|
724
|
-
}
|
|
725
753
|
try {
|
|
726
|
-
agent.
|
|
727
|
-
|
|
754
|
+
await agent.driver.send(params.message);
|
|
755
|
+
const now = Date.now();
|
|
756
|
+
agent.status = "processing";
|
|
757
|
+
agent.exitCode = null;
|
|
758
|
+
agent.exitedAt = null;
|
|
759
|
+
agent.waitReported = false;
|
|
760
|
+
agent.turnCompleted = false;
|
|
761
|
+
agent.lastActivity = now;
|
|
728
762
|
return {
|
|
729
763
|
content: [
|
|
730
764
|
{
|
|
@@ -732,7 +766,7 @@ server.tool("send_message", "Send a message to a running agent's stdin", {
|
|
|
732
766
|
text: JSON.stringify({
|
|
733
767
|
agent_id: agent.id,
|
|
734
768
|
status: "sent",
|
|
735
|
-
message: "Message
|
|
769
|
+
message: "Message accepted by provider driver",
|
|
736
770
|
}),
|
|
737
771
|
},
|
|
738
772
|
],
|
|
@@ -781,7 +815,7 @@ server.tool("list_agents", "List all agents with token-efficient core metrics (s
|
|
|
781
815
|
};
|
|
782
816
|
});
|
|
783
817
|
// Tool 6: wait
|
|
784
|
-
server.tool("wait", "Blocks until one or more sub-agents reach a
|
|
818
|
+
server.tool("wait", "Blocks until one or more sub-agents reach a reportable state (turn-finished, errored, or stopped), returning exit code when known + local-time timestamp; or returns the live-job list after a 15-minute timeout. A `finished` agent can still be alive and accept `send_message` when exit_code is null. A `stalled` agent is still ALIVE and does NOT end the wait. Pass `verbose: true` to add each finished agent's `final_output`.", {
|
|
785
819
|
verbose: z.boolean().optional().default(false),
|
|
786
820
|
}, async (params) => {
|
|
787
821
|
const { verbose } = params;
|
|
@@ -179,10 +179,10 @@ export function classifyClaim(owner_session, baseline_turn, current) {
|
|
|
179
179
|
* temp dir cannot hold the state file would otherwise inject only the compact
|
|
180
180
|
* rule carrier on every prompt and never refresh the LONG block.
|
|
181
181
|
*/
|
|
182
|
-
function cadenceEmit(env, adapter, longFile, count, persisted) {
|
|
182
|
+
function cadenceEmit(env, adapter, longFile, shortFile, count, persisted) {
|
|
183
183
|
return !persisted || count % REMINDER_PERIOD === 0
|
|
184
184
|
? readDirective(env, longFile)
|
|
185
|
-
: readDirective(env,
|
|
185
|
+
: readDirective(env, shortFile);
|
|
186
186
|
}
|
|
187
187
|
/**
|
|
188
188
|
* Claim (or re-claim) an active marker for the current session and emit the
|
|
@@ -237,7 +237,7 @@ export function runHook(payload, env, adapter) {
|
|
|
237
237
|
if (!marker.isActive(cwd)) {
|
|
238
238
|
// OFF: no claim machinery — just the per-prompt reminder cadence.
|
|
239
239
|
const r = reminder.advance(cwd, current);
|
|
240
|
-
return cadenceEmit(env, adapter, adapter.reminderOffFile, r.count, r.persisted);
|
|
240
|
+
return cadenceEmit(env, adapter, adapter.reminderOffFile, adapter.shortOffFile, r.count, r.persisted);
|
|
241
241
|
}
|
|
242
242
|
const m = marker.readMarker(cwd);
|
|
243
243
|
const kind = classifyClaim(m.owner_session, m.baseline_turn, current);
|
|
@@ -247,7 +247,7 @@ export function runHook(payload, env, adapter) {
|
|
|
247
247
|
}
|
|
248
248
|
// SAME-SESSION: per-prompt reminder cadence, ON variant.
|
|
249
249
|
const r = reminder.advance(cwd, current);
|
|
250
|
-
return cadenceEmit(env, adapter, adapter.reminderOnFile, r.count, r.persisted);
|
|
250
|
+
return cadenceEmit(env, adapter, adapter.reminderOnFile, adapter.shortOnFile, r.count, r.persisted);
|
|
251
251
|
}
|
|
252
252
|
catch {
|
|
253
253
|
// Any failure -> inject nothing. Never crash or stall the host turn.
|
package/dist/output-helpers.js
CHANGED
|
@@ -4,13 +4,36 @@
|
|
|
4
4
|
function rawFallback(stdout) {
|
|
5
5
|
return (stdout || "").trim();
|
|
6
6
|
}
|
|
7
|
-
// Pull a final assistant-message string out of one parsed
|
|
8
|
-
//
|
|
9
|
-
//
|
|
7
|
+
// Pull a final assistant-message string out of one parsed Codex event. Codex
|
|
8
|
+
// app-server emits JSON-RPC notifications, while older CLI JSONL used top-level
|
|
9
|
+
// event objects, so match tolerantly.
|
|
10
10
|
function codexEventText(evt) {
|
|
11
11
|
if (!evt || typeof evt !== "object")
|
|
12
12
|
return null;
|
|
13
13
|
const e = evt;
|
|
14
|
+
if (typeof e.method === "string" && e.params && typeof e.params === "object") {
|
|
15
|
+
const params = e.params;
|
|
16
|
+
if (e.method === "item/agentMessage/delta" && typeof params.delta === "string") {
|
|
17
|
+
return params.delta;
|
|
18
|
+
}
|
|
19
|
+
if (e.method === "item/completed" && params.item && typeof params.item === "object") {
|
|
20
|
+
const item = params.item;
|
|
21
|
+
if (item.type === "agentMessage" && typeof item.text === "string")
|
|
22
|
+
return item.text;
|
|
23
|
+
}
|
|
24
|
+
if (e.method === "turn/completed" && params.turn && typeof params.turn === "object") {
|
|
25
|
+
const turn = params.turn;
|
|
26
|
+
const items = Array.isArray(turn.items) ? turn.items : [];
|
|
27
|
+
for (let i = items.length - 1; i >= 0; i--) {
|
|
28
|
+
const item = items[i];
|
|
29
|
+
if (item && typeof item === "object") {
|
|
30
|
+
const obj = item;
|
|
31
|
+
if (obj.type === "agentMessage" && typeof obj.text === "string")
|
|
32
|
+
return obj.text;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
14
37
|
// Shape A: { type: "agent_message", message: "..." }
|
|
15
38
|
if (e.type === "agent_message" && typeof e.message === "string") {
|
|
16
39
|
return e.message;
|