oira666_pi-subagent 0.2.24 → 0.2.26
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/README.md +1 -1
- package/agents/team-lead.md +10 -0
- package/index.ts +77 -49
- package/package.json +8 -7
- package/resume.ts +39 -7
- package/runner.ts +20 -5
- package/shims.d.ts +78 -0
- package/types.ts +2 -1
package/README.md
CHANGED
|
@@ -117,7 +117,7 @@ pi --no-subagent-prevent-cycles # allow cycles (not recommended)
|
|
|
117
117
|
|
|
118
118
|
| Env Var | Default | Description |
|
|
119
119
|
| -------------------------------- | ------- | ---------------------------------------- |
|
|
120
|
-
| `PI_SUBAGENT_MAX_PARALLEL_TASKS` | `
|
|
120
|
+
| `PI_SUBAGENT_MAX_PARALLEL_TASKS` | `30` | Max tasks per single call |
|
|
121
121
|
| `PI_SUBAGENT_MAX_CONCURRENCY` | `8` | Max subagents running simultaneously |
|
|
122
122
|
|
|
123
123
|
## Steering Running Subagents
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: team-lead
|
|
3
|
+
description: Focused on tasks managment, delegates work to its subagents. For more compex tasks.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
You are an experienced team lead, focused on tasks management. You don't do any work yourself. You delegate.
|
|
7
|
+
|
|
8
|
+
Your job is to split your task to small steps and delegate each step to your subagents for implemetation.
|
|
9
|
+
You control quality and deal with all the unexpected situations and errors.
|
|
10
|
+
|
package/index.ts
CHANGED
|
@@ -22,7 +22,7 @@ import {
|
|
|
22
22
|
SUBAGENT_RESUME_PROMPT_ENV,
|
|
23
23
|
branchEntries,
|
|
24
24
|
buildSubagentSessionDir,
|
|
25
|
-
|
|
25
|
+
findLatestResumableSubagentCalls,
|
|
26
26
|
getDefaultSubagentSessionRoot,
|
|
27
27
|
isFinishedResult,
|
|
28
28
|
parseBooleanEnv,
|
|
@@ -55,7 +55,7 @@ import {
|
|
|
55
55
|
// ---------------------------------------------------------------------------
|
|
56
56
|
|
|
57
57
|
const DEFAULT_MAX_DELEGATION_DEPTH = 3;
|
|
58
|
-
const DEFAULT_PREVENT_CYCLE_DELEGATION =
|
|
58
|
+
const DEFAULT_PREVENT_CYCLE_DELEGATION = true;
|
|
59
59
|
const DEFAULT_PROJECT_AGENT_CONFIRMATION = "ask";
|
|
60
60
|
const SUBAGENT_DEPTH_ENV = "PI_SUBAGENT_DEPTH";
|
|
61
61
|
const SUBAGENT_MAX_DEPTH_ENV = "PI_SUBAGENT_MAX_DEPTH";
|
|
@@ -355,6 +355,15 @@ function ensureSubagentToolActive(pi: ExtensionAPI): void {
|
|
|
355
355
|
}
|
|
356
356
|
}
|
|
357
357
|
|
|
358
|
+
function isRpcMode(argv: string[]): boolean {
|
|
359
|
+
for (let i = 2; i < argv.length; i++) {
|
|
360
|
+
const arg = argv[i];
|
|
361
|
+
if (arg === "--mode" && argv[i + 1] === "rpc") return true;
|
|
362
|
+
if (arg === "--mode=rpc") return true;
|
|
363
|
+
}
|
|
364
|
+
return false;
|
|
365
|
+
}
|
|
366
|
+
|
|
358
367
|
function hasCliInitialPrompt(argv: string[]): boolean {
|
|
359
368
|
for (let i = 2; i < argv.length; i++) {
|
|
360
369
|
const arg = argv[i];
|
|
@@ -381,14 +390,14 @@ const SUBAGENT_FALLBACK_MODEL_ENV = "PI_SUBAGENT_FALLBACK_MODEL";
|
|
|
381
390
|
const RESUME_INTERACTIVE_DELAY_MS = 50;
|
|
382
391
|
|
|
383
392
|
type SyntheticResumeState = {
|
|
384
|
-
|
|
393
|
+
plans: ResumableSubagentCall[];
|
|
385
394
|
phase: "tool" | "final";
|
|
386
395
|
trigger: "resumePrompt" | "nextRequest";
|
|
387
396
|
};
|
|
388
397
|
|
|
389
398
|
function clearSyntheticResumeState(): void {
|
|
390
399
|
const state = getSyntheticResumeState();
|
|
391
|
-
state.
|
|
400
|
+
state.plans = [];
|
|
392
401
|
state.phase = "tool";
|
|
393
402
|
state.trigger = "resumePrompt";
|
|
394
403
|
}
|
|
@@ -396,7 +405,7 @@ function clearSyntheticResumeState(): void {
|
|
|
396
405
|
function getSyntheticResumeState(): SyntheticResumeState {
|
|
397
406
|
const g = globalThis as any;
|
|
398
407
|
if (!g[RESUME_STATE_KEY]) {
|
|
399
|
-
g[RESUME_STATE_KEY] = {
|
|
408
|
+
g[RESUME_STATE_KEY] = { plans: [], phase: "tool", trigger: "resumePrompt" } satisfies SyntheticResumeState;
|
|
400
409
|
}
|
|
401
410
|
return g[RESUME_STATE_KEY] as SyntheticResumeState;
|
|
402
411
|
}
|
|
@@ -518,27 +527,34 @@ export default function (pi: ExtensionAPI) {
|
|
|
518
527
|
streamSimple: async (model, context, options) => {
|
|
519
528
|
const stream = createAssistantMessageEventStream();
|
|
520
529
|
const state = getSyntheticResumeState();
|
|
521
|
-
const
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
530
|
+
const discoveredPlans = state.plans.length > 0
|
|
531
|
+
? state.plans
|
|
532
|
+
: pendingResumePlans.length > 0
|
|
533
|
+
? pendingResumePlans
|
|
534
|
+
: latestSessionCtx
|
|
535
|
+
? findLatestResumableSubagentCalls(latestSessionCtx)
|
|
536
|
+
: [];
|
|
537
|
+
if (discoveredPlans.length > 0 && state.plans.length === 0) {
|
|
538
|
+
state.plans = [...discoveredPlans];
|
|
539
|
+
pendingResumePlans = [...discoveredPlans];
|
|
525
540
|
}
|
|
526
|
-
const
|
|
541
|
+
const plans = discoveredPlans;
|
|
542
|
+
const totalTaskCount = plans.reduce((sum, plan) => sum + plan.tasks.length, 0);
|
|
527
543
|
const phase = state.phase;
|
|
528
544
|
const triggerMatches =
|
|
529
545
|
state.trigger === "nextRequest" ||
|
|
530
|
-
(
|
|
531
|
-
if (
|
|
546
|
+
(totalTaskCount > 0 ? isSyntheticResumePrompt(context, totalTaskCount) : false);
|
|
547
|
+
if (plans.length > 0 && phase === "tool" && triggerMatches) {
|
|
532
548
|
state.phase = "final";
|
|
533
|
-
const
|
|
549
|
+
const toolCalls = plans.map((plan, index) => ({
|
|
534
550
|
type: "toolCall" as const,
|
|
535
|
-
id: `resume_subagent_${Date.now()}`,
|
|
551
|
+
id: `resume_subagent_${Date.now()}_${index}`,
|
|
536
552
|
name: "subagent",
|
|
537
553
|
arguments: { tasks: plan.tasks },
|
|
538
|
-
};
|
|
554
|
+
}));
|
|
539
555
|
const message = {
|
|
540
556
|
role: "assistant" as const,
|
|
541
|
-
content:
|
|
557
|
+
content: toolCalls,
|
|
542
558
|
api: model.api,
|
|
543
559
|
provider: model.provider,
|
|
544
560
|
model: model.id,
|
|
@@ -548,13 +564,15 @@ export default function (pi: ExtensionAPI) {
|
|
|
548
564
|
};
|
|
549
565
|
queueMicrotask(() => {
|
|
550
566
|
stream.push({ type: "start", partial: message });
|
|
551
|
-
|
|
552
|
-
|
|
567
|
+
toolCalls.forEach((toolCall, contentIndex) => {
|
|
568
|
+
stream.push({ type: "toolcall_start", contentIndex, partial: message });
|
|
569
|
+
stream.push({ type: "toolcall_end", contentIndex, toolCall, partial: message });
|
|
570
|
+
});
|
|
553
571
|
stream.push({ type: "done", reason: "toolUse", message });
|
|
554
572
|
stream.end(message);
|
|
555
573
|
|
|
556
574
|
// The synthetic provider's only job is to inject the resumed subagent
|
|
557
|
-
// tool
|
|
575
|
+
// tool calls. Restore the real model immediately after that handoff so
|
|
558
576
|
// the TUI does not appear stuck on `pi-subagent-resume` while the
|
|
559
577
|
// subagent tool execution is still running.
|
|
560
578
|
void restoreVisibleModelForResume();
|
|
@@ -572,8 +590,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
572
590
|
const fallback = await streamWithRealModelFallback(context, options, restore ?? lastRestorableModel);
|
|
573
591
|
if (fallback) return fallback;
|
|
574
592
|
|
|
575
|
-
if (!(
|
|
576
|
-
state.
|
|
593
|
+
if (!(plans.length > 0 && phase === "tool")) {
|
|
594
|
+
state.plans = [];
|
|
577
595
|
state.phase = "tool";
|
|
578
596
|
}
|
|
579
597
|
const message = {
|
|
@@ -622,7 +640,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
622
640
|
let discoveredAgents: AgentConfig[] = [];
|
|
623
641
|
let currentSessionId = "ephemeral";
|
|
624
642
|
let currentSubagentSessionRoot = "";
|
|
625
|
-
let
|
|
643
|
+
let pendingResumePlans: ResumableSubagentCall[] = [];
|
|
626
644
|
let modelToRestoreAfterResume: any | undefined;
|
|
627
645
|
const approvedProjectAgentDirsForSession = new Set<string>();
|
|
628
646
|
const activeSubagents = new Map<number, { agent: string; task: string; handle: RunningSubagentHandle }>();
|
|
@@ -851,7 +869,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
851
869
|
return true;
|
|
852
870
|
}
|
|
853
871
|
|
|
854
|
-
function updateLatestBroadcastTargets(details: SubagentDetails | undefined): void {
|
|
872
|
+
function updateLatestBroadcastTargets(details: SubagentDetails | undefined, topLevelBaseId = 1): void {
|
|
855
873
|
latestBroadcastTargets.all = [];
|
|
856
874
|
latestBroadcastTargets.youngest = [];
|
|
857
875
|
if (!details) {
|
|
@@ -863,7 +881,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
863
881
|
return;
|
|
864
882
|
}
|
|
865
883
|
details.results.forEach((result, index) => {
|
|
866
|
-
collectRunningBroadcastTargetsFromResult(result, [
|
|
884
|
+
collectRunningBroadcastTargetsFromResult(result, [topLevelBaseId + index], latestBroadcastTargets);
|
|
867
885
|
});
|
|
868
886
|
const dedupe = (targets: BroadcastTarget[]) =>
|
|
869
887
|
Array.from(new Map(targets.map((target) => [target.display, target])).values())
|
|
@@ -976,7 +994,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
976
994
|
async function restoreModelAfterResumeFailure(ctx?: { ui?: { notify?: (message: string, type?: "info" | "warning" | "error") => void } }) {
|
|
977
995
|
const restore = modelToRestoreAfterResume;
|
|
978
996
|
modelToRestoreAfterResume = undefined;
|
|
979
|
-
|
|
997
|
+
pendingResumePlans = [];
|
|
980
998
|
clearSyntheticResumeState();
|
|
981
999
|
if (!restore) return;
|
|
982
1000
|
try {
|
|
@@ -1024,15 +1042,17 @@ export default function (pi: ExtensionAPI) {
|
|
|
1024
1042
|
const resumeDisabled = parseBooleanEnv(process.env[SUBAGENT_RESUME_DISABLE_ENV]) === true;
|
|
1025
1043
|
if (resumeDisabled || (event.reason !== "resume" && event.reason !== "startup")) return;
|
|
1026
1044
|
|
|
1027
|
-
const
|
|
1028
|
-
if (
|
|
1045
|
+
const plans = findLatestResumableSubagentCalls(ctx);
|
|
1046
|
+
if (plans.length === 0) return;
|
|
1047
|
+
const totalTaskCount = plans.reduce((sum, plan) => sum + plan.tasks.length, 0);
|
|
1029
1048
|
|
|
1030
1049
|
let shouldResume = true;
|
|
1031
1050
|
const shouldPrompt = parseBooleanEnv(process.env[SUBAGENT_RESUME_PROMPT_ENV]) !== false;
|
|
1032
|
-
|
|
1051
|
+
const rpcMode = isRpcMode(process.argv);
|
|
1052
|
+
if (ctx.hasUI && !rpcMode && shouldPrompt) {
|
|
1033
1053
|
shouldResume = await ctx.ui.confirm(
|
|
1034
1054
|
"Resume subagents?",
|
|
1035
|
-
`The resumed session has an unfinished subagent call
|
|
1055
|
+
`The resumed session has ${plans.length === 1 ? "an" : String(plans.length)} unfinished subagent call${plans.length === 1 ? "" : "s"} (${totalTaskCount} task${totalTaskCount === 1 ? "" : "s"}). Resume from saved subagent sessions?`,
|
|
1036
1056
|
);
|
|
1037
1057
|
}
|
|
1038
1058
|
if (!shouldResume) {
|
|
@@ -1057,11 +1077,17 @@ export default function (pi: ExtensionAPI) {
|
|
|
1057
1077
|
return;
|
|
1058
1078
|
}
|
|
1059
1079
|
|
|
1060
|
-
|
|
1080
|
+
pendingResumePlans = [...plans];
|
|
1061
1081
|
const resumeState = getSyntheticResumeState();
|
|
1062
|
-
resumeState.
|
|
1082
|
+
resumeState.plans = [...plans];
|
|
1063
1083
|
resumeState.phase = "tool";
|
|
1064
|
-
|
|
1084
|
+
// Headless subprocess/RPC subagents cannot answer a visible resume
|
|
1085
|
+
// prompt. They already receive an initial RPC prompt from the parent
|
|
1086
|
+
// runner, so inject the synthetic resume tool call into that next model
|
|
1087
|
+
// request. Interactive top-level sessions keep using a visible prompt so
|
|
1088
|
+
// the user sees exactly what is happening.
|
|
1089
|
+
const injectOnNextRequest = rpcMode || hasCliInitialPrompt(process.argv) || !ctx.hasUI;
|
|
1090
|
+
resumeState.trigger = injectOnNextRequest ? "nextRequest" : "resumePrompt";
|
|
1065
1091
|
modelToRestoreAfterResume = restorableModel ?? ctx.model;
|
|
1066
1092
|
resumeModelRegistry = ctx.modelRegistry;
|
|
1067
1093
|
ensureSubagentToolActive(pi);
|
|
@@ -1076,15 +1102,15 @@ export default function (pi: ExtensionAPI) {
|
|
|
1076
1102
|
// to be sent. That prompt will be answered by the synthetic provider with
|
|
1077
1103
|
// a real assistant subagent tool call. In interactive mode, submit a short
|
|
1078
1104
|
// visible prompt that triggers the same synthetic provider path.
|
|
1079
|
-
if (
|
|
1080
|
-
if (ctx.hasUI) ctx.ui.notify(`Resuming ${
|
|
1105
|
+
if (injectOnNextRequest) {
|
|
1106
|
+
if (ctx.hasUI) ctx.ui.notify(`Resuming ${totalTaskCount} subagents...`, "info");
|
|
1081
1107
|
} else {
|
|
1082
1108
|
// Do not start the synthetic resume turn from session_start. Pi renders
|
|
1083
1109
|
// the resumed chat only after session_start/resources_discover complete;
|
|
1084
1110
|
// starting now lets that render wipe out the live tool component, so no
|
|
1085
1111
|
// real-time updates appear. Queue it for resources_discover instead,
|
|
1086
1112
|
// which is the last extension hook before the initial chat render.
|
|
1087
|
-
pendingInteractiveResumePrompt = `Resuming ${
|
|
1113
|
+
pendingInteractiveResumePrompt = `Resuming ${totalTaskCount} subagents...`;
|
|
1088
1114
|
}
|
|
1089
1115
|
} catch (err) {
|
|
1090
1116
|
console.error("[pi-subagent] Error in session_start:", err);
|
|
@@ -1198,9 +1224,7 @@ calls one after another. Do NOT put dependent tasks in the same array.
|
|
|
1198
1224
|
|
|
1199
1225
|
async execute(toolCallId, params, signal, onUpdate, ctx) {
|
|
1200
1226
|
try {
|
|
1201
|
-
activeSubagents.clear();
|
|
1202
1227
|
updateLatestBroadcastTargets(undefined);
|
|
1203
|
-
nextActiveSubagentId = 1;
|
|
1204
1228
|
const discovery = discoverAgents(ctx.cwd, "both");
|
|
1205
1229
|
const { agents } = discovery;
|
|
1206
1230
|
|
|
@@ -1223,9 +1247,11 @@ calls one after another. Do NOT put dependent tasks in the same array.
|
|
|
1223
1247
|
}
|
|
1224
1248
|
|
|
1225
1249
|
const executionMode = tasks.length === 1 ? "single" : "parallel";
|
|
1250
|
+
const topLevelBaseId = nextActiveSubagentId;
|
|
1251
|
+
nextActiveSubagentId += tasks.length;
|
|
1226
1252
|
const trackedOnUpdate = (partial: any) => {
|
|
1227
1253
|
if (isSubagentDetails(partial?.details)) {
|
|
1228
|
-
updateLatestBroadcastTargets(partial.details);
|
|
1254
|
+
updateLatestBroadcastTargets(partial.details, topLevelBaseId);
|
|
1229
1255
|
emitNestedProgressToParent(toolCallId, partial.details);
|
|
1230
1256
|
}
|
|
1231
1257
|
onUpdate?.(partial);
|
|
@@ -1275,7 +1301,7 @@ This guard prevents self-recursion and cyclic handoffs (for example A -> B -> A)
|
|
|
1275
1301
|
projectAgentConfirmationSetting === "ask" &&
|
|
1276
1302
|
!approvedProjectAgentDirsForSession.has(projectAgentSessionKey);
|
|
1277
1303
|
if (shouldConfirmProjectAgents) {
|
|
1278
|
-
if (ctx.hasUI) {
|
|
1304
|
+
if (ctx.hasUI && !isRpcMode(process.argv)) {
|
|
1279
1305
|
const approval = await confirmProjectAgentsIfNeeded(
|
|
1280
1306
|
requestedProjectAgents,
|
|
1281
1307
|
discovery.projectAgentsDir,
|
|
@@ -1311,12 +1337,10 @@ This guard prevents self-recursion and cyclic handoffs (for example A -> B -> A)
|
|
|
1311
1337
|
}
|
|
1312
1338
|
}
|
|
1313
1339
|
|
|
1314
|
-
const
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
if (resumePlan) {
|
|
1319
|
-
pendingResumePlan = null;
|
|
1340
|
+
const resumePlanIndex = pendingResumePlans.findIndex((plan) => sameTasks(plan.tasks, tasks));
|
|
1341
|
+
const resumePlan = resumePlanIndex >= 0 ? pendingResumePlans[resumePlanIndex] : null;
|
|
1342
|
+
if (resumePlanIndex >= 0) {
|
|
1343
|
+
pendingResumePlans.splice(resumePlanIndex, 1);
|
|
1320
1344
|
}
|
|
1321
1345
|
|
|
1322
1346
|
if (tasks.length === 1) {
|
|
@@ -1337,6 +1361,7 @@ This guard prevents self-recursion and cyclic handoffs (for example A -> B -> A)
|
|
|
1337
1361
|
// defaulting to whatever settings.json says at spawn time, which
|
|
1338
1362
|
// can change while the parent session is long-running.
|
|
1339
1363
|
formatModelFlag(modelToRestoreAfterResume ?? ctx.model ?? lastRestorableModel),
|
|
1364
|
+
topLevelBaseId,
|
|
1340
1365
|
);
|
|
1341
1366
|
}
|
|
1342
1367
|
|
|
@@ -1351,6 +1376,7 @@ This guard prevents self-recursion and cyclic handoffs (for example A -> B -> A)
|
|
|
1351
1376
|
(index) => getSessionDirForTask(resumePlan?.previousToolCallId ?? toolCallId, index),
|
|
1352
1377
|
!!resumePlan,
|
|
1353
1378
|
formatModelFlag(modelToRestoreAfterResume ?? ctx.model ?? lastRestorableModel),
|
|
1379
|
+
topLevelBaseId,
|
|
1354
1380
|
);
|
|
1355
1381
|
} catch (err) {
|
|
1356
1382
|
const msg = err instanceof Error ? err.message : String(err);
|
|
@@ -1392,7 +1418,8 @@ This guard prevents self-recursion and cyclic handoffs (for example A -> B -> A)
|
|
|
1392
1418
|
previousResult: SingleResult | undefined,
|
|
1393
1419
|
sessionDir: string,
|
|
1394
1420
|
resumeExistingSession: boolean,
|
|
1395
|
-
fallbackModel
|
|
1421
|
+
fallbackModel: string | undefined,
|
|
1422
|
+
topLevelBaseId: number,
|
|
1396
1423
|
) {
|
|
1397
1424
|
if (previousResult && isFinishedResult(previousResult)) {
|
|
1398
1425
|
return {
|
|
@@ -1425,7 +1452,7 @@ This guard prevents self-recursion and cyclic handoffs (for example A -> B -> A)
|
|
|
1425
1452
|
initialResult: previousResult,
|
|
1426
1453
|
fallbackModel,
|
|
1427
1454
|
onHandle: (handle) => {
|
|
1428
|
-
activeId =
|
|
1455
|
+
activeId = topLevelBaseId;
|
|
1429
1456
|
activeSubagents.set(activeId, { agent: agentName, task, handle });
|
|
1430
1457
|
updateLatestBroadcastTargets(undefined);
|
|
1431
1458
|
},
|
|
@@ -1473,7 +1500,8 @@ This guard prevents self-recursion and cyclic handoffs (for example A -> B -> A)
|
|
|
1473
1500
|
resumeResults: SingleResult[] | undefined,
|
|
1474
1501
|
getSessionDir: (index: number) => string,
|
|
1475
1502
|
resumeExistingSessions: boolean,
|
|
1476
|
-
fallbackModel
|
|
1503
|
+
fallbackModel: string | undefined,
|
|
1504
|
+
topLevelBaseId: number,
|
|
1477
1505
|
) {
|
|
1478
1506
|
const taskIds = new Map<number, number>();
|
|
1479
1507
|
try {
|
|
@@ -1494,7 +1522,7 @@ This guard prevents self-recursion and cyclic handoffs (for example A -> B -> A)
|
|
|
1494
1522
|
currentSubagentSessionRoot,
|
|
1495
1523
|
fallbackModel,
|
|
1496
1524
|
(index, task, handle) => {
|
|
1497
|
-
const id =
|
|
1525
|
+
const id = topLevelBaseId + index;
|
|
1498
1526
|
taskIds.set(index, id);
|
|
1499
1527
|
activeSubagents.set(id, { agent: task.agent, task: task.task, handle });
|
|
1500
1528
|
updateLatestBroadcastTargets(undefined);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "oira666_pi-subagent",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.26",
|
|
4
4
|
"description": "Subagent extension for Pi coding agent. Delegate tasks to specialized agents.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.ts",
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
"render.ts",
|
|
14
14
|
"tree.ts",
|
|
15
15
|
"types.ts",
|
|
16
|
+
"shims.d.ts",
|
|
16
17
|
"agents/*.md",
|
|
17
18
|
"README.md",
|
|
18
19
|
"LICENSE"
|
|
@@ -41,7 +42,8 @@
|
|
|
41
42
|
},
|
|
42
43
|
"license": "MIT",
|
|
43
44
|
"scripts": {
|
|
44
|
-
"test": "node --import tsx/esm --test tests/*.test.ts test/*.test.ts"
|
|
45
|
+
"test": "node --import tsx/esm --test tests/*.test.ts test/*.test.ts",
|
|
46
|
+
"typecheck": "tsc --noEmit"
|
|
45
47
|
},
|
|
46
48
|
"devDependencies": {
|
|
47
49
|
"@types/node": "^25.2.3",
|
|
@@ -52,8 +54,7 @@
|
|
|
52
54
|
"@mariozechner/pi-agent-core": ">=0.37.0",
|
|
53
55
|
"@mariozechner/pi-ai": ">=0.37.0",
|
|
54
56
|
"@mariozechner/pi-coding-agent": ">=0.37.0",
|
|
55
|
-
"@mariozechner/pi-tui": ">=0.37.0"
|
|
56
|
-
"@sinclair/typebox": ">=0.34.0"
|
|
57
|
+
"@mariozechner/pi-tui": ">=0.37.0"
|
|
57
58
|
},
|
|
58
59
|
"peerDependenciesMeta": {
|
|
59
60
|
"@mariozechner/pi-agent-core": {
|
|
@@ -67,9 +68,9 @@
|
|
|
67
68
|
},
|
|
68
69
|
"@mariozechner/pi-ai": {
|
|
69
70
|
"optional": true
|
|
70
|
-
},
|
|
71
|
-
"@sinclair/typebox": {
|
|
72
|
-
"optional": true
|
|
73
71
|
}
|
|
72
|
+
},
|
|
73
|
+
"dependencies": {
|
|
74
|
+
"@sinclair/typebox": ">=0.34.0"
|
|
74
75
|
}
|
|
75
76
|
}
|
package/resume.ts
CHANGED
|
@@ -54,9 +54,13 @@ function getSubagentToolCalls(message: any): Array<{ id: string; args: any }> {
|
|
|
54
54
|
if (!message || message.role !== "assistant" || !Array.isArray(message.content)) return [];
|
|
55
55
|
const calls: Array<{ id: string; args: any }> = [];
|
|
56
56
|
for (const part of message.content) {
|
|
57
|
-
if (part?.type
|
|
58
|
-
|
|
59
|
-
|
|
57
|
+
if (part?.type !== "toolCall" || part.name !== "subagent") continue;
|
|
58
|
+
const id = typeof part.id === "string"
|
|
59
|
+
? part.id
|
|
60
|
+
: typeof part.toolCallId === "string"
|
|
61
|
+
? part.toolCallId
|
|
62
|
+
: undefined;
|
|
63
|
+
if (id) calls.push({ id, args: part.arguments });
|
|
60
64
|
}
|
|
61
65
|
return calls;
|
|
62
66
|
}
|
|
@@ -172,7 +176,7 @@ function hasOnlyIgnorableTrailingEntries(entries: SessionEntry[], activityOrder:
|
|
|
172
176
|
return true;
|
|
173
177
|
}
|
|
174
178
|
|
|
175
|
-
export function
|
|
179
|
+
export function findLatestResumableSubagentCalls(ctx: ExtensionContext): ResumableSubagentCall[] {
|
|
176
180
|
const entries = branchEntries(ctx);
|
|
177
181
|
const calls = new Map<string, { tasks: Array<{ agent: string; task: string }>; order: number }>();
|
|
178
182
|
const results = new Map<string, { details?: SubagentDetails; isError: boolean; order: number }>();
|
|
@@ -193,7 +197,7 @@ export function findLatestResumableSubagentCall(ctx: ExtensionContext): Resumabl
|
|
|
193
197
|
}
|
|
194
198
|
});
|
|
195
199
|
|
|
196
|
-
const candidates: Array<ResumableSubagentCall & { activityOrder: number }> = [];
|
|
200
|
+
const candidates: Array<ResumableSubagentCall & { callOrder: number; activityOrder: number }> = [];
|
|
197
201
|
for (const [toolCallId, call] of calls) {
|
|
198
202
|
const result = results.get(toolCallId);
|
|
199
203
|
const unfinished = !result || result.isError || hasUnfinishedResults(result.details, call.tasks.length);
|
|
@@ -202,13 +206,41 @@ export function findLatestResumableSubagentCall(ctx: ExtensionContext): Resumabl
|
|
|
202
206
|
previousToolCallId: toolCallId,
|
|
203
207
|
tasks: call.tasks,
|
|
204
208
|
details: result?.details,
|
|
209
|
+
callOrder: call.order,
|
|
205
210
|
activityOrder: result?.order ?? call.order,
|
|
206
211
|
});
|
|
207
212
|
}
|
|
208
213
|
|
|
209
214
|
const latest = candidates.sort((a, b) => a.activityOrder - b.activityOrder).at(-1);
|
|
210
|
-
if (!latest
|
|
211
|
-
|
|
215
|
+
if (!latest) return [];
|
|
216
|
+
|
|
217
|
+
// If one assistant message issued several subagent tool calls, Pi records
|
|
218
|
+
// separate tool results for them. Resuming only the last one leaves sibling
|
|
219
|
+
// subagents permanently abandoned. Resume every unfinished call from that
|
|
220
|
+
// same assistant message as one synthetic assistant turn.
|
|
221
|
+
const batch = candidates
|
|
222
|
+
.filter((candidate) => candidate.callOrder === latest.callOrder)
|
|
223
|
+
.sort((a, b) => a.activityOrder - b.activityOrder);
|
|
224
|
+
const siblingResultOrders = Array.from(calls.entries())
|
|
225
|
+
.filter(([, call]) => call.order === latest.callOrder)
|
|
226
|
+
.map(([toolCallId]) => results.get(toolCallId)?.order)
|
|
227
|
+
.filter((order): order is number => typeof order === "number");
|
|
228
|
+
const latestBatchActivity = Math.max(
|
|
229
|
+
latest.callOrder,
|
|
230
|
+
...batch.map((candidate) => candidate.activityOrder),
|
|
231
|
+
...siblingResultOrders,
|
|
232
|
+
);
|
|
233
|
+
if (!hasOnlyIgnorableTrailingEntries(entries, latestBatchActivity)) return [];
|
|
234
|
+
|
|
235
|
+
return batch.map(({ previousToolCallId, tasks, details }) => ({
|
|
236
|
+
previousToolCallId,
|
|
237
|
+
tasks,
|
|
238
|
+
details,
|
|
239
|
+
}));
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
export function findLatestResumableSubagentCall(ctx: ExtensionContext): ResumableSubagentCall | null {
|
|
243
|
+
return findLatestResumableSubagentCalls(ctx).at(-1) ?? null;
|
|
212
244
|
}
|
|
213
245
|
|
|
214
246
|
export function sameTasks(
|
package/runner.ts
CHANGED
|
@@ -62,6 +62,7 @@ function endedWithSyntheticResumeFailure(messages: Message[]): boolean {
|
|
|
62
62
|
const hasToolCall = content.some((part: any) => part?.type === "toolCall");
|
|
63
63
|
return !handedOffToRealModel && !hasToolCall;
|
|
64
64
|
}
|
|
65
|
+
|
|
65
66
|
const SUBAGENT_DEPTH_ENV = "PI_SUBAGENT_DEPTH";
|
|
66
67
|
const SUBAGENT_MAX_DEPTH_ENV = "PI_SUBAGENT_MAX_DEPTH";
|
|
67
68
|
const SUBAGENT_STACK_ENV = "PI_SUBAGENT_STACK";
|
|
@@ -620,6 +621,8 @@ export async function runAgentSubprocess(opts: RunAgentOptions): Promise<SingleR
|
|
|
620
621
|
|
|
621
622
|
const shouldContinueSession = resumeSession && (!sessionDir || sessionDirExists(sessionDir));
|
|
622
623
|
|
|
624
|
+
const initialMessageCount = initialResult?.messages?.length ?? 0;
|
|
625
|
+
|
|
623
626
|
const result: SingleResult = {
|
|
624
627
|
agent: agentName,
|
|
625
628
|
agentSource: agent.source,
|
|
@@ -704,6 +707,7 @@ export async function runAgentSubprocess(opts: RunAgentOptions): Promise<SingleR
|
|
|
704
707
|
let hangTimer: ReturnType<typeof setTimeout> | undefined;
|
|
705
708
|
let startupTimer: ReturnType<typeof setTimeout> | undefined;
|
|
706
709
|
let receivedFirstEvent = false;
|
|
710
|
+
let forcedExitCode: number | undefined;
|
|
707
711
|
|
|
708
712
|
const sendRpc = (command: Record<string, unknown>) => {
|
|
709
713
|
proc.stdin?.write(`${JSON.stringify(command)}\n`);
|
|
@@ -774,9 +778,9 @@ export async function runAgentSubprocess(opts: RunAgentOptions): Promise<SingleR
|
|
|
774
778
|
let event: any;
|
|
775
779
|
try { event = JSON.parse(line); } catch { event = null; }
|
|
776
780
|
if (event?.type === "agent_end") {
|
|
777
|
-
if (result.exitCode === -1) result.exitCode = 0;
|
|
781
|
+
if (result.exitCode === -1) result.exitCode = forcedExitCode ?? 0;
|
|
778
782
|
try { proc.kill("SIGTERM"); } catch { /* already dead */ }
|
|
779
|
-
doResolve(0);
|
|
783
|
+
doResolve(forcedExitCode ?? 0);
|
|
780
784
|
return;
|
|
781
785
|
}
|
|
782
786
|
const accepted = processJsonLine(line, result);
|
|
@@ -818,7 +822,11 @@ export async function runAgentSubprocess(opts: RunAgentOptions): Promise<SingleR
|
|
|
818
822
|
if (startupTimeoutMs > 0) {
|
|
819
823
|
startupTimer = setTimeout(() => {
|
|
820
824
|
if (resolved || receivedFirstEvent) return;
|
|
821
|
-
|
|
825
|
+
const message = `Subagent startup timeout: no JSON output after ${startupTimeoutMs}ms.`;
|
|
826
|
+
forcedExitCode = 1;
|
|
827
|
+
result.stopReason = "error";
|
|
828
|
+
result.errorMessage = message;
|
|
829
|
+
result.stderr += `\n[pi-subagent] Killed: ${message}`;
|
|
822
830
|
try { proc.kill("SIGTERM"); } catch { /* already dead */ }
|
|
823
831
|
setTimeout(() => {
|
|
824
832
|
if (!resolved) {
|
|
@@ -840,13 +848,13 @@ export async function runAgentSubprocess(opts: RunAgentOptions): Promise<SingleR
|
|
|
840
848
|
});
|
|
841
849
|
|
|
842
850
|
proc.on("close", (code) => {
|
|
843
|
-
doResolve(code ?? 0);
|
|
851
|
+
doResolve(forcedExitCode ?? code ?? 0);
|
|
844
852
|
});
|
|
845
853
|
|
|
846
854
|
proc.on("exit", (code) => {
|
|
847
855
|
// If the process exits, resolve as soon as possible.
|
|
848
856
|
// Give a tiny grace period for any remaining buffered stdout data.
|
|
849
|
-
setTimeout(() => doResolve(code ?? 0), 100);
|
|
857
|
+
setTimeout(() => doResolve(forcedExitCode ?? code ?? 0), 100);
|
|
850
858
|
});
|
|
851
859
|
|
|
852
860
|
proc.on("error", (err) => {
|
|
@@ -879,6 +887,13 @@ export async function runAgentSubprocess(opts: RunAgentOptions): Promise<SingleR
|
|
|
879
887
|
if (!result.stderr.trim()) result.stderr = "Subagent was aborted.";
|
|
880
888
|
}
|
|
881
889
|
|
|
890
|
+
if (result.exitCode === 0 && shouldContinueSession && result.messages.length <= initialMessageCount) {
|
|
891
|
+
result.exitCode = 1;
|
|
892
|
+
result.stopReason = "error";
|
|
893
|
+
result.errorMessage = "Subagent resume made no progress: resumed subprocess exited without producing any new messages.";
|
|
894
|
+
if (!result.stderr.trim()) result.stderr = result.errorMessage;
|
|
895
|
+
}
|
|
896
|
+
|
|
882
897
|
if (result.exitCode === 0 && endedWithSyntheticResumeFailure(result.messages)) {
|
|
883
898
|
result.exitCode = 1;
|
|
884
899
|
result.stopReason = "error";
|
package/shims.d.ts
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
declare module "@mariozechner/pi-ai" {
|
|
2
|
+
export interface ModelUsage {
|
|
3
|
+
input?: number;
|
|
4
|
+
output?: number;
|
|
5
|
+
cacheRead?: number;
|
|
6
|
+
cacheWrite?: number;
|
|
7
|
+
totalTokens?: number;
|
|
8
|
+
cost?: number;
|
|
9
|
+
contextTokens?: number;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export type Message = any;
|
|
13
|
+
|
|
14
|
+
export function createAssistantMessageEventStream(): any;
|
|
15
|
+
export function streamSimple(model: any, context: any, options: any): any;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
declare module "@mariozechner/pi-agent-core" {
|
|
19
|
+
export interface AgentToolResult<TDetails = unknown> {
|
|
20
|
+
content: Array<{ type: string; text?: string }>;
|
|
21
|
+
details?: TDetails;
|
|
22
|
+
isError?: boolean;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
declare module "@mariozechner/pi-coding-agent" {
|
|
27
|
+
export interface ExtensionContext {
|
|
28
|
+
cwd: string;
|
|
29
|
+
model?: any;
|
|
30
|
+
hasUI?: boolean;
|
|
31
|
+
ui?: any;
|
|
32
|
+
sessionManager: {
|
|
33
|
+
getEntries: () => any[];
|
|
34
|
+
getSessionDir: () => string | undefined;
|
|
35
|
+
getLeafId: () => string | undefined;
|
|
36
|
+
getBranch: (leafId: string) => any[] | undefined;
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface ExtensionAPI {
|
|
41
|
+
registerFlag(name: string, config: any): void;
|
|
42
|
+
getFlag(name: string): unknown;
|
|
43
|
+
registerProvider(name: string, provider: any): void;
|
|
44
|
+
registerTool(tool: any): void;
|
|
45
|
+
addBeforeAgentStart(hook: (ctx: ExtensionContext) => unknown): void;
|
|
46
|
+
addBeforeRequest(hook: (event: any, ctx: any) => unknown): void;
|
|
47
|
+
addAfterMessage(hook: (event: any, ctx: any) => unknown): void;
|
|
48
|
+
setModel(model: any): Promise<boolean> | boolean;
|
|
49
|
+
[key: string]: any;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function getAgentDir(): string;
|
|
53
|
+
export function parseFrontmatter<T extends Record<string, unknown>>(content: string): { frontmatter: T; body: string };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
declare module "@mariozechner/pi-tui" {
|
|
57
|
+
export class Text {
|
|
58
|
+
constructor(text: string, x?: number, y?: number);
|
|
59
|
+
}
|
|
60
|
+
export class Spacer {
|
|
61
|
+
constructor(size?: number);
|
|
62
|
+
}
|
|
63
|
+
export class Container {
|
|
64
|
+
addChild(child: unknown): void;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
declare module "express" {
|
|
69
|
+
export interface Request {
|
|
70
|
+
method: string;
|
|
71
|
+
path: string;
|
|
72
|
+
}
|
|
73
|
+
export interface Response {
|
|
74
|
+
statusCode: number;
|
|
75
|
+
on(event: "finish", listener: () => void): void;
|
|
76
|
+
}
|
|
77
|
+
export type NextFunction = () => void;
|
|
78
|
+
}
|
package/types.ts
CHANGED
|
@@ -242,7 +242,8 @@ export function getFinalOutput(messages: Message[]): string {
|
|
|
242
242
|
for (let i = messages.length - 1; i >= 0; i--) {
|
|
243
243
|
const msg = messages[i];
|
|
244
244
|
if (msg.role === "assistant") {
|
|
245
|
-
for (
|
|
245
|
+
for (let j = msg.content.length - 1; j >= 0; j--) {
|
|
246
|
+
const part = msg.content[j];
|
|
246
247
|
if (part.type === "text") return part.text;
|
|
247
248
|
}
|
|
248
249
|
}
|