@henryqw/pi-subagent 3.0.3 → 4.0.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.
@@ -0,0 +1,189 @@
1
+ import type { Usage } from "@earendil-works/pi-ai";
2
+ import {
3
+ addUsage,
4
+ capEphemeralSubagentOutput,
5
+ type EphemeralSubagentResult,
6
+ type WorktreePayload,
7
+ } from "@henryqw/pi-subagent";
8
+ import type { WorkflowEntry, WorkflowMode } from "./workflow.ts";
9
+
10
+ const EVIDENCE_PREVIEW_CODE_POINTS = 256;
11
+
12
+ export type WorkflowTransportStatus = "pending" | "running" | "succeeded" | "failed" | "rejected" | "skipped";
13
+
14
+ type TransportEntryBase = {
15
+ id: WorkflowEntry["id"];
16
+ index: WorkflowEntry["index"];
17
+ role: WorkflowEntry["delegation"]["role"];
18
+ model?: string;
19
+ thinkingLevel?: string;
20
+ worktreePayload?: WorktreePayload;
21
+ usage?: Usage;
22
+ };
23
+
24
+ export type WorkflowTransportEntry =
25
+ | TransportEntryBase & { status: "pending" | "skipped"; assistantOutput?: never; failure?: never }
26
+ | TransportEntryBase & {
27
+ status: "running" | "succeeded";
28
+ assistantOutput: EphemeralSubagentResult["output"];
29
+ failure?: never;
30
+ }
31
+ | TransportEntryBase & { status: "failed" | "rejected"; assistantOutput?: never; failure: string };
32
+
33
+ export type WorkflowTransportEntryDetails = {
34
+ id: string;
35
+ index: number;
36
+ role: string;
37
+ status: WorkflowTransportStatus;
38
+ model?: string;
39
+ thinkingLevel?: string;
40
+ worktree?: WorktreePayload;
41
+ };
42
+
43
+ export type WorkflowTransportDetails = {
44
+ mode: WorkflowMode;
45
+ entries: WorkflowTransportEntryDetails[];
46
+ };
47
+
48
+ export type WorkflowTransport = {
49
+ text: string;
50
+ details: WorkflowTransportDetails;
51
+ usage?: Usage;
52
+ failed: boolean;
53
+ };
54
+
55
+ type TransportKind = "result" | "update" | "background" | "abort";
56
+ type Evidence = { heading: string; preview: string; remainder: string };
57
+
58
+ function compareEntries(left: WorkflowTransportEntry, right: WorkflowTransportEntry): number {
59
+ return left.index - right.index || (left.id < right.id ? -1 : left.id > right.id ? 1 : 0);
60
+ }
61
+
62
+ function splitEvidence(text: string): [string, string] {
63
+ const preview = Array.from(text).slice(0, EVIDENCE_PREVIEW_CODE_POINTS).join("");
64
+ return [preview, text.slice(preview.length)];
65
+ }
66
+
67
+ function label(kind: TransportKind, failed: boolean): string {
68
+ if (kind === "update") return "Workflow update.";
69
+ if (kind === "background") return `Background workflow ${failed ? "failed" : "succeeded"}.`;
70
+ if (kind === "abort") return "Workflow aborted.";
71
+ return `Workflow ${failed ? "failed" : "succeeded"}.`;
72
+ }
73
+
74
+ function evidenceFor(entry: WorkflowTransportEntry): Evidence | undefined {
75
+ if (entry.status === "pending" || entry.status === "skipped") return;
76
+ const source = entry.status === "failed" || entry.status === "rejected" ? entry.failure : entry.assistantOutput;
77
+ const [preview, remainder] = splitEvidence(source || (entry.status === "running" ? "(no output yet)" : "(no output)"));
78
+ return {
79
+ heading: `- [${entry.index}] ${JSON.stringify(entry.id)} ${entry.status === "failed" || entry.status === "rejected" ? "failure" : "assistant"}:`,
80
+ preview,
81
+ remainder,
82
+ };
83
+ }
84
+
85
+ function formatWorkflowTransport(
86
+ mode: WorkflowMode,
87
+ entries: readonly WorkflowTransportEntry[],
88
+ kind: TransportKind,
89
+ ): WorkflowTransport {
90
+ const ordered = [...entries].sort(compareEntries);
91
+ if (kind !== "update" && ordered.some(({ status }) => status === "pending" || status === "running")) {
92
+ throw new TypeError("Final workflow transport requires terminal entry states.");
93
+ }
94
+ const failed = ordered.some(({ status }) => status === "failed" || status === "rejected");
95
+ const recoveries = ordered.filter(({ worktreePayload }) => worktreePayload && !worktreePayload.pruned);
96
+ const evidence = ordered.flatMap((entry) => {
97
+ const value = evidenceFor(entry);
98
+ return value ? [value] : [];
99
+ });
100
+ const lines = [
101
+ label(kind, failed),
102
+ `Mode: ${mode}`,
103
+ "Entries:",
104
+ ...ordered.map((entry) =>
105
+ `- [${entry.index}] id=${JSON.stringify(entry.id)} role=${JSON.stringify(entry.role)} status=${entry.status}`),
106
+ ...(recoveries.length ? [
107
+ "Retained worktrees:",
108
+ ...recoveries.map((entry) =>
109
+ `- [${entry.index}] path=${JSON.stringify(entry.worktreePayload!.path)} branch=${JSON.stringify(entry.worktreePayload!.branch)}`),
110
+ ] : []),
111
+ ...(evidence.length ? [
112
+ "Evidence:",
113
+ ...evidence.flatMap(({ heading, preview }) => [heading, preview]),
114
+ ...(evidence.some(({ remainder }) => remainder) ? [
115
+ "Continued evidence:",
116
+ ...evidence.flatMap(({ heading, remainder }) => remainder ? [heading, remainder] : []),
117
+ ] : []),
118
+ ] : []),
119
+ ];
120
+ let usage: Usage | undefined;
121
+ for (const entry of ordered) if (entry.usage) usage = addUsage(usage, entry.usage);
122
+ return {
123
+ text: capEphemeralSubagentOutput(lines.join("\n")),
124
+ details: {
125
+ mode,
126
+ entries: ordered.map((entry) => ({
127
+ id: entry.id,
128
+ index: entry.index,
129
+ role: entry.role,
130
+ status: entry.status,
131
+ ...(entry.model === undefined ? {} : { model: entry.model }),
132
+ ...(entry.thinkingLevel === undefined ? {} : { thinkingLevel: entry.thinkingLevel }),
133
+ ...(entry.worktreePayload === undefined ? {} : { worktree: { ...entry.worktreePayload } }),
134
+ })),
135
+ },
136
+ ...(usage === undefined ? {} : { usage }),
137
+ failed,
138
+ };
139
+ }
140
+
141
+ export function formatWorkflowResult(
142
+ mode: WorkflowMode,
143
+ entries: readonly WorkflowTransportEntry[],
144
+ ): WorkflowTransport {
145
+ return formatWorkflowTransport(mode, entries, "result");
146
+ }
147
+
148
+ export function formatWorkflowUpdate(
149
+ mode: WorkflowMode,
150
+ entries: readonly WorkflowTransportEntry[],
151
+ ): WorkflowTransport {
152
+ return formatWorkflowTransport(mode, entries, "update");
153
+ }
154
+
155
+ export function formatBackgroundWorkflowResult(
156
+ mode: WorkflowMode,
157
+ entries: readonly WorkflowTransportEntry[],
158
+ ): WorkflowTransport {
159
+ return formatWorkflowTransport(mode, entries, "background");
160
+ }
161
+
162
+ export class WorkflowFailureError extends Error {
163
+ override name = "WorkflowFailureError";
164
+ readonly details: WorkflowTransportDetails;
165
+ readonly usage?: Usage;
166
+ readonly failed = true;
167
+
168
+ constructor(mode: WorkflowMode, entries: readonly WorkflowTransportEntry[]) {
169
+ const transport = formatWorkflowResult(mode, entries);
170
+ if (!transport.failed) throw new TypeError("WorkflowFailureError requires a failed or rejected entry.");
171
+ super(transport.text);
172
+ this.details = transport.details;
173
+ this.usage = transport.usage;
174
+ }
175
+ }
176
+
177
+ export class WorkflowAbortedError extends Error {
178
+ override name = "AbortError";
179
+ readonly details: WorkflowTransportDetails;
180
+ readonly usage?: Usage;
181
+ readonly failed = true;
182
+
183
+ constructor(mode: WorkflowMode, entries: readonly WorkflowTransportEntry[], cause: unknown) {
184
+ const transport = formatWorkflowTransport(mode, entries, "abort");
185
+ super(transport.text, { cause });
186
+ this.details = transport.details;
187
+ this.usage = transport.usage;
188
+ }
189
+ }
@@ -1,7 +1,7 @@
1
1
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
+ import { CHILD_EXCLUDED_TOOLS, ROLE_TOOL_POLICY_FLAG } from "@henryqw/pi-subagent";
2
3
 
3
- export const ROLE_TOOL_POLICY_FLAG = "pi-subagent-role-tools";
4
- const CHILD_EXCLUDED_TOOLS = new Set(["delegate_task", "ask_question", "auto_dag_execute", "auto_dag_acknowledge"]);
4
+ const childExcludedTools = new Set(CHILD_EXCLUDED_TOOLS.split(","));
5
5
 
6
6
  function configuredTools(value: unknown): string[] | undefined {
7
7
  if (value === undefined) return;
@@ -29,6 +29,6 @@ export default function roleTools(pi: ExtensionAPI): void {
29
29
  const extensionTools = pi.getAllTools()
30
30
  .filter((tool) => !["builtin", "sdk", "inline"].includes(tool.sourceInfo.source))
31
31
  .map((tool) => tool.name);
32
- pi.setActiveTools([...new Set([...selected, ...extensionTools])].filter((name) => !CHILD_EXCLUDED_TOOLS.has(name)));
32
+ pi.setActiveTools([...new Set([...selected, ...extensionTools])].filter((name) => !childExcludedTools.has(name)));
33
33
  });
34
34
  }