@mystilleef/pi-subagent 0.9.0 → 0.10.2
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 +68 -40
- package/package.json +8 -8
- package/src/agent/agent-cache.ts +1 -0
- package/src/agent/agents.ts +22 -1
- package/src/child/child-events.ts +2 -0
- package/src/child/process.ts +196 -84
- package/src/child/termination.ts +316 -1
- package/src/env.d.ts +2 -0
- package/src/notification/delivery.ts +231 -0
- package/src/notification/desktop-notification.ts +73 -0
- package/src/orchestration/run-command.ts +1 -1
- package/src/orchestration/subagent-orchestrator.ts +49 -79
- package/src/output/normalize.ts +2 -2
- package/src/output/ui.ts +17 -28
- package/src/progress/progress-format.ts +111 -0
- package/src/progress/progress-state.ts +20 -108
- package/src/progress/progress.ts +29 -27
- package/src/progress/result-details.ts +96 -33
- package/src/shared/types.ts +2 -0
- package/src/shared/utils.ts +27 -6
package/src/child/process.ts
CHANGED
|
@@ -17,19 +17,19 @@ import {
|
|
|
17
17
|
import type { AgentConfig, ThinkingLevel } from "../agent/agents.js";
|
|
18
18
|
import { getFinalOutput } from "../output/ui.js";
|
|
19
19
|
import { makeToolPreview, renderToolActivity } from "../progress/progress.js";
|
|
20
|
+
import { SENSITIVE_PATTERN } from "../progress/progress-format.js";
|
|
21
|
+
import { isToolCallPart } from "../progress/progress-state.js";
|
|
20
22
|
import {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
StreamingProgress,
|
|
28
|
-
SubagentDetails,
|
|
29
|
-
ToolActivity,
|
|
23
|
+
type OnUpdateCallback,
|
|
24
|
+
type SingleResult,
|
|
25
|
+
type StreamingProgress,
|
|
26
|
+
type SubagentDetails,
|
|
27
|
+
TOOL_RESULT_FAILED_MESSAGE,
|
|
28
|
+
type ToolActivity,
|
|
30
29
|
} from "../shared/types.js";
|
|
31
30
|
import {
|
|
32
31
|
detectMessageError,
|
|
32
|
+
findLastAssistantTextMessage,
|
|
33
33
|
getPiInvocation,
|
|
34
34
|
getSubagentDepth,
|
|
35
35
|
getSubagentRuntimeLimits,
|
|
@@ -46,7 +46,11 @@ import {
|
|
|
46
46
|
} from "./child-events.js";
|
|
47
47
|
import { appendSubagentResultContract } from "./prompt-contract.js";
|
|
48
48
|
import {
|
|
49
|
+
acquireChildSleepInhibitor,
|
|
49
50
|
getProcessTreeSpawnOptions,
|
|
51
|
+
isFinitePid,
|
|
52
|
+
makeHostSleepInhibitorAdapter,
|
|
53
|
+
type SleepInhibitorHandle,
|
|
50
54
|
terminateChildProcess,
|
|
51
55
|
} from "./termination.js";
|
|
52
56
|
|
|
@@ -73,10 +77,18 @@ export function resolveThinkingLevel(
|
|
|
73
77
|
return { level: clamped, warning: mkWarning(clamped) };
|
|
74
78
|
}
|
|
75
79
|
|
|
76
|
-
export const TOOL_RESULT_FAILED_MESSAGE = "Subagent tool result failed.";
|
|
77
|
-
|
|
78
80
|
type RuntimeLimits = ReturnType<typeof getSubagentRuntimeLimits>;
|
|
79
81
|
type RuntimeResult = SingleResult & { messages: Message[] };
|
|
82
|
+
type ChildModelSettings = {
|
|
83
|
+
provider?: string | undefined;
|
|
84
|
+
id?: string | undefined;
|
|
85
|
+
};
|
|
86
|
+
type SleepInhibitorAcquirer = (pid: number) => Promise<SleepInhibitorHandle>;
|
|
87
|
+
|
|
88
|
+
type RunSingleAgentOptions = {
|
|
89
|
+
acquireSleepInhibitor?: SleepInhibitorAcquirer;
|
|
90
|
+
getOrchestratorPid?: () => unknown;
|
|
91
|
+
};
|
|
80
92
|
|
|
81
93
|
export class SubagentAbortError extends Error {
|
|
82
94
|
readonly result: SingleResult;
|
|
@@ -142,6 +154,7 @@ function resolveContextWindowTokens(msg: Message): number | undefined {
|
|
|
142
154
|
? contextWindow
|
|
143
155
|
: undefined;
|
|
144
156
|
} catch {
|
|
157
|
+
/* model lookup failures return undefined to skip context window tracking */
|
|
145
158
|
return;
|
|
146
159
|
}
|
|
147
160
|
}
|
|
@@ -153,6 +166,56 @@ function getAbortReason(signal: AbortSignal): string {
|
|
|
153
166
|
return "abort";
|
|
154
167
|
}
|
|
155
168
|
|
|
169
|
+
const hostSleepInhibitorAdapter = makeHostSleepInhibitorAdapter();
|
|
170
|
+
|
|
171
|
+
async function acquireDefaultSleepInhibitor(
|
|
172
|
+
pid: number,
|
|
173
|
+
): Promise<SleepInhibitorHandle> {
|
|
174
|
+
return acquireChildSleepInhibitor(pid, hostSleepInhibitorAdapter);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
async function acquireSubagentSleepInhibitor(
|
|
178
|
+
pid: number,
|
|
179
|
+
acquireSleepInhibitor: SleepInhibitorAcquirer,
|
|
180
|
+
): Promise<SleepInhibitorHandle | undefined> {
|
|
181
|
+
try {
|
|
182
|
+
return await acquireSleepInhibitor(pid);
|
|
183
|
+
} catch {
|
|
184
|
+
/* acquisition failures degrade gracefully to no inhibitor */
|
|
185
|
+
return undefined;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function getValidatedOrchestratorPid(
|
|
190
|
+
options: RunSingleAgentOptions,
|
|
191
|
+
): number | undefined {
|
|
192
|
+
try {
|
|
193
|
+
const pid = options.getOrchestratorPid
|
|
194
|
+
? options.getOrchestratorPid()
|
|
195
|
+
: process.pid;
|
|
196
|
+
return isFinitePid(pid) ? pid : undefined;
|
|
197
|
+
} catch {
|
|
198
|
+
return undefined;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
async function releaseSleepInhibitor(
|
|
203
|
+
handle: SleepInhibitorHandle | undefined,
|
|
204
|
+
): Promise<void> {
|
|
205
|
+
if (!handle) return;
|
|
206
|
+
try {
|
|
207
|
+
await handle.release();
|
|
208
|
+
} catch {
|
|
209
|
+
/* release failures are non-fatal */
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function startSleepInhibitorRelease(
|
|
214
|
+
acquisitionPromise: Promise<SleepInhibitorHandle | undefined>,
|
|
215
|
+
): Promise<void> {
|
|
216
|
+
return acquisitionPromise.then(releaseSleepInhibitor, () => {});
|
|
217
|
+
}
|
|
218
|
+
|
|
156
219
|
function hasCompletedAgentOutput(result: RuntimeResult): boolean {
|
|
157
220
|
if (result.finalOutput.trim()) return true;
|
|
158
221
|
return result.messages.some(
|
|
@@ -241,13 +304,20 @@ async function waitForSubagentProcess(
|
|
|
241
304
|
}
|
|
242
305
|
|
|
243
306
|
function buildModelDisplay(
|
|
244
|
-
|
|
307
|
+
effectiveModel: ChildModelSettings,
|
|
245
308
|
thinking: ThinkingLevel,
|
|
246
309
|
): string | undefined {
|
|
247
|
-
|
|
248
|
-
|
|
310
|
+
const parts: string[] = [];
|
|
311
|
+
if (effectiveModel.provider) {
|
|
312
|
+
parts.push(effectiveModel.provider);
|
|
313
|
+
}
|
|
314
|
+
if (effectiveModel.id) {
|
|
315
|
+
parts.push(effectiveModel.id);
|
|
316
|
+
}
|
|
317
|
+
if (thinking) {
|
|
318
|
+
parts.push(thinking);
|
|
249
319
|
}
|
|
250
|
-
return
|
|
320
|
+
return parts.length > 0 ? parts.join(" ・ ") : undefined;
|
|
251
321
|
}
|
|
252
322
|
|
|
253
323
|
const EMPTY_USAGE = {
|
|
@@ -366,7 +436,7 @@ async function cleanupTempPrompt(tmpPrompt: TempPrompt): Promise<void> {
|
|
|
366
436
|
await fs.promises.unlink(tmpPrompt.filePath);
|
|
367
437
|
await fs.promises.rmdir(tmpPrompt.dir);
|
|
368
438
|
} catch {
|
|
369
|
-
/*
|
|
439
|
+
/* temp file cleanup failures are non-fatal; OS will clean up eventually */
|
|
370
440
|
}
|
|
371
441
|
}
|
|
372
442
|
|
|
@@ -387,18 +457,7 @@ async function cleanupPromptSetupResult(
|
|
|
387
457
|
}
|
|
388
458
|
|
|
389
459
|
function findRecentMessagesAnchor(messages: Message[]): number {
|
|
390
|
-
|
|
391
|
-
const msg = messages[i];
|
|
392
|
-
if (
|
|
393
|
-
msg?.role === "assistant" &&
|
|
394
|
-
msg.content.some(
|
|
395
|
-
(c) => c.type === "text" && (c as { text?: string }).text?.trim(),
|
|
396
|
-
)
|
|
397
|
-
) {
|
|
398
|
-
return i;
|
|
399
|
-
}
|
|
400
|
-
}
|
|
401
|
-
return -1;
|
|
460
|
+
return findLastAssistantTextMessage(messages);
|
|
402
461
|
}
|
|
403
462
|
|
|
404
463
|
function deriveStreamingProgress(messages: Message[]): StreamingProgress {
|
|
@@ -431,6 +490,63 @@ function sanitizeProgressPreview(preview: string, toolName: string): string {
|
|
|
431
490
|
return SENSITIVE_PATTERN.test(preview) ? toolName : preview;
|
|
432
491
|
}
|
|
433
492
|
|
|
493
|
+
/**
|
|
494
|
+
* Merge incoming tool activity with existing progress activity.
|
|
495
|
+
* When tool names match, prefer richer inputSummary from incoming.
|
|
496
|
+
* Otherwise, replace entirely with incoming activity.
|
|
497
|
+
*/
|
|
498
|
+
function mergeToolActivity(
|
|
499
|
+
existing: ToolActivity | undefined,
|
|
500
|
+
incoming: ToolActivity,
|
|
501
|
+
): ToolActivity {
|
|
502
|
+
if (existing && existing.toolName === incoming.toolName) {
|
|
503
|
+
const incomingSummary = incoming.inputSummary;
|
|
504
|
+
const preferIncoming =
|
|
505
|
+
incomingSummary && incomingSummary !== incoming.toolName;
|
|
506
|
+
return {
|
|
507
|
+
...existing,
|
|
508
|
+
inputSummary: preferIncoming ? incomingSummary : existing.inputSummary,
|
|
509
|
+
instanceName: incoming.instanceName ?? existing.instanceName,
|
|
510
|
+
child: incoming.child ?? existing.child,
|
|
511
|
+
};
|
|
512
|
+
}
|
|
513
|
+
return incoming;
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
/**
|
|
517
|
+
* Apply tool activity and result completion updates to progress state.
|
|
518
|
+
* Handles merging of child events with parent activity tree.
|
|
519
|
+
*/
|
|
520
|
+
function applyActivityUpdates(
|
|
521
|
+
progress: StreamingProgress,
|
|
522
|
+
options: { toolActivity?: ToolActivity; toolResultCompleted?: boolean },
|
|
523
|
+
previousActivity?: ToolActivity,
|
|
524
|
+
): void {
|
|
525
|
+
// Preserve stored activity tree for tool-result completion signals
|
|
526
|
+
// so the parent retains nested context until newer activity arrives
|
|
527
|
+
if (options.toolResultCompleted && previousActivity) {
|
|
528
|
+
progress.activeToolActivity = previousActivity;
|
|
529
|
+
const renderedText = renderToolActivity(previousActivity);
|
|
530
|
+
if (renderedText !== undefined) progress.activityText = renderedText;
|
|
531
|
+
else delete progress.activityText;
|
|
532
|
+
}
|
|
533
|
+
// Handle parsed tool activity from child events
|
|
534
|
+
// Merge with parent activity if this is a nested update
|
|
535
|
+
if (options.toolActivity) {
|
|
536
|
+
progress.activeToolActivity = mergeToolActivity(
|
|
537
|
+
progress.activeToolActivity,
|
|
538
|
+
options.toolActivity,
|
|
539
|
+
);
|
|
540
|
+
const renderedActivity = renderToolActivity(progress.activeToolActivity);
|
|
541
|
+
if (renderedActivity !== undefined)
|
|
542
|
+
progress.activityText = renderedActivity;
|
|
543
|
+
else delete progress.activityText;
|
|
544
|
+
}
|
|
545
|
+
if (options.toolResultCompleted) {
|
|
546
|
+
progress.toolResultCompleted = true;
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
|
|
434
550
|
export function makeEmitUpdate(
|
|
435
551
|
result: RuntimeResult,
|
|
436
552
|
onUpdate: OnUpdateCallback | undefined,
|
|
@@ -448,46 +564,12 @@ export function makeEmitUpdate(
|
|
|
448
564
|
const recentMessages =
|
|
449
565
|
anchorIdx >= 0 ? msgs.slice(anchorIdx) : msgs.slice(-5);
|
|
450
566
|
const progress = deriveStreamingProgress(msgs);
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
else delete progress.activityText;
|
|
458
|
-
}
|
|
459
|
-
// Handle parsed tool activity from child events
|
|
460
|
-
// Merge with parent activity if this is a nested update
|
|
461
|
-
if (options?.toolActivity) {
|
|
462
|
-
if (
|
|
463
|
-
progress.activeToolActivity &&
|
|
464
|
-
progress.activeToolActivity.toolName === options.toolActivity.toolName
|
|
465
|
-
) {
|
|
466
|
-
// Merge: prefer parser inputSummary when non-empty and richer than bare toolName fallback
|
|
467
|
-
const incomingSummary = options.toolActivity.inputSummary;
|
|
468
|
-
const preferIncoming =
|
|
469
|
-
incomingSummary && incomingSummary !== options.toolActivity.toolName;
|
|
470
|
-
progress.activeToolActivity = {
|
|
471
|
-
...progress.activeToolActivity,
|
|
472
|
-
inputSummary: preferIncoming
|
|
473
|
-
? incomingSummary
|
|
474
|
-
: progress.activeToolActivity.inputSummary,
|
|
475
|
-
instanceName:
|
|
476
|
-
options.toolActivity.instanceName ??
|
|
477
|
-
progress.activeToolActivity.instanceName,
|
|
478
|
-
child:
|
|
479
|
-
options.toolActivity.child ?? progress.activeToolActivity.child,
|
|
480
|
-
};
|
|
481
|
-
} else {
|
|
482
|
-
progress.activeToolActivity = options.toolActivity;
|
|
483
|
-
}
|
|
484
|
-
const renderedActivity = renderToolActivity(progress.activeToolActivity);
|
|
485
|
-
if (renderedActivity !== undefined)
|
|
486
|
-
progress.activityText = renderedActivity;
|
|
487
|
-
else delete progress.activityText;
|
|
488
|
-
}
|
|
489
|
-
if (options?.toolResultCompleted) {
|
|
490
|
-
progress.toolResultCompleted = true;
|
|
567
|
+
if (options) {
|
|
568
|
+
applyActivityUpdates(
|
|
569
|
+
progress,
|
|
570
|
+
options,
|
|
571
|
+
result.progress?.activeToolActivity,
|
|
572
|
+
);
|
|
491
573
|
}
|
|
492
574
|
result.progress = progress;
|
|
493
575
|
onUpdate?.({
|
|
@@ -637,18 +719,30 @@ function setupAbortHandler(
|
|
|
637
719
|
return onAbort;
|
|
638
720
|
}
|
|
639
721
|
|
|
722
|
+
function resolveEffectiveChildModelSettings(
|
|
723
|
+
agent: AgentConfig,
|
|
724
|
+
parentModel: ChildModelSettings | undefined,
|
|
725
|
+
): ChildModelSettings {
|
|
726
|
+
return {
|
|
727
|
+
provider: agent.provider ?? parentModel?.provider,
|
|
728
|
+
id:
|
|
729
|
+
agent.model ??
|
|
730
|
+
(agent.provider === undefined ? parentModel?.id : undefined),
|
|
731
|
+
};
|
|
732
|
+
}
|
|
733
|
+
|
|
640
734
|
function buildPiArgs(
|
|
641
735
|
agent: AgentConfig,
|
|
642
736
|
task: string,
|
|
643
|
-
|
|
737
|
+
effectiveModel: ChildModelSettings,
|
|
644
738
|
thinking: ThinkingLevel,
|
|
645
739
|
resolvedSkills: { args: string[] },
|
|
646
740
|
tmpPrompt: { filePath: string } | null,
|
|
647
741
|
): string[] {
|
|
648
|
-
const args: string[] = ["--mode", "json", "-p", "--no-session"];
|
|
649
|
-
if (
|
|
650
|
-
args.push("--provider",
|
|
651
|
-
|
|
742
|
+
const args: string[] = ["--mode", "json", "-p", "--no-session", "--approve"];
|
|
743
|
+
if (effectiveModel.provider && effectiveModel.id)
|
|
744
|
+
args.push("--provider", effectiveModel.provider);
|
|
745
|
+
if (effectiveModel.id) args.push("--model", effectiveModel.id);
|
|
652
746
|
args.push("--thinking", thinking);
|
|
653
747
|
if (agent.tools?.length) args.push("--tools", agent.tools.join(","));
|
|
654
748
|
if (agent.skills) args.push("--no-skills", ...resolvedSkills.args);
|
|
@@ -722,7 +816,13 @@ async function finalizeResult(
|
|
|
722
816
|
if (agentEndTimeoutExitCode !== undefined) {
|
|
723
817
|
state.result.exitCode = agentEndTimeoutExitCode;
|
|
724
818
|
}
|
|
725
|
-
if (state.
|
|
819
|
+
if (state.result.termination?.cancelReason === "agent_end_timeout") {
|
|
820
|
+
state.result.stderr = state.result.stderr.replace(/^Terminated\r?\n/gm, "");
|
|
821
|
+
}
|
|
822
|
+
if (state.wasAborted) {
|
|
823
|
+
state.result.stderr = "";
|
|
824
|
+
throw new SubagentAbortError(state.result);
|
|
825
|
+
}
|
|
726
826
|
return state.result;
|
|
727
827
|
}
|
|
728
828
|
|
|
@@ -750,9 +850,10 @@ export async function runSingleAgent(
|
|
|
750
850
|
results: RuntimeResult[],
|
|
751
851
|
options?: { includeMessages?: boolean; recentMessages?: Message[] },
|
|
752
852
|
) => SubagentDetails,
|
|
753
|
-
parentModel:
|
|
853
|
+
parentModel: ChildModelSettings | undefined,
|
|
754
854
|
parentThinking: ThinkingLevel,
|
|
755
855
|
debugEventDiagnostics = false,
|
|
856
|
+
options: RunSingleAgentOptions = {},
|
|
756
857
|
): Promise<SingleResult> {
|
|
757
858
|
const agent = agents.find((a) => a.name === agentName);
|
|
758
859
|
if (!agent) return errorForUnknownAgent(agentName, agents, task);
|
|
@@ -768,14 +869,16 @@ export async function runSingleAgent(
|
|
|
768
869
|
);
|
|
769
870
|
}
|
|
770
871
|
const requestedThinking = agent.thinking ?? parentThinking;
|
|
771
|
-
const
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
872
|
+
const effectiveModel = resolveEffectiveChildModelSettings(agent, parentModel);
|
|
873
|
+
const { level: thinking, warning: thinkingWarning } =
|
|
874
|
+
effectiveModel.provider && effectiveModel.id
|
|
875
|
+
? resolveThinkingLevel(
|
|
876
|
+
requestedThinking,
|
|
877
|
+
effectiveModel.provider,
|
|
878
|
+
effectiveModel.id,
|
|
879
|
+
)
|
|
880
|
+
: { level: requestedThinking };
|
|
881
|
+
const modelDisplay = buildModelDisplay(effectiveModel, thinking);
|
|
779
882
|
const resolvedSkillsPromise: Promise<{ args: string[] } | { error: string }> =
|
|
780
883
|
agent.skills
|
|
781
884
|
? resolveAgentSkillArgs(defaultCwd, agent.skills)
|
|
@@ -807,7 +910,7 @@ export async function runSingleAgent(
|
|
|
807
910
|
const args = buildPiArgs(
|
|
808
911
|
agent,
|
|
809
912
|
task,
|
|
810
|
-
|
|
913
|
+
effectiveModel,
|
|
811
914
|
thinking,
|
|
812
915
|
resolvedSkills,
|
|
813
916
|
tmpPrompt,
|
|
@@ -845,12 +948,21 @@ export async function runSingleAgent(
|
|
|
845
948
|
() => clearGraceTimer(state),
|
|
846
949
|
requestTermination,
|
|
847
950
|
);
|
|
951
|
+
const orchestratorPid = getValidatedOrchestratorPid(options);
|
|
952
|
+
const sleepInhibitorPromise =
|
|
953
|
+
orchestratorPid !== undefined
|
|
954
|
+
? acquireSubagentSleepInhibitor(
|
|
955
|
+
orchestratorPid,
|
|
956
|
+
options.acquireSleepInhibitor ?? acquireDefaultSleepInhibitor,
|
|
957
|
+
)
|
|
958
|
+
: Promise.resolve(undefined);
|
|
848
959
|
try {
|
|
849
960
|
state.result.exitCode = (await processDone) ?? 0;
|
|
850
961
|
return await finalizeResult(state, startedAt);
|
|
851
962
|
} finally {
|
|
852
963
|
clearGraceTimer(state);
|
|
853
964
|
if (signal && onAbort) signal.removeEventListener("abort", onAbort);
|
|
965
|
+
void startSleepInhibitorRelease(sleepInhibitorPromise);
|
|
854
966
|
}
|
|
855
967
|
} finally {
|
|
856
968
|
if (tmpPrompt) await cleanupTempPrompt(tmpPrompt);
|