pi-subagents 0.32.0 → 0.33.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +30 -3
- package/README.md +147 -58
- package/install.mjs +2 -1
- package/package.json +3 -2
- package/skills/pi-subagents/SKILL.md +70 -14
- package/src/agents/agent-management.ts +177 -5
- package/src/agents/agent-memory.ts +254 -0
- package/src/agents/agent-serializer.ts +11 -0
- package/src/agents/agents.ts +142 -12
- package/src/agents/chain-serializer.ts +27 -2
- package/src/extension/doctor.ts +1 -9
- package/src/extension/fanout-child.ts +2 -2
- package/src/extension/index.ts +65 -90
- package/src/extension/rpc.ts +369 -0
- package/src/extension/schemas.ts +52 -8
- package/src/extension/tool-description.ts +200 -0
- package/src/intercom/intercom-bridge.ts +21 -253
- package/src/intercom/native-supervisor-channel.ts +510 -0
- package/src/runs/background/async-execution.ts +51 -7
- package/src/runs/background/async-job-tracker.ts +12 -2
- package/src/runs/background/async-status.ts +27 -2
- package/src/runs/background/completion-batcher.ts +166 -0
- package/src/runs/background/control-channel.ts +106 -1
- package/src/runs/background/fleet-view.ts +515 -0
- package/src/runs/background/notify.ts +161 -44
- package/src/runs/background/result-watcher.ts +1 -2
- package/src/runs/background/run-id-resolver.ts +3 -2
- package/src/runs/background/run-status.ts +166 -6
- package/src/runs/background/scheduled-runs.ts +514 -0
- package/src/runs/background/subagent-runner.ts +409 -35
- package/src/runs/background/wait.ts +353 -0
- package/src/runs/foreground/chain-execution.ts +95 -21
- package/src/runs/foreground/execution.ts +150 -21
- package/src/runs/foreground/subagent-executor.ts +378 -64
- package/src/runs/shared/dynamic-fanout.ts +1 -1
- package/src/runs/shared/model-fallback.ts +167 -20
- package/src/runs/shared/model-scope.ts +128 -0
- package/src/runs/shared/nested-events.ts +31 -0
- package/src/runs/shared/parallel-utils.ts +1 -0
- package/src/runs/shared/pi-args.ts +30 -1
- package/src/runs/shared/subagent-prompt-runtime.ts +108 -2
- package/src/runs/shared/tool-budget.ts +74 -0
- package/src/runs/shared/turn-budget.ts +52 -0
- package/src/shared/artifacts.ts +1 -0
- package/src/shared/atomic-json.ts +15 -2
- package/src/shared/child-transcript.ts +212 -0
- package/src/shared/settings.ts +3 -1
- package/src/shared/types.ts +134 -19
- package/src/slash/prompt-workflows.ts +330 -0
- package/src/slash/slash-commands.ts +16 -2
- package/src/tui/render.ts +16 -8
- package/src/extension/companion-suggestions.ts +0 -359
package/src/shared/types.ts
CHANGED
|
@@ -7,6 +7,7 @@ import * as path from "node:path";
|
|
|
7
7
|
import type { Message } from "@earendil-works/pi-ai";
|
|
8
8
|
import type { FSWatcher } from "node:fs";
|
|
9
9
|
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
10
|
+
import type { ModelScopeConfig } from "../runs/shared/model-scope.ts";
|
|
10
11
|
|
|
11
12
|
// ============================================================================
|
|
12
13
|
// Basic Types
|
|
@@ -88,6 +89,47 @@ export interface Usage {
|
|
|
88
89
|
turns: number;
|
|
89
90
|
}
|
|
90
91
|
|
|
92
|
+
export interface TurnBudgetConfig {
|
|
93
|
+
maxTurns: number;
|
|
94
|
+
graceTurns?: number;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export interface ResolvedTurnBudget {
|
|
98
|
+
maxTurns: number;
|
|
99
|
+
graceTurns: number;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export interface ToolBudgetConfig {
|
|
103
|
+
soft?: number;
|
|
104
|
+
hard: number;
|
|
105
|
+
block?: string[] | "*";
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export interface ResolvedToolBudget {
|
|
109
|
+
soft?: number;
|
|
110
|
+
hard: number;
|
|
111
|
+
block: string[] | "*";
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export type ToolBudgetOutcome = "within-budget" | "soft-reached" | "hard-blocked";
|
|
115
|
+
|
|
116
|
+
export interface ToolBudgetState extends ResolvedToolBudget {
|
|
117
|
+
outcome: ToolBudgetOutcome;
|
|
118
|
+
toolCount: number;
|
|
119
|
+
softReachedAt?: number;
|
|
120
|
+
hardReachedAt?: number;
|
|
121
|
+
blockedTool?: string;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export type TurnBudgetOutcome = "within-budget" | "wrap-up-requested" | "exceeded";
|
|
125
|
+
|
|
126
|
+
export interface TurnBudgetState extends ResolvedTurnBudget {
|
|
127
|
+
outcome: TurnBudgetOutcome;
|
|
128
|
+
turnCount: number;
|
|
129
|
+
wrapUpRequestedAtTurn?: number;
|
|
130
|
+
exceededAtTurn?: number;
|
|
131
|
+
}
|
|
132
|
+
|
|
91
133
|
export interface TokenUsage {
|
|
92
134
|
input: number;
|
|
93
135
|
output: number;
|
|
@@ -120,6 +162,25 @@ export interface ResolvedControlConfig {
|
|
|
120
162
|
notifyChannels: ControlNotificationChannel[];
|
|
121
163
|
}
|
|
122
164
|
|
|
165
|
+
/**
|
|
166
|
+
* Smart completion batching for async-completion notifications. Successful
|
|
167
|
+
* sibling completions are held briefly so they arrive as one grouped message;
|
|
168
|
+
* failure and attention signals bypass grouping and always fire immediately.
|
|
169
|
+
*/
|
|
170
|
+
export interface CompletionBatchConfig {
|
|
171
|
+
enabled?: boolean;
|
|
172
|
+
/** Idle window after each arrival; resets on every new item. */
|
|
173
|
+
debounceMs?: number;
|
|
174
|
+
/** Hard cap measured from the first item in a group. */
|
|
175
|
+
maxWaitMs?: number;
|
|
176
|
+
/** Shorter idle window for straggler groups. */
|
|
177
|
+
stragglerDebounceMs?: number;
|
|
178
|
+
/** Shorter hard cap for straggler groups. */
|
|
179
|
+
stragglerMaxWaitMs?: number;
|
|
180
|
+
/** Arrivals within this window after an emit join a straggler group. */
|
|
181
|
+
stragglerWindowMs?: number;
|
|
182
|
+
}
|
|
183
|
+
|
|
123
184
|
export interface ControlEvent {
|
|
124
185
|
type: ControlEventType;
|
|
125
186
|
from?: ActivityState;
|
|
@@ -149,7 +210,7 @@ export type SubagentLifecycleArtifactVersion = typeof SUBAGENT_LIFECYCLE_ARTIFAC
|
|
|
149
210
|
|
|
150
211
|
export type PublicNestedStepSummary = Pick<
|
|
151
212
|
NestedStepSummary,
|
|
152
|
-
"agent" | "status" | "sessionFile" | "activityState" | "lastActivityAt" | "currentTool" | "currentToolStartedAt" | "currentPath" | "turnCount" | "toolCount" | "startedAt" | "endedAt" | "error" | "timedOut"
|
|
213
|
+
"agent" | "status" | "sessionFile" | "transcriptPath" | "transcriptError" | "activityState" | "lastActivityAt" | "currentTool" | "currentToolStartedAt" | "currentPath" | "turnCount" | "toolCount" | "toolBudget" | "toolBudgetBlocked" | "startedAt" | "endedAt" | "error" | "timedOut"
|
|
153
214
|
> & {
|
|
154
215
|
children?: PublicNestedRunSummary[];
|
|
155
216
|
};
|
|
@@ -162,7 +223,7 @@ export type CostSummary = {
|
|
|
162
223
|
|
|
163
224
|
export type PublicNestedRunSummary = Pick<
|
|
164
225
|
NestedRunSummary,
|
|
165
|
-
"id" | "parentRunId" | "parentStepIndex" | "parentAgent" | "depth" | "path" | "asyncDir" | "sessionId" | "sessionFile" | "intercomTarget" | "ownerIntercomTarget" | "leafIntercomTarget" | "ownerState" | "mode" | "state" | "agent" | "agents" | "currentStep" | "chainStepCount" | "parallelGroups" | "activityState" | "lastActivityAt" | "currentTool" | "currentToolStartedAt" | "currentPath" | "turnCount" | "toolCount" | "totalTokens" | "totalCost" | "startedAt" | "endedAt" | "lastUpdate" | "error" | "timeoutMs" | "deadlineAt" | "timedOut"
|
|
226
|
+
"id" | "parentRunId" | "parentStepIndex" | "parentAgent" | "depth" | "path" | "asyncDir" | "sessionId" | "sessionFile" | "intercomTarget" | "ownerIntercomTarget" | "leafIntercomTarget" | "ownerState" | "mode" | "state" | "agent" | "agents" | "currentStep" | "chainStepCount" | "parallelGroups" | "activityState" | "lastActivityAt" | "currentTool" | "currentToolStartedAt" | "currentPath" | "turnCount" | "toolCount" | "toolBudget" | "toolBudgetBlocked" | "totalTokens" | "totalCost" | "startedAt" | "endedAt" | "lastUpdate" | "error" | "timeoutMs" | "deadlineAt" | "timedOut" | "turnBudget" | "turnBudgetExceeded" | "wrapUpRequested"
|
|
166
227
|
> & {
|
|
167
228
|
steps?: PublicNestedStepSummary[];
|
|
168
229
|
children?: PublicNestedRunSummary[];
|
|
@@ -400,6 +461,11 @@ export interface SingleResult {
|
|
|
400
461
|
detachedReason?: string;
|
|
401
462
|
interrupted?: boolean;
|
|
402
463
|
timedOut?: boolean;
|
|
464
|
+
turnBudget?: TurnBudgetState;
|
|
465
|
+
turnBudgetExceeded?: boolean;
|
|
466
|
+
wrapUpRequested?: boolean;
|
|
467
|
+
toolBudget?: ToolBudgetState;
|
|
468
|
+
toolBudgetBlocked?: boolean;
|
|
403
469
|
messages?: Message[];
|
|
404
470
|
usage: Usage;
|
|
405
471
|
model?: string;
|
|
@@ -424,6 +490,8 @@ export interface SingleResult {
|
|
|
424
490
|
structuredOutputPath?: string;
|
|
425
491
|
structuredOutputSchemaPath?: string;
|
|
426
492
|
acceptance?: AcceptanceLedger;
|
|
493
|
+
transcriptPath?: string;
|
|
494
|
+
transcriptError?: string;
|
|
427
495
|
children?: NestedRunSummary[];
|
|
428
496
|
}
|
|
429
497
|
|
|
@@ -438,6 +506,8 @@ export interface Details {
|
|
|
438
506
|
timeoutMs?: number;
|
|
439
507
|
deadlineAt?: number;
|
|
440
508
|
timedOut?: boolean;
|
|
509
|
+
turnBudget?: ResolvedTurnBudget;
|
|
510
|
+
toolBudget?: ResolvedToolBudget;
|
|
441
511
|
progress?: AgentProgress[];
|
|
442
512
|
progressSummary?: ProgressSummary;
|
|
443
513
|
artifacts?: {
|
|
@@ -470,6 +540,7 @@ export interface ArtifactPaths {
|
|
|
470
540
|
inputPath: string;
|
|
471
541
|
outputPath: string;
|
|
472
542
|
jsonlPath: string;
|
|
543
|
+
transcriptPath: string;
|
|
473
544
|
metadataPath: string;
|
|
474
545
|
}
|
|
475
546
|
|
|
@@ -478,6 +549,7 @@ export interface ArtifactConfig {
|
|
|
478
549
|
includeInput: boolean;
|
|
479
550
|
includeOutput: boolean;
|
|
480
551
|
includeJsonl: boolean;
|
|
552
|
+
includeTranscript?: boolean;
|
|
481
553
|
includeMetadata: boolean;
|
|
482
554
|
cleanupDays: number;
|
|
483
555
|
}
|
|
@@ -508,6 +580,8 @@ export interface NestedStepSummary {
|
|
|
508
580
|
agent: string;
|
|
509
581
|
status: "pending" | "running" | "complete" | "completed" | "failed" | "paused";
|
|
510
582
|
sessionFile?: string;
|
|
583
|
+
transcriptPath?: string;
|
|
584
|
+
transcriptError?: string;
|
|
511
585
|
activityState?: ActivityState;
|
|
512
586
|
lastActivityAt?: number;
|
|
513
587
|
currentTool?: string;
|
|
@@ -519,6 +593,11 @@ export interface NestedStepSummary {
|
|
|
519
593
|
endedAt?: number;
|
|
520
594
|
error?: string;
|
|
521
595
|
timedOut?: boolean;
|
|
596
|
+
turnBudget?: TurnBudgetState;
|
|
597
|
+
turnBudgetExceeded?: boolean;
|
|
598
|
+
wrapUpRequested?: boolean;
|
|
599
|
+
toolBudget?: ToolBudgetState;
|
|
600
|
+
toolBudgetBlocked?: boolean;
|
|
522
601
|
children?: NestedRunSummary[];
|
|
523
602
|
}
|
|
524
603
|
|
|
@@ -557,6 +636,11 @@ export interface NestedRunSummary extends NestedRunAddress {
|
|
|
557
636
|
timeoutMs?: number;
|
|
558
637
|
deadlineAt?: number;
|
|
559
638
|
timedOut?: boolean;
|
|
639
|
+
turnBudget?: TurnBudgetState;
|
|
640
|
+
turnBudgetExceeded?: boolean;
|
|
641
|
+
wrapUpRequested?: boolean;
|
|
642
|
+
toolBudget?: ToolBudgetState;
|
|
643
|
+
toolBudgetBlocked?: boolean;
|
|
560
644
|
error?: string;
|
|
561
645
|
}
|
|
562
646
|
|
|
@@ -582,6 +666,7 @@ export interface AsyncStartedEvent {
|
|
|
582
666
|
workflowGraph?: WorkflowGraphSnapshot;
|
|
583
667
|
timeoutMs?: number;
|
|
584
668
|
deadlineAt?: number;
|
|
669
|
+
turnBudget?: TurnBudgetState;
|
|
585
670
|
nestedRoute?: NestedRouteInfo;
|
|
586
671
|
}
|
|
587
672
|
|
|
@@ -599,12 +684,19 @@ export interface AsyncStatus {
|
|
|
599
684
|
currentPath?: string;
|
|
600
685
|
turnCount?: number;
|
|
601
686
|
toolCount?: number;
|
|
687
|
+
steerCount?: number;
|
|
688
|
+
lastSteerAt?: number;
|
|
602
689
|
startedAt: number;
|
|
603
690
|
endedAt?: number;
|
|
604
691
|
lastUpdate?: number;
|
|
605
692
|
timeoutMs?: number;
|
|
606
693
|
deadlineAt?: number;
|
|
607
694
|
timedOut?: boolean;
|
|
695
|
+
turnBudget?: TurnBudgetState;
|
|
696
|
+
turnBudgetExceeded?: boolean;
|
|
697
|
+
wrapUpRequested?: boolean;
|
|
698
|
+
toolBudget?: ToolBudgetState;
|
|
699
|
+
toolBudgetBlocked?: boolean;
|
|
608
700
|
pid?: number;
|
|
609
701
|
cwd?: string;
|
|
610
702
|
currentStep?: number;
|
|
@@ -621,6 +713,8 @@ export interface AsyncStatus {
|
|
|
621
713
|
status: "pending" | "running" | "complete" | "completed" | "failed" | "paused";
|
|
622
714
|
children?: NestedRunSummary[];
|
|
623
715
|
sessionFile?: string;
|
|
716
|
+
transcriptPath?: string;
|
|
717
|
+
transcriptError?: string;
|
|
624
718
|
activityState?: ActivityState;
|
|
625
719
|
lastActivityAt?: number;
|
|
626
720
|
currentTool?: string;
|
|
@@ -636,6 +730,11 @@ export interface AsyncStatus {
|
|
|
636
730
|
durationMs?: number;
|
|
637
731
|
exitCode?: number | null;
|
|
638
732
|
timedOut?: boolean;
|
|
733
|
+
turnBudget?: TurnBudgetState;
|
|
734
|
+
turnBudgetExceeded?: boolean;
|
|
735
|
+
wrapUpRequested?: boolean;
|
|
736
|
+
toolBudget?: ToolBudgetState;
|
|
737
|
+
toolBudgetBlocked?: boolean;
|
|
639
738
|
tokens?: TokenUsage;
|
|
640
739
|
skills?: string[];
|
|
641
740
|
model?: string;
|
|
@@ -643,6 +742,8 @@ export interface AsyncStatus {
|
|
|
643
742
|
attemptedModels?: string[];
|
|
644
743
|
modelAttempts?: ModelAttempt[];
|
|
645
744
|
totalCost?: CostSummary;
|
|
745
|
+
steerCount?: number;
|
|
746
|
+
lastSteerAt?: number;
|
|
646
747
|
error?: string;
|
|
647
748
|
structuredOutput?: unknown;
|
|
648
749
|
structuredOutputPath?: string;
|
|
@@ -674,6 +775,8 @@ export interface AsyncJobState {
|
|
|
674
775
|
currentPath?: string;
|
|
675
776
|
turnCount?: number;
|
|
676
777
|
toolCount?: number;
|
|
778
|
+
steerCount?: number;
|
|
779
|
+
lastSteerAt?: number;
|
|
677
780
|
mode?: SubagentRunMode;
|
|
678
781
|
agents?: string[];
|
|
679
782
|
currentStep?: number;
|
|
@@ -690,6 +793,11 @@ export interface AsyncJobState {
|
|
|
690
793
|
timeoutMs?: number;
|
|
691
794
|
deadlineAt?: number;
|
|
692
795
|
timedOut?: boolean;
|
|
796
|
+
turnBudget?: TurnBudgetState;
|
|
797
|
+
turnBudgetExceeded?: boolean;
|
|
798
|
+
wrapUpRequested?: boolean;
|
|
799
|
+
toolBudget?: ToolBudgetState;
|
|
800
|
+
toolBudgetBlocked?: boolean;
|
|
693
801
|
sessionDir?: string;
|
|
694
802
|
outputFile?: string;
|
|
695
803
|
totalTokens?: TokenUsage;
|
|
@@ -704,6 +812,13 @@ export interface ForegroundResumeChild {
|
|
|
704
812
|
index: number;
|
|
705
813
|
sessionFile?: string;
|
|
706
814
|
status: SubagentResultStatus;
|
|
815
|
+
exitCode?: number;
|
|
816
|
+
finalOutput?: string;
|
|
817
|
+
artifactPaths?: ArtifactPaths;
|
|
818
|
+
transcriptPath?: string;
|
|
819
|
+
transcriptError?: string;
|
|
820
|
+
detachedReason?: string;
|
|
821
|
+
updatedAt?: number;
|
|
707
822
|
}
|
|
708
823
|
|
|
709
824
|
export interface ForegroundResumeRun {
|
|
@@ -748,8 +863,6 @@ export interface SubagentState {
|
|
|
748
863
|
completionSeen: Map<string, number>;
|
|
749
864
|
watcher: FSWatcher | null;
|
|
750
865
|
watcherRestartTimer: ReturnType<typeof setTimeout> | null;
|
|
751
|
-
companionSuggestionStartupShown?: boolean;
|
|
752
|
-
companionSuggestionListShown?: boolean;
|
|
753
866
|
resultFileCoalescer: {
|
|
754
867
|
schedule(file: string, delayMs?: number): boolean;
|
|
755
868
|
clear(): void;
|
|
@@ -801,10 +914,13 @@ export interface RunSyncOptions {
|
|
|
801
914
|
interruptSignal?: AbortSignal;
|
|
802
915
|
timeoutMs?: number;
|
|
803
916
|
deadlineAt?: number;
|
|
917
|
+
turnBudget?: ResolvedTurnBudget;
|
|
918
|
+
toolBudget?: ResolvedToolBudget;
|
|
804
919
|
allowIntercomDetach?: boolean;
|
|
805
920
|
intercomEvents?: IntercomEventBus;
|
|
806
921
|
onUpdate?: (r: import("@earendil-works/pi-agent-core").AgentToolResult<Details>) => void;
|
|
807
922
|
onControlEvent?: (event: ControlEvent) => void;
|
|
923
|
+
onDetachedExit?: (result: SingleResult) => void;
|
|
808
924
|
controlConfig?: ResolvedControlConfig;
|
|
809
925
|
intercomSessionName?: string;
|
|
810
926
|
orchestratorIntercomTarget?: string;
|
|
@@ -828,6 +944,8 @@ export interface RunSyncOptions {
|
|
|
828
944
|
availableModels?: Array<{ provider: string; id: string; fullId: string }>;
|
|
829
945
|
/** Current parent-session provider to prefer for ambiguous bare model ids */
|
|
830
946
|
preferredModelProvider?: string;
|
|
947
|
+
/** Optional subagent model-scope enforcement for fallback candidates */
|
|
948
|
+
modelScope?: ModelScopeConfig;
|
|
831
949
|
/** Skills to make available (overrides agent default if provided) */
|
|
832
950
|
skills?: string[];
|
|
833
951
|
structuredOutput?: {
|
|
@@ -869,25 +987,18 @@ export interface ProactiveSkillSubagentsConfig {
|
|
|
869
987
|
preferredAgent?: string;
|
|
870
988
|
}
|
|
871
989
|
|
|
872
|
-
export type
|
|
873
|
-
export type CompanionSuggestionSurface = "session_start" | "list" | "doctor";
|
|
874
|
-
|
|
875
|
-
export interface CompanionSuggestionPackageConfig {
|
|
876
|
-
enabled?: boolean;
|
|
877
|
-
surfaces?: CompanionSuggestionSurface[];
|
|
878
|
-
dismissed?: {
|
|
879
|
-
user?: boolean;
|
|
880
|
-
workspaces?: string[];
|
|
881
|
-
};
|
|
882
|
-
}
|
|
990
|
+
export type ToolDescriptionMode = "full" | "compact" | "custom";
|
|
883
991
|
|
|
884
|
-
export interface
|
|
992
|
+
export interface ScheduledRunsConfig {
|
|
885
993
|
enabled?: boolean;
|
|
886
|
-
|
|
994
|
+
maxLatenessMs?: number;
|
|
995
|
+
maxPending?: number;
|
|
887
996
|
}
|
|
888
997
|
|
|
889
998
|
export interface ExtensionConfig {
|
|
890
999
|
asyncByDefault?: boolean;
|
|
1000
|
+
/** Tool description variant registered for the parent-facing subagent tool. Defaults to full. */
|
|
1001
|
+
toolDescriptionMode?: ToolDescriptionMode;
|
|
891
1002
|
forceTopLevelAsync?: boolean;
|
|
892
1003
|
defaultSessionDir?: string;
|
|
893
1004
|
singleRunOutputBaseDir?: string;
|
|
@@ -896,6 +1007,9 @@ export interface ExtensionConfig {
|
|
|
896
1007
|
/** Global cap on simultaneously-running subagent tasks within a single run. Defaults to 20. */
|
|
897
1008
|
globalConcurrencyLimit?: number;
|
|
898
1009
|
control?: ControlConfig;
|
|
1010
|
+
completionBatch?: CompletionBatchConfig;
|
|
1011
|
+
turnBudget?: TurnBudgetConfig;
|
|
1012
|
+
toolBudget?: ToolBudgetConfig;
|
|
899
1013
|
parallel?: TopLevelParallelConfig;
|
|
900
1014
|
chain?: ExtensionChainConfig;
|
|
901
1015
|
worktreeSetupHook?: string;
|
|
@@ -903,7 +1017,7 @@ export interface ExtensionConfig {
|
|
|
903
1017
|
worktreeBaseDir?: string;
|
|
904
1018
|
intercomBridge?: IntercomBridgeConfig;
|
|
905
1019
|
proactiveSkillSubagents?: ProactiveSkillSubagentsConfig | false;
|
|
906
|
-
|
|
1020
|
+
scheduledRuns?: ScheduledRunsConfig;
|
|
907
1021
|
}
|
|
908
1022
|
|
|
909
1023
|
// ============================================================================
|
|
@@ -920,6 +1034,7 @@ export const DEFAULT_ARTIFACT_CONFIG: ArtifactConfig = {
|
|
|
920
1034
|
includeInput: true,
|
|
921
1035
|
includeOutput: true,
|
|
922
1036
|
includeJsonl: false,
|
|
1037
|
+
includeTranscript: true,
|
|
923
1038
|
includeMetadata: true,
|
|
924
1039
|
cleanupDays: 7,
|
|
925
1040
|
};
|
|
@@ -996,7 +1111,7 @@ export const POLL_INTERVAL_MS = 250;
|
|
|
996
1111
|
export const MAX_WIDGET_JOBS = 4;
|
|
997
1112
|
export const DEFAULT_SUBAGENT_MAX_DEPTH = 2;
|
|
998
1113
|
export const DEFAULT_MAX_SUBAGENT_SPAWNS_PER_SESSION = 40;
|
|
999
|
-
export const SUBAGENT_ACTIONS = ["list", "get", "models", "create", "update", "delete", "status", "interrupt", "resume", "append-step", "doctor"] as const;
|
|
1114
|
+
export const SUBAGENT_ACTIONS = ["list", "get", "models", "create", "update", "delete", "eject", "disable", "enable", "reset", "status", "interrupt", "resume", "steer", "append-step", "doctor", "schedule", "schedule-list", "schedule-status", "schedule-cancel"] as const;
|
|
1000
1115
|
|
|
1001
1116
|
export const DEFAULT_FORK_PREAMBLE =
|
|
1002
1117
|
"You are a delegated subagent running from a fork of the parent session. " +
|
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
5
|
+
import { parseFrontmatter } from "../agents/frontmatter.ts";
|
|
6
|
+
import type { SubagentParamsLike } from "../runs/foreground/subagent-executor.ts";
|
|
7
|
+
import type { ChainStep } from "../shared/settings.ts";
|
|
8
|
+
import { getAgentDir, getProjectConfigDir } from "../shared/utils.ts";
|
|
9
|
+
|
|
10
|
+
interface PromptWorkflow {
|
|
11
|
+
name: string;
|
|
12
|
+
description: string;
|
|
13
|
+
body: string;
|
|
14
|
+
filePath: string;
|
|
15
|
+
agent: string;
|
|
16
|
+
context?: "fresh" | "fork";
|
|
17
|
+
model?: string;
|
|
18
|
+
skill?: string | string[] | false;
|
|
19
|
+
cwd?: string;
|
|
20
|
+
worktree?: boolean;
|
|
21
|
+
chain?: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
type PromptWorkflowRunner = (params: SubagentParamsLike, ctx: ExtensionContext) => Promise<void>;
|
|
25
|
+
|
|
26
|
+
const RESERVED_COMMAND_NAMES = new Set([
|
|
27
|
+
"chain-prompts",
|
|
28
|
+
"prompt-workflow",
|
|
29
|
+
"run",
|
|
30
|
+
"chain",
|
|
31
|
+
"parallel",
|
|
32
|
+
"run-chain",
|
|
33
|
+
"subagents-doctor",
|
|
34
|
+
"subagents-models",
|
|
35
|
+
]);
|
|
36
|
+
|
|
37
|
+
function packagePromptsDir(): string {
|
|
38
|
+
return path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "..", "prompts");
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function promptDirs(cwd: string): string[] {
|
|
42
|
+
return [
|
|
43
|
+
packagePromptsDir(),
|
|
44
|
+
path.join(getAgentDir(), "prompts"),
|
|
45
|
+
path.join(getProjectConfigDir(cwd), "prompts"),
|
|
46
|
+
];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function readPromptFiles(cwd: string): string[] {
|
|
50
|
+
const files: string[] = [];
|
|
51
|
+
for (const dir of promptDirs(cwd)) {
|
|
52
|
+
let entries: fs.Dirent[];
|
|
53
|
+
try {
|
|
54
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
55
|
+
} catch {
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
for (const entry of entries) {
|
|
59
|
+
if (entry.isFile() && entry.name.endsWith(".md")) files.push(path.join(dir, entry.name));
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return files;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function firstNonEmptyLine(value: string): string {
|
|
66
|
+
return value.split(/\r?\n/).map((line) => line.trim()).find(Boolean) ?? "Prompt workflow";
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function stringField(frontmatter: Record<string, string>, key: string): string | undefined {
|
|
70
|
+
const value = frontmatter[key]?.trim();
|
|
71
|
+
return value ? value : undefined;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function booleanField(frontmatter: Record<string, string>, key: string): boolean | undefined {
|
|
75
|
+
const value = frontmatter[key]?.trim().toLowerCase();
|
|
76
|
+
if (value === "true" || value === "yes" || value === "1") return true;
|
|
77
|
+
if (value === "false" || value === "no" || value === "0") return false;
|
|
78
|
+
return undefined;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function parseSkill(value: string | undefined): string | string[] | false | undefined {
|
|
82
|
+
if (!value) return undefined;
|
|
83
|
+
if (value === "false") return false;
|
|
84
|
+
const parts = value.split(",").map((part) => part.trim()).filter(Boolean);
|
|
85
|
+
return parts.length > 1 ? parts : parts[0];
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function parseAgent(frontmatter: Record<string, string>): string {
|
|
89
|
+
const subagent = stringField(frontmatter, "subagent");
|
|
90
|
+
if (!subagent || subagent === "true") return "delegate";
|
|
91
|
+
return subagent;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function loadPromptWorkflow(filePath: string): PromptWorkflow | undefined {
|
|
95
|
+
const content = fs.readFileSync(filePath, "utf-8");
|
|
96
|
+
const { frontmatter, body } = parseFrontmatter(content);
|
|
97
|
+
const name = path.basename(filePath, ".md");
|
|
98
|
+
if (!name || RESERVED_COMMAND_NAMES.has(name)) return undefined;
|
|
99
|
+
const model = stringField(frontmatter, "model");
|
|
100
|
+
const skill = parseSkill(stringField(frontmatter, "skill"));
|
|
101
|
+
const cwd = stringField(frontmatter, "cwd");
|
|
102
|
+
const chain = stringField(frontmatter, "chain");
|
|
103
|
+
return {
|
|
104
|
+
name,
|
|
105
|
+
description: stringField(frontmatter, "description") ?? firstNonEmptyLine(body),
|
|
106
|
+
body,
|
|
107
|
+
filePath,
|
|
108
|
+
agent: parseAgent(frontmatter),
|
|
109
|
+
...(booleanField(frontmatter, "inheritContext") === true || booleanField(frontmatter, "fork") === true ? { context: "fork" as const } : {}),
|
|
110
|
+
...(booleanField(frontmatter, "fresh") === true ? { context: "fresh" as const } : {}),
|
|
111
|
+
...(model ? { model } : {}),
|
|
112
|
+
...(skill !== undefined ? { skill } : {}),
|
|
113
|
+
...(cwd ? { cwd } : {}),
|
|
114
|
+
...(booleanField(frontmatter, "worktree") === true ? { worktree: true } : {}),
|
|
115
|
+
...(chain ? { chain } : {}),
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function discoverPromptWorkflows(cwd: string): PromptWorkflow[] {
|
|
120
|
+
const workflows = new Map<string, PromptWorkflow>();
|
|
121
|
+
for (const file of readPromptFiles(cwd)) {
|
|
122
|
+
const workflow = loadPromptWorkflow(file);
|
|
123
|
+
if (workflow) workflows.set(workflow.name, workflow);
|
|
124
|
+
}
|
|
125
|
+
return [...workflows.values()].sort((a, b) => a.name.localeCompare(b.name));
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function shellWords(input: string): string[] {
|
|
129
|
+
const words: string[] = [];
|
|
130
|
+
let current = "";
|
|
131
|
+
let quote: "'" | '"' | undefined;
|
|
132
|
+
let escaped = false;
|
|
133
|
+
for (const ch of input) {
|
|
134
|
+
if (escaped) {
|
|
135
|
+
current += ch;
|
|
136
|
+
escaped = false;
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
if (ch === "\\") {
|
|
140
|
+
escaped = true;
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
if (quote) {
|
|
144
|
+
if (ch === quote) quote = undefined;
|
|
145
|
+
else current += ch;
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
if (ch === "'" || ch === '"') {
|
|
149
|
+
quote = ch;
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
if (/\s/.test(ch)) {
|
|
153
|
+
if (current) {
|
|
154
|
+
words.push(current);
|
|
155
|
+
current = "";
|
|
156
|
+
}
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
current += ch;
|
|
160
|
+
}
|
|
161
|
+
if (current) words.push(current);
|
|
162
|
+
return words;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function substituteArgs(template: string, args: string[]): string {
|
|
166
|
+
const all = args.join(" ");
|
|
167
|
+
return template
|
|
168
|
+
.replace(/\$ARGUMENTS/g, all)
|
|
169
|
+
.replace(/\$@/g, all)
|
|
170
|
+
.replace(/\$\{(\d+):-([^}]*)\}/g, (_match, index: string, fallback: string) => args[Number(index) - 1] || fallback)
|
|
171
|
+
.replace(/\$(\d+)/g, (_match, index: string) => args[Number(index) - 1] ?? "");
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function parseRuntimeOptions(words: string[]): { args: string[]; agentOverride?: string; fork?: boolean; fresh?: boolean; worktree?: boolean; bg?: boolean } {
|
|
175
|
+
const args: string[] = [];
|
|
176
|
+
let agentOverride: string | undefined;
|
|
177
|
+
let fork = false;
|
|
178
|
+
let fresh = false;
|
|
179
|
+
let worktree = false;
|
|
180
|
+
let bg = false;
|
|
181
|
+
for (let i = 0; i < words.length; i++) {
|
|
182
|
+
const word = words[i]!;
|
|
183
|
+
if (word === "--fork") {
|
|
184
|
+
fork = true;
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
if (word === "--fresh") {
|
|
188
|
+
fresh = true;
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
if (word === "--worktree") {
|
|
192
|
+
worktree = true;
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
if (word === "--bg" || word === "--async") {
|
|
196
|
+
bg = true;
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
if (word === "--subagent") {
|
|
200
|
+
agentOverride = words[++i];
|
|
201
|
+
continue;
|
|
202
|
+
}
|
|
203
|
+
const eq = word.match(/^--subagent(?:=|:)(.+)$/);
|
|
204
|
+
if (eq) {
|
|
205
|
+
agentOverride = eq[1];
|
|
206
|
+
continue;
|
|
207
|
+
}
|
|
208
|
+
args.push(word);
|
|
209
|
+
}
|
|
210
|
+
return { args, agentOverride, fork, fresh, worktree, bg };
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function splitChainDeclaration(input: string): { declaration: string; argsText: string } {
|
|
214
|
+
const delimiter = input.indexOf(" -- ");
|
|
215
|
+
if (delimiter === -1) return { declaration: input.trim(), argsText: "" };
|
|
216
|
+
return { declaration: input.slice(0, delimiter).trim(), argsText: input.slice(delimiter + 4).trim() };
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function splitPromptChain(input: string): string[] {
|
|
220
|
+
return input.split(" -> ").map((part) => part.trim()).filter(Boolean);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function workflowParams(workflow: PromptWorkflow, args: string[], runtime: ReturnType<typeof parseRuntimeOptions>): SubagentParamsLike {
|
|
224
|
+
const task = substituteArgs(workflow.body, args).trim();
|
|
225
|
+
const context = runtime.fork ? "fork" : runtime.fresh ? "fresh" : workflow.context;
|
|
226
|
+
return {
|
|
227
|
+
agent: runtime.agentOverride ?? workflow.agent,
|
|
228
|
+
task,
|
|
229
|
+
clarify: false,
|
|
230
|
+
agentScope: "both",
|
|
231
|
+
...(context ? { context } : {}),
|
|
232
|
+
...(workflow.model ? { model: workflow.model } : {}),
|
|
233
|
+
...(workflow.skill !== undefined ? { skill: workflow.skill } : {}),
|
|
234
|
+
...(workflow.cwd ? { cwd: workflow.cwd } : {}),
|
|
235
|
+
...(runtime.worktree || workflow.worktree ? { worktree: true } : {}),
|
|
236
|
+
...(runtime.bg ? { async: true } : {}),
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function workflowChainStep(workflow: PromptWorkflow, args: string[], runtime: ReturnType<typeof parseRuntimeOptions>): ChainStep {
|
|
241
|
+
const params = workflowParams(workflow, args, runtime);
|
|
242
|
+
return {
|
|
243
|
+
agent: params.agent ?? "delegate",
|
|
244
|
+
task: params.task,
|
|
245
|
+
...(params.model ? { model: params.model } : {}),
|
|
246
|
+
...(params.skill !== undefined ? { skill: params.skill } : {}),
|
|
247
|
+
...(params.cwd ? { cwd: params.cwd } : {}),
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function findWorkflow(workflows: PromptWorkflow[], name: string): PromptWorkflow | undefined {
|
|
252
|
+
return workflows.find((workflow) => workflow.name === name);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function formatWorkflowList(workflows: PromptWorkflow[]): string {
|
|
256
|
+
if (workflows.length === 0) return "No prompt workflows found in package, user, or project prompts.";
|
|
257
|
+
return [
|
|
258
|
+
"Prompt workflows:",
|
|
259
|
+
...workflows.map((workflow) => `- ${workflow.name}: ${workflow.description} (${workflow.filePath})`),
|
|
260
|
+
].join("\n");
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
export function registerPromptWorkflowCommands(input: {
|
|
264
|
+
pi: ExtensionAPI;
|
|
265
|
+
run: PromptWorkflowRunner;
|
|
266
|
+
}): void {
|
|
267
|
+
const { pi, run } = input;
|
|
268
|
+
|
|
269
|
+
pi.registerCommand("prompt-workflow", {
|
|
270
|
+
description: "Run a prompt template through native pi-subagents: /prompt-workflow <name> [args]",
|
|
271
|
+
handler: async (rawArgs, ctx) => {
|
|
272
|
+
const words = shellWords(rawArgs);
|
|
273
|
+
const name = words.shift();
|
|
274
|
+
const workflows = discoverPromptWorkflows(ctx.cwd);
|
|
275
|
+
if (!name || name === "list") {
|
|
276
|
+
pi.sendMessage({ content: formatWorkflowList(workflows), display: true });
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
const workflow = findWorkflow(workflows, name);
|
|
280
|
+
if (!workflow) {
|
|
281
|
+
ctx.ui.notify(`Unknown prompt workflow: ${name}`, "error");
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
const runtime = parseRuntimeOptions(words);
|
|
285
|
+
try {
|
|
286
|
+
if (workflow.chain) {
|
|
287
|
+
const chainNames = splitPromptChain(workflow.chain);
|
|
288
|
+
const chain = chainNames.map((stepName) => {
|
|
289
|
+
const step = findWorkflow(workflows, stepName);
|
|
290
|
+
if (!step) throw new Error(`Unknown prompt workflow in chain '${workflow.name}': ${stepName}`);
|
|
291
|
+
return workflowChainStep(step, runtime.args, runtime);
|
|
292
|
+
});
|
|
293
|
+
await run({ chain, task: runtime.args.join(" "), clarify: false, agentScope: "both", ...(runtime.bg ? { async: true } : {}) }, ctx);
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
await run(workflowParams(workflow, runtime.args, runtime), ctx);
|
|
297
|
+
} catch (error) {
|
|
298
|
+
ctx.ui.notify(error instanceof Error ? error.message : String(error), "error");
|
|
299
|
+
}
|
|
300
|
+
},
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
pi.registerCommand("chain-prompts", {
|
|
304
|
+
description: "Run prompt templates as a native subagent chain: /chain-prompts analyze -> fix -- args",
|
|
305
|
+
handler: async (rawArgs, ctx) => {
|
|
306
|
+
const { declaration, argsText } = splitChainDeclaration(rawArgs);
|
|
307
|
+
const workflows = discoverPromptWorkflows(ctx.cwd);
|
|
308
|
+
if (!declaration || declaration === "list") {
|
|
309
|
+
pi.sendMessage({ content: formatWorkflowList(workflows), display: true });
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
const runtime = parseRuntimeOptions(shellWords(argsText));
|
|
313
|
+
const names = splitPromptChain(declaration);
|
|
314
|
+
if (names.length === 0) {
|
|
315
|
+
ctx.ui.notify("Usage: /chain-prompts prompt-a -> prompt-b -- args", "error");
|
|
316
|
+
return;
|
|
317
|
+
}
|
|
318
|
+
try {
|
|
319
|
+
const chain = names.map((name) => {
|
|
320
|
+
const workflow = findWorkflow(workflows, name);
|
|
321
|
+
if (!workflow) throw new Error(`Unknown prompt workflow: ${name}`);
|
|
322
|
+
return workflowChainStep(workflow, runtime.args, runtime);
|
|
323
|
+
});
|
|
324
|
+
await run({ chain, task: runtime.args.join(" "), clarify: false, agentScope: "both", ...(runtime.bg ? { async: true } : {}) }, ctx);
|
|
325
|
+
} catch (error) {
|
|
326
|
+
ctx.ui.notify(error instanceof Error ? error.message : String(error), "error");
|
|
327
|
+
}
|
|
328
|
+
},
|
|
329
|
+
});
|
|
330
|
+
}
|