@tt-a1i/openpi 0.1.0 → 0.2.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.
Files changed (55) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +295 -389
  3. package/SETUP.md +24 -22
  4. package/THIRD_PARTY_NOTICES.md +3 -4
  5. package/assets/readme-hero-mobile.svg +2 -2
  6. package/assets/readme-hero.svg +10 -10
  7. package/extensions/ask-user/handoff.ts +5 -1
  8. package/extensions/ask-user/index.ts +44 -0
  9. package/extensions/background-terminals/index.ts +118 -29
  10. package/extensions/background-terminals/src/domain.ts +5 -1
  11. package/extensions/background-terminals/src/manager.ts +2 -1
  12. package/extensions/background-terminals/src/prompt.ts +35 -0
  13. package/extensions/background-terminals/src/result-delivery.ts +76 -3
  14. package/extensions/background-terminals/src/ui/tool-result.ts +52 -1
  15. package/extensions/capabilities/index.ts +198 -0
  16. package/extensions/context-pivot/index.ts +21 -0
  17. package/extensions/cron/index.ts +42 -15
  18. package/extensions/execution-convergence/active-evidence.ts +129 -0
  19. package/extensions/execution-convergence/index.ts +442 -0
  20. package/extensions/execution-convergence/workspace-provenance.ts +338 -0
  21. package/extensions/file-search/index.ts +8 -1
  22. package/extensions/file-search/src/binaries.ts +2 -1
  23. package/extensions/git-info/src/runtime.ts +1 -1
  24. package/extensions/goal/controller.ts +2 -1
  25. package/extensions/goal/index.ts +20 -1
  26. package/extensions/plan-mode/index.ts +12 -0
  27. package/extensions/setup/index.ts +241 -45
  28. package/extensions/setup/intercom-fs-helper.cjs +130 -0
  29. package/extensions/setup/intercom.ts +603 -0
  30. package/extensions/shared/child-session.ts +42 -5
  31. package/extensions/shared/setup-config.ts +27 -1
  32. package/extensions/shared/setup-episode-state.ts +7 -0
  33. package/extensions/shared/tool-surface.ts +435 -0
  34. package/extensions/subagents/index.ts +16 -1
  35. package/extensions/subagents/src/manager.ts +13 -11
  36. package/extensions/subagents/src/prompt.ts +1 -1
  37. package/extensions/tasks/index.ts +39 -12
  38. package/extensions/ui-customization/footer.ts +6 -1
  39. package/extensions/workflows/artifacts.ts +6 -1
  40. package/extensions/workflows/dashboard.ts +138 -27
  41. package/extensions/workflows/graph-projection.ts +240 -0
  42. package/extensions/workflows/handoff.ts +194 -0
  43. package/extensions/workflows/index.ts +258 -56
  44. package/extensions/workflows/invocation-ledger.ts +368 -0
  45. package/extensions/workflows/model.ts +57 -1
  46. package/extensions/workflows/operator.ts +131 -0
  47. package/extensions/workflows/prompt.ts +10 -38
  48. package/extensions/workflows/replay-safety.ts +9 -8
  49. package/extensions/workflows/runner.ts +10 -2
  50. package/extensions/workflows/sandbox.ts +5 -0
  51. package/package.json +15 -15
  52. package/skills/subagents/SKILL.md +6 -0
  53. package/skills/workflows/EXAMPLES.md +58 -0
  54. package/skills/workflows/REFERENCE.md +44 -0
  55. package/skills/workflows/SKILL.md +39 -0
@@ -0,0 +1,368 @@
1
+ export interface InvocationIdentity {
2
+ readonly runId: string;
3
+ readonly callIndex: number;
4
+ }
5
+
6
+ export type InvocationIntentState = "requested";
7
+ export type InvocationAdmissionState =
8
+ | "pending"
9
+ | "claimed"
10
+ | "replayed"
11
+ | "rejected";
12
+ export type InvocationExecutionState =
13
+ | "pending"
14
+ | "running"
15
+ | "settled"
16
+ | "uncertain";
17
+ export type InvocationOutcome = "success" | "error" | "uncertain";
18
+
19
+ /**
20
+ * Three-plane durable lifecycle for one workflow call.
21
+ *
22
+ * Intent says what the script requested, admission says whether the host
23
+ * accepted or replayed it, and execution says what the child runtime actually
24
+ * proved. Keeping them separate prevents a persisted `running` label from
25
+ * pretending that an interrupted side effect is known to have failed.
26
+ */
27
+ export interface InvocationRecord {
28
+ readonly identity: InvocationIdentity;
29
+ readonly intentState: InvocationIntentState;
30
+ readonly admissionState: InvocationAdmissionState;
31
+ readonly executionState: InvocationExecutionState;
32
+ readonly outcome?: InvocationOutcome;
33
+ readonly requestedAt: number;
34
+ readonly claimedAt?: number;
35
+ readonly runningAt?: number;
36
+ readonly terminalAt?: number;
37
+ }
38
+
39
+ export type InvocationTransition =
40
+ | { readonly status: "claimed"; readonly at: number }
41
+ | { readonly status: "running"; readonly at: number }
42
+ | {
43
+ readonly status: "settled";
44
+ readonly outcome: "success" | "error";
45
+ readonly at: number;
46
+ }
47
+ | { readonly status: "replayed"; readonly at: number }
48
+ | { readonly status: "rejected"; readonly at: number };
49
+
50
+ const ADMISSION_STATES: readonly InvocationAdmissionState[] = [
51
+ "pending",
52
+ "claimed",
53
+ "replayed",
54
+ "rejected",
55
+ ];
56
+ const EXECUTION_STATES: readonly InvocationExecutionState[] = [
57
+ "pending",
58
+ "running",
59
+ "settled",
60
+ "uncertain",
61
+ ];
62
+
63
+ function assertIdentity(identity: InvocationIdentity) {
64
+ if (typeof identity.runId !== "string" || identity.runId.trim() === "") {
65
+ throw new Error("runId must be a nonblank string");
66
+ }
67
+ if (!Number.isSafeInteger(identity.callIndex) || identity.callIndex <= 0) {
68
+ throw new Error("callIndex must be a positive safe integer");
69
+ }
70
+ }
71
+
72
+ function assertFinite(value: number | undefined, name: string) {
73
+ if (typeof value !== "number" || !Number.isFinite(value)) {
74
+ throw new Error(`${name} must be finite`);
75
+ }
76
+ return value;
77
+ }
78
+
79
+ function immutableSnapshot(record: InvocationRecord) {
80
+ return Object.freeze({
81
+ ...record,
82
+ identity: Object.freeze({ ...record.identity }),
83
+ });
84
+ }
85
+
86
+ function lifecycleLabel(record: InvocationRecord) {
87
+ if (record.admissionState === "replayed") return "replayed";
88
+ if (record.admissionState === "rejected") return "rejected";
89
+ if (
90
+ record.executionState === "settled" ||
91
+ record.executionState === "uncertain"
92
+ ) {
93
+ return "settled";
94
+ }
95
+ if (record.executionState === "running") return "running";
96
+ if (record.admissionState === "claimed") return "claimed";
97
+ return "requested";
98
+ }
99
+
100
+ function assertRecord(record: InvocationRecord) {
101
+ assertIdentity(record.identity);
102
+ assertFinite(record.requestedAt, "requestedAt");
103
+ if (record.intentState !== "requested") {
104
+ throw new Error("Invalid invocation intent state");
105
+ }
106
+ if (!ADMISSION_STATES.includes(record.admissionState)) {
107
+ throw new Error("Invalid invocation admission state");
108
+ }
109
+ if (!EXECUTION_STATES.includes(record.executionState)) {
110
+ throw new Error("Invalid invocation execution state");
111
+ }
112
+ if (record.admissionState === "claimed") {
113
+ assertFinite(record.claimedAt, "claimedAt");
114
+ }
115
+ if (record.executionState === "running") {
116
+ assertFinite(record.runningAt, "runningAt");
117
+ }
118
+ if (
119
+ record.executionState === "settled" ||
120
+ record.executionState === "uncertain"
121
+ ) {
122
+ assertFinite(record.terminalAt, "terminalAt");
123
+ }
124
+ if (
125
+ record.admissionState === "replayed" &&
126
+ (record.executionState !== "settled" || record.outcome !== "success")
127
+ ) {
128
+ throw new Error("Invalid replayed invocation state");
129
+ }
130
+ if (
131
+ record.admissionState === "rejected" &&
132
+ (record.executionState !== "settled" || record.outcome !== "error")
133
+ ) {
134
+ throw new Error("Invalid rejected invocation state");
135
+ }
136
+ if (
137
+ record.admissionState === "pending" &&
138
+ record.executionState !== "pending" &&
139
+ record.executionState !== "uncertain"
140
+ ) {
141
+ throw new Error("Invalid pending invocation state");
142
+ }
143
+ if (
144
+ record.executionState === "settled" &&
145
+ record.outcome !== "success" &&
146
+ record.outcome !== "error"
147
+ ) {
148
+ throw new Error("Settled invocation requires a success or error outcome");
149
+ }
150
+ if (record.executionState === "uncertain" && record.outcome !== "uncertain") {
151
+ throw new Error("Uncertain invocation requires an uncertain outcome");
152
+ }
153
+ if (
154
+ (record.executionState === "pending" ||
155
+ record.executionState === "running") &&
156
+ record.outcome !== undefined
157
+ ) {
158
+ throw new Error("Nonterminal invocation cannot have an outcome");
159
+ }
160
+ if (
161
+ record.executionState !== "settled" &&
162
+ record.executionState !== "uncertain" &&
163
+ record.terminalAt !== undefined
164
+ ) {
165
+ throw new Error("Nonterminal invocation cannot have terminalAt");
166
+ }
167
+ if (record.admissionState !== "claimed" && record.claimedAt !== undefined) {
168
+ throw new Error("Only a claimed invocation can have claimedAt");
169
+ }
170
+ if (
171
+ record.runningAt !== undefined &&
172
+ (record.admissionState !== "claimed" ||
173
+ (record.executionState !== "running" &&
174
+ record.executionState !== "settled" &&
175
+ record.executionState !== "uncertain"))
176
+ ) {
177
+ throw new Error("Invalid runningAt for invocation state");
178
+ }
179
+ if (
180
+ record.admissionState === "claimed" &&
181
+ record.executionState === "settled" &&
182
+ record.runningAt === undefined
183
+ ) {
184
+ throw new Error("A normally settled invocation requires runningAt");
185
+ }
186
+
187
+ const timestamps = [
188
+ ["requestedAt", record.requestedAt],
189
+ ["claimedAt", record.claimedAt],
190
+ ["runningAt", record.runningAt],
191
+ ["terminalAt", record.terminalAt],
192
+ ] as const;
193
+ let previous = record.requestedAt;
194
+ for (const [name, timestamp] of timestamps.slice(1)) {
195
+ if (timestamp === undefined) continue;
196
+ assertFinite(timestamp, name);
197
+ if (timestamp < previous) {
198
+ throw new Error(`${name} cannot precede an earlier lifecycle timestamp`);
199
+ }
200
+ previous = timestamp;
201
+ }
202
+ }
203
+
204
+ function isRecord(value: unknown): value is Record<string, unknown> {
205
+ return typeof value === "object" && value !== null && !Array.isArray(value);
206
+ }
207
+
208
+ /** Decode untrusted persisted data without inventing missing lifecycle facts. */
209
+ export function decodeInvocationRecord(value: unknown) {
210
+ if (!isRecord(value) || !isRecord(value.identity)) return undefined;
211
+ const candidate = value as unknown as InvocationRecord;
212
+ try {
213
+ assertRecord(candidate);
214
+ return immutableSnapshot(candidate);
215
+ } catch {
216
+ return undefined;
217
+ }
218
+ }
219
+
220
+ function currentTransitionTime(record: InvocationRecord) {
221
+ if (record.terminalAt !== undefined) {
222
+ return {
223
+ name: "terminalAt",
224
+ at: assertFinite(record.terminalAt, "terminalAt"),
225
+ };
226
+ }
227
+ if (record.runningAt !== undefined) {
228
+ return {
229
+ name: "runningAt",
230
+ at: assertFinite(record.runningAt, "runningAt"),
231
+ };
232
+ }
233
+ if (record.claimedAt !== undefined) {
234
+ return {
235
+ name: "claimedAt",
236
+ at: assertFinite(record.claimedAt, "claimedAt"),
237
+ };
238
+ }
239
+ return { name: "requestedAt", at: record.requestedAt };
240
+ }
241
+
242
+ function assertChronology(record: InvocationRecord, at: number) {
243
+ assertFinite(at, "at");
244
+ const current = currentTransitionTime(record);
245
+ if (at < current.at) {
246
+ throw new Error(`Transition time cannot precede ${current.name}`);
247
+ }
248
+ }
249
+
250
+ function isLegalTransition(
251
+ record: InvocationRecord,
252
+ next: InvocationTransition["status"],
253
+ ) {
254
+ if (
255
+ record.admissionState === "pending" &&
256
+ record.executionState === "pending"
257
+ ) {
258
+ return next === "claimed" || next === "replayed" || next === "rejected";
259
+ }
260
+ if (
261
+ record.admissionState === "claimed" &&
262
+ record.executionState === "pending"
263
+ ) {
264
+ return next === "running";
265
+ }
266
+ if (
267
+ record.admissionState === "claimed" &&
268
+ record.executionState === "running"
269
+ ) {
270
+ return next === "settled";
271
+ }
272
+ return false;
273
+ }
274
+
275
+ /** Structured identity avoids ambiguous string concatenation of run and call. */
276
+ export function createInvocationIdentity(runId: string, callIndex: number) {
277
+ const identity = { runId, callIndex };
278
+ assertIdentity(identity);
279
+ return Object.freeze(identity);
280
+ }
281
+
282
+ /** Start a call record without consulting a clock inside the ledger. */
283
+ export function requestInvocation(identity: InvocationIdentity, at: number) {
284
+ assertIdentity(identity);
285
+ assertFinite(at, "at");
286
+ return immutableSnapshot({
287
+ identity,
288
+ intentState: "requested",
289
+ admissionState: "pending",
290
+ executionState: "pending",
291
+ requestedAt: at,
292
+ });
293
+ }
294
+
295
+ /** Apply one normal lifecycle or replay/rejection transition. */
296
+ export function transitionInvocation(
297
+ record: InvocationRecord,
298
+ transition: InvocationTransition,
299
+ ) {
300
+ assertRecord(record);
301
+ if (!isLegalTransition(record, transition.status)) {
302
+ throw new Error(
303
+ `Illegal invocation transition: ${lifecycleLabel(record)} -> ${transition.status}`,
304
+ );
305
+ }
306
+ assertChronology(record, transition.at);
307
+
308
+ switch (transition.status) {
309
+ case "claimed":
310
+ return immutableSnapshot({
311
+ ...record,
312
+ admissionState: "claimed",
313
+ claimedAt: transition.at,
314
+ });
315
+ case "running":
316
+ return immutableSnapshot({
317
+ ...record,
318
+ executionState: "running",
319
+ runningAt: transition.at,
320
+ });
321
+ case "settled":
322
+ return immutableSnapshot({
323
+ ...record,
324
+ executionState: "settled",
325
+ outcome: transition.outcome,
326
+ terminalAt: transition.at,
327
+ });
328
+ case "replayed":
329
+ return immutableSnapshot({
330
+ ...record,
331
+ admissionState: "replayed",
332
+ executionState: "settled",
333
+ outcome: "success",
334
+ terminalAt: transition.at,
335
+ });
336
+ case "rejected":
337
+ return immutableSnapshot({
338
+ ...record,
339
+ admissionState: "rejected",
340
+ executionState: "settled",
341
+ outcome: "error",
342
+ terminalAt: transition.at,
343
+ });
344
+ }
345
+ }
346
+
347
+ /** Interrupt recovery is conservative: execution may have happened. */
348
+ export function classifyInterruptedInvocation(
349
+ record: InvocationRecord,
350
+ at: number,
351
+ ) {
352
+ assertRecord(record);
353
+ if (
354
+ record.executionState === "settled" ||
355
+ record.executionState === "uncertain"
356
+ ) {
357
+ throw new Error(
358
+ `Cannot classify terminal invocation ${lifecycleLabel(record)} as uncertain`,
359
+ );
360
+ }
361
+ assertChronology(record, at);
362
+ return immutableSnapshot({
363
+ ...record,
364
+ executionState: "uncertain",
365
+ outcome: "uncertain",
366
+ terminalAt: at,
367
+ });
368
+ }
@@ -12,6 +12,12 @@ import { formatContextUtilization } from "../shared/context-utilization.ts";
12
12
  import { sanitizeTerminalText } from "../shared/terminal-text.ts";
13
13
  import type { WorktreeCleanup } from "../shared/worktree.ts";
14
14
  import type { AcceptanceLedger } from "./acceptance.ts";
15
+ import {
16
+ projectWorkflowGraph,
17
+ type WorkflowGraphProjection,
18
+ type WorkflowGraphRecord,
19
+ } from "./graph-projection.ts";
20
+ import type { InvocationRecord } from "./invocation-ledger.ts";
15
21
  import { safeStringify } from "./serialization.ts";
16
22
 
17
23
  export type Theme = ExtensionContext["ui"]["theme"];
@@ -45,7 +51,11 @@ export type AgentState = "running" | "done" | "error";
45
51
  export type WorkflowStatus = "running" | "completed" | "failed" | "aborted";
46
52
 
47
53
  export type TranscriptRole =
48
- "user" | "assistant" | "thinking" | "tool" | "toolResult";
54
+ | "user"
55
+ | "assistant"
56
+ | "thinking"
57
+ | "tool"
58
+ | "toolResult";
49
59
 
50
60
  export interface TranscriptEntry {
51
61
  role: TranscriptRole;
@@ -65,6 +75,15 @@ export interface TranscriptEntry {
65
75
 
66
76
  export interface AgentRecord {
67
77
  index: number;
78
+ /** Stable identity and independent intent/admission/execution status planes. */
79
+ callId?: string;
80
+ invocation?: InvocationRecord;
81
+ /** Ephemeral same-run child Session reused for this logical operator. */
82
+ operatorKey?: string;
83
+ /** Explicit result-reference dependencies used by the derived graph. */
84
+ inputCallIds?: string[];
85
+ /** Opaque same-run reference returned to the workflow script. */
86
+ resultRef?: string;
68
87
  label: string;
69
88
  phase?: string;
70
89
  state: AgentState;
@@ -122,9 +141,46 @@ export interface WorkflowDetails {
122
141
  resumedFrom?: string;
123
142
  /** Why a requested resume produced no cache, for an honest result message. */
124
143
  resumeNote?: string;
144
+ /** Read-only lineage projection; never execution or admission authority. */
145
+ graph?: WorkflowGraphProjection;
125
146
  error?: string;
126
147
  }
127
148
 
149
+ export function workflowGraphRecords(
150
+ agents: readonly AgentRecord[],
151
+ ): WorkflowGraphRecord[] {
152
+ return agents.flatMap((agent) =>
153
+ agent.callId
154
+ ? [
155
+ {
156
+ callId: agent.callId,
157
+ index: agent.index,
158
+ label: agent.label,
159
+ state: agent.state,
160
+ ...(agent.invocation
161
+ ? {
162
+ admissionState: agent.invocation.admissionState,
163
+ executionState: agent.invocation.executionState,
164
+ }
165
+ : {}),
166
+ ...(agent.operatorKey ? { operatorKey: agent.operatorKey } : {}),
167
+ ...(agent.inputCallIds
168
+ ? { inputCallIds: [...agent.inputCallIds] }
169
+ : {}),
170
+ ...(agent.resultRef ? { resultRef: agent.resultRef } : {}),
171
+ },
172
+ ]
173
+ : [],
174
+ );
175
+ }
176
+
177
+ /** Refresh the descriptive graph without granting it scheduling authority. */
178
+ export function refreshWorkflowGraph(details: WorkflowDetails) {
179
+ const records = workflowGraphRecords(details.agents);
180
+ details.graph = projectWorkflowGraph(records);
181
+ return details.graph;
182
+ }
183
+
128
184
  export const MAX_LOG_ENTRIES = 100;
129
185
  export const MAX_LOG_TEXT = 300;
130
186
 
@@ -0,0 +1,131 @@
1
+ import { SessionManager } from "@earendil-works/pi-coding-agent";
2
+
3
+ export interface WorkflowOperatorActivation {
4
+ key: string;
5
+ fingerprint: string;
6
+ cwd: string;
7
+ signal?: AbortSignal;
8
+ }
9
+
10
+ type OperatorEntry = {
11
+ fingerprint: string;
12
+ cwd: string;
13
+ sessionManager: SessionManager;
14
+ tail: Promise<void>;
15
+ };
16
+
17
+ export function normalizeWorkflowOperatorKey(key: string) {
18
+ if (typeof key !== "string" || !/^[A-Za-z0-9._:-]{1,80}$/.test(key)) {
19
+ throw new Error(
20
+ "Workflow operator key must be 1-80 ASCII letters, digits, or ._:- characters",
21
+ );
22
+ }
23
+ return key;
24
+ }
25
+
26
+ function abortError(signal: AbortSignal) {
27
+ if (signal.reason instanceof Error) return signal.reason;
28
+ const error = new Error("Workflow operator activation was aborted");
29
+ error.name = "AbortError";
30
+ return error;
31
+ }
32
+
33
+ function invokeAfter<T>(
34
+ turn: Promise<void>,
35
+ signal: AbortSignal | undefined,
36
+ invoke: () => T | PromiseLike<T>,
37
+ ) {
38
+ if (!signal) return turn.then(invoke);
39
+ if (signal.aborted) return Promise.reject(abortError(signal));
40
+
41
+ return new Promise<T>((resolve, reject) => {
42
+ let waiting = true;
43
+ const onAbort = () => {
44
+ if (!waiting) return;
45
+ waiting = false;
46
+ signal.removeEventListener("abort", onAbort);
47
+ reject(abortError(signal));
48
+ };
49
+ signal.addEventListener("abort", onAbort, { once: true });
50
+ void turn.then(
51
+ () => {
52
+ if (!waiting) return;
53
+ if (signal.aborted) {
54
+ onAbort();
55
+ return;
56
+ }
57
+ waiting = false;
58
+ signal.removeEventListener("abort", onAbort);
59
+ try {
60
+ Promise.resolve(invoke()).then(resolve, reject);
61
+ } catch (error) {
62
+ reject(error);
63
+ }
64
+ },
65
+ (error) => {
66
+ if (!waiting) return;
67
+ waiting = false;
68
+ signal.removeEventListener("abort", onAbort);
69
+ reject(error);
70
+ },
71
+ );
72
+ });
73
+ }
74
+
75
+ /**
76
+ * Per-run, in-process operator sessions. Nothing in this registry is persisted
77
+ * or shared with another process.
78
+ */
79
+ export class WorkflowOperatorRegistry {
80
+ private readonly operators = new Map<string, OperatorEntry>();
81
+ private closed = false;
82
+ private closePromise?: Promise<void>;
83
+
84
+ activate<T>(
85
+ activation: WorkflowOperatorActivation,
86
+ invoke: (sessionManager: SessionManager) => T | PromiseLike<T>,
87
+ ) {
88
+ if (this.closed) {
89
+ return Promise.reject(new Error("Workflow operator registry is closed"));
90
+ }
91
+
92
+ const key = normalizeWorkflowOperatorKey(activation.key);
93
+ let operator = this.operators.get(key);
94
+ if (!operator) {
95
+ operator = {
96
+ fingerprint: activation.fingerprint,
97
+ cwd: activation.cwd,
98
+ sessionManager: SessionManager.inMemory(activation.cwd),
99
+ tail: Promise.resolve(),
100
+ };
101
+ this.operators.set(key, operator);
102
+ } else if (
103
+ operator.fingerprint !== activation.fingerprint ||
104
+ operator.cwd !== activation.cwd
105
+ ) {
106
+ return Promise.reject(
107
+ new Error(`Workflow operator identity mismatch for "${key}"`),
108
+ );
109
+ }
110
+ const previous = operator.tail;
111
+ const result = invokeAfter(previous, activation.signal, () =>
112
+ invoke(operator.sessionManager),
113
+ );
114
+ operator.tail = Promise.allSettled([previous, result]).then(
115
+ () => undefined,
116
+ );
117
+ return result;
118
+ }
119
+
120
+ close() {
121
+ if (this.closePromise) return this.closePromise;
122
+ this.closed = true;
123
+ const queues = [...this.operators.values()].map(
124
+ (operator) => operator.tail,
125
+ );
126
+ this.closePromise = Promise.allSettled(queues).then(() => {
127
+ this.operators.clear();
128
+ });
129
+ return this.closePromise;
130
+ }
131
+ }
@@ -15,7 +15,7 @@ export const WORKFLOW_PARAMETER_DESCRIPTIONS = {
15
15
  background:
16
16
  "Run in the background: the tool returns a run id immediately and you receive a follow-up message when the workflow finishes. Defaults to false (blocking with live progress).",
17
17
  resumeFromRunId:
18
- 'Optional run id of a previous workflow (e.g. "wf_1a2b3c4d5e6f", or a unique suffix) to replay cached read-only agent results. A call replays only when its prompt, resolved agent type/schema/model/provider/effort, canonical cwd, repository state, loaded resources, and trust context match. Unrestricted/no-type agents, writable or unknown tool lists, worktree-isolated agents, failed calls, and calls whose context cannot be fingerprinted always run for real. Matching remains content-based and order-independent. Old or unknown journals simply run everything fresh.',
18
+ "Optional prior run id or unique suffix for safe read-only replay. See the workflows Skill for matching rules.",
19
19
  };
20
20
 
21
21
  /** Describes stopping a running background workflow, mirroring subagent_cancel/bg_kill. */
@@ -41,53 +41,25 @@ export const WORKFLOW_STATUS_PARAMETER_DESCRIPTIONS = {
41
41
  export const WORKFLOW_LIFECYCLE_PROMPT_SNIPPET =
42
42
  "Inspect (workflow_status) or cancel (workflow_stop) a background workflow by run id";
43
43
 
44
- /** Defines the workflow DSL, constraints, reliability guidance, and model-authored task examples. */
44
+ /** Compact resident contract; the workflows Skill carries the complete guide. */
45
45
  export const WORKFLOW_TOOL_DESCRIPTION = [
46
46
  "Use the workflow tool when the user explicitly requests a workflow run or when the task clearly requires multi-phase dynamic orchestration.",
47
- "Run a multi-agent workflow from a JavaScript orchestration script you write inline. Use this when a task benefits from fanning work out across several isolated subagents in ordered phases (research fan-out, per-file review, verify-then-synthesize pipelines).",
48
- "The script runs as an async function body with these primitives:",
49
- " export const meta = { name, description, phases: [{ title, detail? }] } — metadata for the progress UI. Declare all phases up front.",
50
- " phase(title) mark the current phase at runtime (use titles from meta.phases).",
51
- " log(message) — emit one progress line to the user and to your own final report. This is the run's narrator: use it for anything the reader needs while the run is still going, or that the return value would not capture — round counts, how many agents were dropped, why a branch was skipped. Unlike phase(), it does not touch the phase list. Lines are one row each (newlines are flattened); the most recent 100 are kept and any earlier ones are reported as dropped.",
52
- "• usage() — read this run's cumulative token spend so far: { input, output, cacheRead, cacheWrite, total, cost, agents }. The reading refreshes as each agent settles, so evaluating it right after an `await` reflects that agent. `total` never decreases, but it is a LOWER BOUND rather than an exact figure: a child session that compacts drops the tokens of the messages it discarded. Use it to report or adapt cost — e.g. log a running total, or stop a discovery loop once the spend stops paying for itself — and expect a long run to have spent somewhat more than it says. It is a reading, not a limit: nothing is enforced for you.",
53
- "• await agent(prompt, { agent_type?, label?, phase?, schema?, acceptance?, model?, provider?, effort?, isolation? }) — run ONE subagent in an isolated context and wait for it. `agent_type` applies the same named preset and enforced capabilities as subagent_spawn: specialized system prompt, tool allowlist, model assignment, and default effort. Prefer a matching type when one exists. Model precedence is explicit model/provider > type-file model > configured built-in role model > parent model; effort precedence is explicit effort > type default > parent effort. Omit `agent_type` for a general-purpose child. Always resolves to { ok, output, structured?, acceptance?, error? }. Check `ok` before using the result. When you pass a JSON `schema`, `structured` holds the validated object on success. Optional `acceptance: { criteria: [{ id, description, requiredEvidence? }] }` is explicit and adds no extra agent: the same structured result must include an evidence ledger; missing, malformed, or rejected criteria make `ok:false` while preserving output and ledger. Children receive normal built-ins and trust-appropriate extensions, settings, skills, and AGENTS.md context, but cannot recursively orchestrate or ask the user.",
54
- "• isolation: 'worktree' runs that one agent in its own git worktree on its own branch, instead of the shared working directory. Use it for any fan-out where agents WRITE — without it, concurrent agents share one checkout and one git index, so their edits and `git add`s silently overwrite each other. Tell such an agent to COMMIT its work: on completion the worktree directory is reclaimed and its branch is kept for you to merge (an empty branch is deleted; uncommitted changes keep the directory instead). The branch name comes back in the run artifacts. Costs a fresh checkout, needs a git repo, and starts without gitignored files, so leave it off for read-only agents.",
55
- "• await parallel([() => agent(...), () => agent(...)], { concurrency? }) — run zero-argument agent thunks concurrently and return results in order. This is a BARRIER: nothing after it starts until every thunk settles. A thunk that throws settles to null (filter it out) rather than failing the whole batch, so one bad item never discards the others' results. The package default is 8 concurrent agents per workflow and can be changed with /my-pi-setup (hard maximum 64).",
56
- "• await pipeline(items, stage1, stage2, ...) — run each item through every stage independently, with NO barrier between stages: item A can be in stage 3 while item B is still in stage 1. Results come back in input order. Each stage receives (previousResult, originalItem, index), so a later stage can label its work without threading context through the earlier stage's return value. A stage that throws drops that item to null and skips its remaining stages, leaving siblings untouched.",
57
- "PREFER pipeline() for multi-stage work. parallel() forces every item to wait for the slowest one in each stage, so wall-clock becomes the sum of per-stage worst cases (max stage1 + max stage2) instead of the slowest single chain. The gap is widest when different items are slow in different stages; when one item is slowest everywhere it is the critical path either way. Reach for a barrier only when a stage genuinely needs cross-item context from ALL of the previous one: deduping or merging the full result set, exiting early when the total count is zero, or a prompt that compares one finding against the others. Needing to flatten/map/filter in between is NOT a reason — do that inside a pipeline stage.",
58
-
59
- "• args — the parsed value of the `args` tool parameter (or undefined).",
60
- "Workflow JavaScript runs in a restricted, killable child with no imports, eval, timers, filesystem, network, or process APIs. The package default permits 128 agent calls per run and can be changed with /my-pi-setup (hard maximum 1024); there is no overall deadline. Each agent must receive its first assistant response event within 45 seconds so silent provider requests fail clearly; after that, agent() has no wall-clock deadline. Each individual child tool call times out independently after 3 minutes, becomes an error tool result, and leaves the agent loop free to recover. Use map/filter/if/await/template strings to orchestrate, and `return` a JSON-serializable aggregate.",
61
- "Pass a `schema` to agent() whenever a later step branches on the result, so you get typed fields instead of prose. Artifacts are saved under ~/.pi/agent/workflows/<runId>/ for inspection. To re-run an edited workflow cheaply, pass `resume_from_run_id` with the previous run id: only provably read-only calls whose content and project/resource context are unchanged can replay; writable, unrestricted, unknown-tool, and worktree calls always run for real.",
62
- "Example — each file is verified as soon as ITS OWN scan lands, instead of waiting for every scan:",
63
- "export const meta = { name: 'reliability-review', description: 'Review modules for reliability risks, then report', phases: [{ title: 'Scan' }, { title: 'Verify' }, { title: 'Report' }] }",
64
- "const FINDINGS = { type: 'object', properties: { issues: { type: 'array', items: { type: 'string' } }, ok: { type: 'boolean' } }, required: ['issues', 'ok'] }",
65
- "phase('Scan')",
66
- "const checked = await pipeline(args.files,",
67
- " (f) => agent(`Trace ${f} for candidate reliability risks with file:line evidence.`, { agent_type: 'explorer', label: `scan:${f}`, phase: 'Scan', schema: FINDINGS }),",
68
- " (scan, f) => scan.ok ? agent(`Review whether these candidate issues in ${f} are real: ${JSON.stringify(scan.structured.issues)}`, { agent_type: 'reviewer', label: `verify:${f}`, phase: 'Verify' }) : null)",
69
- "const verified = checked.filter((r) => r && r.ok)",
70
- "const dropped = checked.length - verified.length // agents that failed/dropped: surface, never silently swallow",
71
- "if (dropped) log(`${dropped}/${checked.length} file(s) dropped before verification`)",
72
- "phase('Report')",
73
- "const report = await agent(`Synthesize tradeoffs and recommendations from these verified findings: ${JSON.stringify(verified.map((r) => r.output))}`, { agent_type: 'advisor', label: 'report', phase: 'Report' })",
74
- "log(`done — ${verified.length} verified, ${usage().total} tokens`)",
75
- "return { verified: verified.length, dropped, report: report.ok ? report.output : report.error }",
47
+ "Write an async JavaScript body using optional meta, phase(), log(), usage(), agent(), pipeline(), parallel(), args, and a JSON-serializable return.",
48
+ "agent() returns { ok, output, structured?, ref?, error? }; always check `.ok`, use a schema for branching, and surface failed or null results.",
49
+ "Prefer pipeline() for independent multi-stage items. Use parallel() only for a real barrier where the next step needs every prior result.",
50
+ "For concurrent writers use isolation: 'worktree' and tell each agent to commit. Read-only work should normally stay in the shared checkout.",
51
+ "Read the workflows Skill before a nontrivial script; it covers the restricted sandbox, full DSL, acceptance, result refs, replay, background lifecycle, limits, and examples.",
76
52
  ].join("\n");
77
53
 
78
54
  /** Adds workflow orchestration primitives and background execution to the model's tool prompt. */
79
55
  export const WORKFLOW_PROMPT_SNIPPET =
80
- "Orchestrate isolated subagents from an inline JS script: phase()/agent()/pipeline()/parallel() with structured outputs, log() progress, usage() token readings, and optional background execution";
56
+ "Orchestrate subagents from an inline JS script; read the workflows Skill for the complete DSL";
81
57
 
82
58
  /** Guides the model on appropriate workflow fan-out and mandatory agent result checks. */
83
59
  export const WORKFLOW_PROMPT_GUIDELINES = [
84
60
  "Use workflow when a task needs several subagents with phase dependencies or dynamic fan-out; keep single small delegations in the main session.",
85
- "For each workflow agent() call, select a matching agent_type when one exists (explorer, implementer, reviewer, advisor, or a loaded custom type) so its configured model, prompt, effort, and enforced tools apply; do not hardcode that role's model. Omit agent_type only for genuinely general-purpose work.",
86
- "Default to pipeline() for multi-stage fan-out so each item advances as soon as its own previous stage lands; use parallel() only when a stage truly needs every prior result at once.",
87
- "In workflow scripts, agent() never throws — check `.ok` before using `.output`/`.structured`; but parallel() and pipeline() settle a throwing thunk or stage to `null`, so guard those with `r && r.ok`.",
88
- "A filtered-out or null result is a failed agent, not a clean pass: surface how many dropped (e.g. return a count) so a crashed or timed-out agent never reads as success.",
89
- "log() anything the reader would want before the run ends — round counts, dropped agents, why a branch was skipped. A long run that narrates nothing is indistinguishable from a stalled one, and the return value only arrives at the end.",
90
- "When several agents will edit files concurrently, give each one isolation: 'worktree' and tell it to commit; otherwise they share one checkout and one git index and overwrite each other. Read-only agents do not need it.",
61
+ "select a matching agent_type when available; use its configured model and do not hardcode that role's model.",
62
+ "Read the workflows Skill before a nontrivial script; check every agent result, surface dropped work, and use worktree isolation for concurrent writers.",
91
63
  ];
92
64
 
93
65
  /** Marks and forwards a workflow script's agent() task as an isolated child-model prompt. */