@humain/terminal 0.0.16 → 0.0.17
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 +6 -0
- package/dist/bundle/chunks/{chunk-Z4RA2T5Q.js → chunk-PFVS5ZYL.js} +55 -53
- package/dist/bundle/cli.js +1 -1
- package/dist/bundle/index.js +1 -1
- package/dist/bundle/rpc-entry.js +1 -1
- package/dist/core/tools/subagent-worktree.d.ts +24 -0
- package/dist/core/tools/subagent-worktree.d.ts.map +1 -0
- package/dist/core/tools/subagent-worktree.js +243 -0
- package/dist/core/tools/subagent-worktree.js.map +1 -0
- package/dist/core/tools/subagent.d.ts +8 -0
- package/dist/core/tools/subagent.d.ts.map +1 -1
- package/dist/core/tools/subagent.js +113 -20
- package/dist/core/tools/subagent.js.map +1 -1
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
|
@@ -23,6 +23,7 @@ import { getMarkdownTheme } from "../../modes/interactive/theme/theme.js";
|
|
|
23
23
|
import { parseFrontmatter } from "../../utils/frontmatter.js";
|
|
24
24
|
import { withFileMutationQueue } from "./file-mutation-queue.js";
|
|
25
25
|
import { shortenPath } from "./render-utils.js";
|
|
26
|
+
import { createSubagentWorktree, isGitWorkTree, } from "./subagent-worktree.js";
|
|
26
27
|
import { wrapToolDefinition } from "./tool-definition-wrapper.js";
|
|
27
28
|
const MAX_PARALLEL_TASKS = 8;
|
|
28
29
|
const MAX_CONCURRENCY = 4;
|
|
@@ -223,7 +224,10 @@ function getFinalOutput(messages) {
|
|
|
223
224
|
return "";
|
|
224
225
|
}
|
|
225
226
|
function isFailedResult(result) {
|
|
226
|
-
return result.
|
|
227
|
+
return (Boolean(result.errorMessage) ||
|
|
228
|
+
result.exitCode !== 0 ||
|
|
229
|
+
result.stopReason === "error" ||
|
|
230
|
+
result.stopReason === "aborted");
|
|
227
231
|
}
|
|
228
232
|
function getResultOutput(result) {
|
|
229
233
|
if (isFailedResult(result)) {
|
|
@@ -255,7 +259,7 @@ function getDisplayItems(messages) {
|
|
|
255
259
|
}
|
|
256
260
|
return items;
|
|
257
261
|
}
|
|
258
|
-
async function mapWithConcurrencyLimit(items, concurrency, fn, signal) {
|
|
262
|
+
async function mapWithConcurrencyLimit(items, concurrency, fn, signal, onAbort) {
|
|
259
263
|
if (items.length === 0)
|
|
260
264
|
return [];
|
|
261
265
|
const limit = Math.max(1, Math.min(concurrency, items.length));
|
|
@@ -263,12 +267,14 @@ async function mapWithConcurrencyLimit(items, concurrency, fn, signal) {
|
|
|
263
267
|
let nextIndex = 0;
|
|
264
268
|
const workers = new Array(limit).fill(null).map(async () => {
|
|
265
269
|
while (true) {
|
|
266
|
-
// Don't spawn doomed children for tasks still queued after an abort.
|
|
267
|
-
if (signal?.aborted)
|
|
268
|
-
throw new Error("Subagent was aborted");
|
|
269
270
|
const current = nextIndex++;
|
|
270
271
|
if (current >= items.length)
|
|
271
272
|
return;
|
|
273
|
+
if (signal?.aborted) {
|
|
274
|
+
if (onAbort)
|
|
275
|
+
results[current] = await onAbort(items[current], current);
|
|
276
|
+
continue;
|
|
277
|
+
}
|
|
272
278
|
results[current] = await fn(items[current], current);
|
|
273
279
|
}
|
|
274
280
|
});
|
|
@@ -297,7 +303,7 @@ function getCliInvocation(args) {
|
|
|
297
303
|
}
|
|
298
304
|
return { command: "humain-terminal", args };
|
|
299
305
|
}
|
|
300
|
-
async function runSingleAgent(defaultCwd, agents, agentName, task, cwd, step, model, parentModel, signal, onUpdate, makeDetails) {
|
|
306
|
+
async function runSingleAgent(defaultCwd, agents, agentName, task, cwd, isolation, step, model, parentModel, signal, onUpdate, makeDetails, toolCallId, childIndex) {
|
|
301
307
|
const agent = agents.find((a) => a.name === agentName);
|
|
302
308
|
if (!agent) {
|
|
303
309
|
const available = agents.map((a) => `"${a.name}"`).join(", ") || "none";
|
|
@@ -310,8 +316,13 @@ async function runSingleAgent(defaultCwd, agents, agentName, task, cwd, step, mo
|
|
|
310
316
|
stderr: `Unknown agent: "${agentName}". Available agents: ${available}.`,
|
|
311
317
|
usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, contextTokens: 0, turns: 0 },
|
|
312
318
|
step,
|
|
319
|
+
isolation: "shared",
|
|
313
320
|
};
|
|
314
321
|
}
|
|
322
|
+
let worktree = null;
|
|
323
|
+
let isolationDegradedReason;
|
|
324
|
+
const parentCwd = cwd ?? defaultCwd;
|
|
325
|
+
let childCwd = parentCwd;
|
|
315
326
|
const args = ["--mode", "json", "-p", "--no-session"];
|
|
316
327
|
if (model) {
|
|
317
328
|
// Canonical "provider/modelId" handle: split on the FIRST slash so
|
|
@@ -343,6 +354,7 @@ async function runSingleAgent(defaultCwd, agents, agentName, task, cwd, step, mo
|
|
|
343
354
|
usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, contextTokens: 0, turns: 0 },
|
|
344
355
|
model: model ?? `${parentModel.provider}/${parentModel.id}`,
|
|
345
356
|
step,
|
|
357
|
+
isolation: "shared",
|
|
346
358
|
};
|
|
347
359
|
const emitUpdate = () => {
|
|
348
360
|
if (onUpdate) {
|
|
@@ -352,7 +364,22 @@ async function runSingleAgent(defaultCwd, agents, agentName, task, cwd, step, mo
|
|
|
352
364
|
});
|
|
353
365
|
}
|
|
354
366
|
};
|
|
367
|
+
let wasAborted = false;
|
|
355
368
|
try {
|
|
369
|
+
if (isolation === "worktree") {
|
|
370
|
+
if (await isGitWorkTree(parentCwd)) {
|
|
371
|
+
worktree = await createSubagentWorktree({ parentCwd, label: `${toolCallId}-${childIndex}` });
|
|
372
|
+
childCwd = worktree.path;
|
|
373
|
+
currentResult.isolation = "worktree";
|
|
374
|
+
}
|
|
375
|
+
else {
|
|
376
|
+
isolationDegradedReason = "not-a-git-repo";
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
if (isolationDegradedReason)
|
|
380
|
+
currentResult.isolationDegradedReason = isolationDegradedReason;
|
|
381
|
+
if (signal?.aborted)
|
|
382
|
+
throw new Error("Subagent was aborted");
|
|
356
383
|
if (agent.systemPrompt.trim()) {
|
|
357
384
|
const tmp = await writePromptToTempFile(agent.name, agent.systemPrompt);
|
|
358
385
|
tmpPromptDir = tmp.dir;
|
|
@@ -360,16 +387,16 @@ async function runSingleAgent(defaultCwd, agents, agentName, task, cwd, step, mo
|
|
|
360
387
|
args.push("--append-system-prompt", tmpPromptPath);
|
|
361
388
|
}
|
|
362
389
|
args.push(`Task: ${task}`);
|
|
363
|
-
let wasAborted = false;
|
|
364
390
|
const exitCode = await new Promise((resolve) => {
|
|
365
391
|
const invocation = getCliInvocation(args);
|
|
366
392
|
const proc = spawn(invocation.command, invocation.args, {
|
|
367
|
-
cwd:
|
|
393
|
+
cwd: childCwd,
|
|
368
394
|
shell: false,
|
|
369
395
|
stdio: ["ignore", "pipe", "pipe"],
|
|
370
396
|
env: { ...process.env, [SUBAGENT_DEPTH_ENV]: String(currentSubagentDepth() + 1) },
|
|
371
397
|
});
|
|
372
398
|
let buffer = "";
|
|
399
|
+
let settled = false;
|
|
373
400
|
const processLine = (line) => {
|
|
374
401
|
if (!line.trim())
|
|
375
402
|
return;
|
|
@@ -418,23 +445,39 @@ async function runSingleAgent(defaultCwd, agents, agentName, task, cwd, step, mo
|
|
|
418
445
|
proc.stderr.on("data", (data) => {
|
|
419
446
|
currentResult.stderr += data.toString();
|
|
420
447
|
});
|
|
448
|
+
let abortListener;
|
|
449
|
+
let killTimer;
|
|
450
|
+
const cleanupAbort = () => {
|
|
451
|
+
if (signal && abortListener)
|
|
452
|
+
signal.removeEventListener("abort", abortListener);
|
|
453
|
+
if (killTimer)
|
|
454
|
+
clearTimeout(killTimer);
|
|
455
|
+
};
|
|
421
456
|
proc.on("close", (code) => {
|
|
457
|
+
settled = true;
|
|
458
|
+
cleanupAbort();
|
|
422
459
|
if (buffer.trim())
|
|
423
460
|
processLine(buffer);
|
|
424
461
|
resolve(code ?? 0);
|
|
425
462
|
});
|
|
426
463
|
proc.on("error", () => {
|
|
464
|
+
settled = true;
|
|
465
|
+
cleanupAbort();
|
|
427
466
|
resolve(1);
|
|
428
467
|
});
|
|
429
468
|
if (signal) {
|
|
430
469
|
const killProc = () => {
|
|
470
|
+
if (settled)
|
|
471
|
+
return;
|
|
431
472
|
wasAborted = true;
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
if (!proc.killed)
|
|
473
|
+
killTimer = setTimeout(() => {
|
|
474
|
+
if (!settled)
|
|
435
475
|
proc.kill("SIGKILL");
|
|
436
476
|
}, 5000);
|
|
477
|
+
killTimer.unref?.();
|
|
478
|
+
proc.kill("SIGTERM");
|
|
437
479
|
};
|
|
480
|
+
abortListener = killProc;
|
|
438
481
|
if (signal.aborted)
|
|
439
482
|
killProc();
|
|
440
483
|
else
|
|
@@ -442,11 +485,50 @@ async function runSingleAgent(defaultCwd, agents, agentName, task, cwd, step, mo
|
|
|
442
485
|
}
|
|
443
486
|
});
|
|
444
487
|
currentResult.exitCode = exitCode;
|
|
445
|
-
if (wasAborted)
|
|
446
|
-
|
|
447
|
-
|
|
488
|
+
if (wasAborted || signal?.aborted) {
|
|
489
|
+
currentResult.exitCode = 1;
|
|
490
|
+
currentResult.stopReason = "aborted";
|
|
491
|
+
currentResult.errorMessage = "Subagent was aborted";
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
catch (error) {
|
|
495
|
+
currentResult.exitCode = 1;
|
|
496
|
+
currentResult.errorMessage = error instanceof Error ? error.message : String(error);
|
|
497
|
+
if (wasAborted || signal?.aborted)
|
|
498
|
+
currentResult.stopReason = "aborted";
|
|
448
499
|
}
|
|
449
500
|
finally {
|
|
501
|
+
if (worktree) {
|
|
502
|
+
try {
|
|
503
|
+
if (isFailedResult(currentResult) || signal?.aborted) {
|
|
504
|
+
currentResult.mergeOutcome = "skipped";
|
|
505
|
+
}
|
|
506
|
+
else {
|
|
507
|
+
const merged = await worktree.mergeBack();
|
|
508
|
+
currentResult.mergeOutcome = merged.outcome;
|
|
509
|
+
currentResult.changedFiles = merged.changedFiles;
|
|
510
|
+
if (merged.patchPath)
|
|
511
|
+
currentResult.patchPath = merged.patchPath;
|
|
512
|
+
if (merged.outcome === "conflict") {
|
|
513
|
+
currentResult.errorMessage = `subagent merge conflict: ${merged.changedFiles.join(", ")} could not be applied to the parent workspace; patch kept at ${merged.patchPath}`;
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
catch (error) {
|
|
518
|
+
currentResult.exitCode = 1;
|
|
519
|
+
currentResult.mergeOutcome = "skipped";
|
|
520
|
+
currentResult.errorMessage = `subagent merge failed: ${error instanceof Error ? error.message : String(error)}`;
|
|
521
|
+
}
|
|
522
|
+
finally {
|
|
523
|
+
try {
|
|
524
|
+
await worktree.dispose();
|
|
525
|
+
}
|
|
526
|
+
catch (error) {
|
|
527
|
+
currentResult.exitCode = 1;
|
|
528
|
+
currentResult.errorMessage = `subagent worktree cleanup failed: ${error instanceof Error ? error.message : String(error)}`;
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
}
|
|
450
532
|
if (tmpPromptPath)
|
|
451
533
|
try {
|
|
452
534
|
fs.unlinkSync(tmpPromptPath);
|
|
@@ -462,6 +544,7 @@ async function runSingleAgent(defaultCwd, agents, agentName, task, cwd, step, mo
|
|
|
462
544
|
/* ignore */
|
|
463
545
|
}
|
|
464
546
|
}
|
|
547
|
+
return currentResult;
|
|
465
548
|
}
|
|
466
549
|
const TaskItem = Type.Object({
|
|
467
550
|
agent: Type.String({ description: "Name of the agent to invoke" }),
|
|
@@ -470,6 +553,9 @@ const TaskItem = Type.Object({
|
|
|
470
553
|
description: "Optional canonical provider/model for this child. Defaults to the parent provider/model.",
|
|
471
554
|
})),
|
|
472
555
|
cwd: Type.Optional(Type.String({ description: "Working directory for the agent process" })),
|
|
556
|
+
isolation: Type.Optional(StringEnum(["shared", "worktree"], {
|
|
557
|
+
description: "Filesystem isolation for this child. 'worktree' runs it in a disposable git worktree and merges its changes back; the caller's policy sets this, not the model.",
|
|
558
|
+
})),
|
|
473
559
|
});
|
|
474
560
|
const ChainItem = Type.Object({
|
|
475
561
|
agent: Type.String({ description: "Name of the agent to invoke" }),
|
|
@@ -494,6 +580,9 @@ const SubagentParams = Type.Object({
|
|
|
494
580
|
agentScope: Type.Optional(AgentScopeSchema),
|
|
495
581
|
confirmProjectAgents: Type.Optional(Type.Boolean({ description: "Prompt before running project-local agents. Default: true.", default: true })),
|
|
496
582
|
cwd: Type.Optional(Type.String({ description: "Working directory for the agent process (single mode)" })),
|
|
583
|
+
isolation: Type.Optional(StringEnum(["shared", "worktree"], {
|
|
584
|
+
description: "Filesystem isolation for this child. 'worktree' runs it in a disposable git worktree and merges its changes back; the caller's policy sets this, not the model.",
|
|
585
|
+
})),
|
|
497
586
|
});
|
|
498
587
|
export function createSubagentToolDefinition(cwd) {
|
|
499
588
|
return {
|
|
@@ -502,6 +591,7 @@ export function createSubagentToolDefinition(cwd) {
|
|
|
502
591
|
description: [
|
|
503
592
|
"Delegate tasks to specialized subagents with isolated context.",
|
|
504
593
|
"Modes: single (agent + task), parallel (tasks array), chain (sequential with {previous} placeholder).",
|
|
594
|
+
"The optional isolation field is policy-set for each child.",
|
|
505
595
|
`Default agent scope is "user" (from ${path.join(getAgentDir(), "agents")}).`,
|
|
506
596
|
`To enable project-local agents in ${CONFIG_DIR_NAME}/agents, set agentScope: "both" (or "project").`,
|
|
507
597
|
].join(" "),
|
|
@@ -510,7 +600,7 @@ export function createSubagentToolDefinition(cwd) {
|
|
|
510
600
|
"Use subagent to fan out independent work in parallel (tasks array) or run multi-step pipelines (chain). Subagents run with isolated context and cannot delegate further.",
|
|
511
601
|
],
|
|
512
602
|
parameters: SubagentParams,
|
|
513
|
-
async execute(
|
|
603
|
+
async execute(toolCallId, params, signal, onUpdate, ctx) {
|
|
514
604
|
const agentScope = params.agentScope ?? "user";
|
|
515
605
|
const defaultCwd = ctx?.cwd ?? cwd;
|
|
516
606
|
const parentModel = ctx?.model;
|
|
@@ -539,6 +629,7 @@ export function createSubagentToolDefinition(cwd) {
|
|
|
539
629
|
usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, contextTokens: 0, turns: 0 },
|
|
540
630
|
errorMessage,
|
|
541
631
|
step: child.step,
|
|
632
|
+
isolation: "shared",
|
|
542
633
|
});
|
|
543
634
|
const stubAllChildren = (errorMessage) => requestedChildren().map((child) => stubResult(child, errorMessage));
|
|
544
635
|
const makeDetails = (mode) => (results) => ({
|
|
@@ -631,7 +722,7 @@ export function createSubagentToolDefinition(cwd) {
|
|
|
631
722
|
}
|
|
632
723
|
}
|
|
633
724
|
: undefined;
|
|
634
|
-
const result = await runSingleAgent(defaultCwd, agents, step.agent, taskWithContext, step.cwd, i + 1, step.model, parentModel, signal, chainUpdate, makeDetails("chain"));
|
|
725
|
+
const result = await runSingleAgent(defaultCwd, agents, step.agent, taskWithContext, step.cwd, undefined, i + 1, step.model, parentModel, signal, chainUpdate, makeDetails("chain"), toolCallId, i);
|
|
635
726
|
results.push(result);
|
|
636
727
|
const isError = isFailedResult(result);
|
|
637
728
|
if (isError) {
|
|
@@ -676,6 +767,7 @@ export function createSubagentToolDefinition(cwd) {
|
|
|
676
767
|
messages: [],
|
|
677
768
|
stderr: "",
|
|
678
769
|
usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, contextTokens: 0, turns: 0 },
|
|
770
|
+
isolation: "shared",
|
|
679
771
|
};
|
|
680
772
|
}
|
|
681
773
|
const emitParallelUpdate = () => {
|
|
@@ -691,18 +783,18 @@ export function createSubagentToolDefinition(cwd) {
|
|
|
691
783
|
}
|
|
692
784
|
};
|
|
693
785
|
const results = await mapWithConcurrencyLimit(params.tasks, MAX_CONCURRENCY, async (t, index) => {
|
|
694
|
-
const result = await runSingleAgent(defaultCwd, agents, t.agent, t.task, t.cwd, undefined, t.model, parentModel, signal,
|
|
786
|
+
const result = await runSingleAgent(defaultCwd, agents, t.agent, t.task, t.cwd, t.isolation, undefined, t.model, parentModel, signal,
|
|
695
787
|
// Per-task update callback
|
|
696
788
|
(partial) => {
|
|
697
789
|
if (partial.details?.results[0]) {
|
|
698
790
|
allResults[index] = partial.details.results[0];
|
|
699
791
|
emitParallelUpdate();
|
|
700
792
|
}
|
|
701
|
-
}, makeDetails("parallel"));
|
|
793
|
+
}, makeDetails("parallel"), toolCallId, index);
|
|
702
794
|
allResults[index] = result;
|
|
703
795
|
emitParallelUpdate();
|
|
704
796
|
return result;
|
|
705
|
-
}, signal);
|
|
797
|
+
}, signal, (t) => stubResult({ agent: t.agent, task: t.task }, "not run: subagent was aborted"));
|
|
706
798
|
const successCount = results.filter((r) => !isFailedResult(r)).length;
|
|
707
799
|
const summaries = results.map((r) => {
|
|
708
800
|
const output = truncateParallelOutput(getResultOutput(r));
|
|
@@ -719,10 +811,11 @@ export function createSubagentToolDefinition(cwd) {
|
|
|
719
811
|
},
|
|
720
812
|
],
|
|
721
813
|
details: makeDetails("parallel")(results),
|
|
814
|
+
isError: successCount !== results.length,
|
|
722
815
|
};
|
|
723
816
|
}
|
|
724
817
|
if (params.agent && params.task) {
|
|
725
|
-
const result = await runSingleAgent(defaultCwd, agents, params.agent, params.task, params.cwd, undefined, params.model, parentModel, signal, onUpdate, makeDetails("single"));
|
|
818
|
+
const result = await runSingleAgent(defaultCwd, agents, params.agent, params.task, params.cwd, params.isolation, undefined, params.model, parentModel, signal, onUpdate, makeDetails("single"), toolCallId, 0);
|
|
726
819
|
const isError = isFailedResult(result);
|
|
727
820
|
if (isError) {
|
|
728
821
|
const errorMsg = getResultOutput(result);
|