@tt-a1i/openpi 0.5.0 → 0.6.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 (74) hide show
  1. package/README.md +18 -10
  2. package/SETUP.md +8 -2
  3. package/THIRD_PARTY_NOTICES.md +16 -0
  4. package/bin/openpi.js +25 -15
  5. package/extensions/ai-providers/LICENSE.upstream +23 -0
  6. package/extensions/ai-providers/README.md +59 -0
  7. package/extensions/ai-providers/antigravity/credentials.ts +52 -0
  8. package/extensions/ai-providers/antigravity/discovery.ts +130 -0
  9. package/extensions/ai-providers/antigravity/google-conversion.ts +455 -0
  10. package/extensions/ai-providers/antigravity/models.ts +84 -0
  11. package/extensions/ai-providers/antigravity/oauth.ts +700 -0
  12. package/extensions/ai-providers/antigravity/provider.ts +1116 -0
  13. package/extensions/ai-providers/antigravity/routing.ts +340 -0
  14. package/extensions/ai-providers/antigravity/with-resolvers.d.ts +19 -0
  15. package/extensions/ai-providers/cursor/constants.ts +5 -0
  16. package/extensions/ai-providers/cursor/credentials.ts +14 -0
  17. package/extensions/ai-providers/cursor/discovery.ts +291 -0
  18. package/extensions/ai-providers/cursor/input-images.ts +106 -0
  19. package/extensions/ai-providers/cursor/models.ts +45 -0
  20. package/extensions/ai-providers/cursor/oauth.ts +263 -0
  21. package/extensions/ai-providers/cursor/proto.ts +1064 -0
  22. package/extensions/ai-providers/cursor/protobuf.ts +1171 -0
  23. package/extensions/ai-providers/cursor/provider.ts +1175 -0
  24. package/extensions/ai-providers/cursor/proxy.ts +213 -0
  25. package/extensions/ai-providers/cursor/with-resolvers.d.ts +12 -0
  26. package/extensions/ai-providers/index.ts +86 -0
  27. package/extensions/ai-providers/oauth-adapter.ts +81 -0
  28. package/extensions/ai-providers/usage.ts +10 -0
  29. package/extensions/background-terminals/index.ts +8 -1
  30. package/extensions/background-terminals/src/manager.ts +3 -5
  31. package/extensions/background-terminals/src/result-delivery.ts +43 -23
  32. package/extensions/cron/index.ts +68 -27
  33. package/extensions/cron/schedule.ts +5 -1
  34. package/extensions/model-info/cache-diagnostics.ts +220 -0
  35. package/extensions/model-info/index.ts +45 -1
  36. package/extensions/plan-mode/index.ts +75 -4
  37. package/extensions/setup/index.ts +15 -3
  38. package/extensions/shared/child-session.ts +25 -5
  39. package/extensions/shared/completion-inbox.ts +193 -0
  40. package/extensions/shared/setup-config.ts +10 -1
  41. package/extensions/shared/structured-output.ts +154 -0
  42. package/extensions/subagents/index.ts +44 -4
  43. package/extensions/subagents/src/backends/pi.ts +76 -5
  44. package/extensions/subagents/src/domain.ts +16 -1
  45. package/extensions/subagents/src/manager.ts +5 -0
  46. package/extensions/subagents/src/prompt.ts +17 -3
  47. package/extensions/subagents/src/result-artifact.ts +32 -0
  48. package/extensions/subagents/src/result-delivery.ts +33 -14
  49. package/extensions/ui-customization/footer.ts +16 -5
  50. package/extensions/user-input-fold/index.ts +42 -6
  51. package/extensions/web/index.ts +25 -2
  52. package/extensions/workflows/acceptance.ts +43 -19
  53. package/extensions/workflows/completion-projection.ts +3 -1
  54. package/extensions/workflows/dashboard.ts +8 -0
  55. package/extensions/workflows/index.ts +13 -0
  56. package/extensions/workflows/model.ts +5 -1
  57. package/extensions/workflows/prompt.ts +4 -10
  58. package/extensions/workflows/result-delivery.ts +96 -22
  59. package/extensions/workflows/retention.ts +6 -0
  60. package/extensions/workflows/runner.ts +6 -71
  61. package/package.json +7 -7
  62. package/skills/subagents/REFERENCE.md +3 -2
  63. package/skills/subagents/SKILL.md +1 -0
  64. package/skills/workflows/REFERENCE.md +3 -3
  65. package/skills/workflows/SKILL.md +1 -1
  66. package/web/adapter/pi-adapter.ts +3 -0
  67. package/web/host/pi-coding-agent-entry.ts +162 -0
  68. package/web/host/web-host.ts +330 -50
  69. package/web/protocol/types.ts +5 -0
  70. package/web/runtime/pi-runtime.ts +240 -25
  71. package/web/runtime/types.ts +32 -1
  72. package/web/ui/app.js +343 -41
  73. package/web/ui/index.html +3 -0
  74. package/web/ui/styles.css +119 -37
@@ -67,6 +67,10 @@ export type WorkflowDeliveryState =
67
67
  export interface WorkflowDelivery {
68
68
  /** Stable per-run idempotency identity, never a transport-batch id. */
69
69
  id: string;
70
+ /** Destination transcript identity; legacy records fall back to run.sessionId. */
71
+ ownerSessionId?: string;
72
+ /** Process-local generation of the Pi SessionManager owner. */
73
+ ownerEpoch?: number;
70
74
  state: WorkflowDeliveryState;
71
75
  attempts: number;
72
76
  updatedAt: number;
@@ -123,7 +127,7 @@ export interface AgentRecord {
123
127
  usage: AgentUsage;
124
128
  /** Replayed from a prior run's journal instead of actually executed. */
125
129
  replayed?: boolean;
126
- /** Explicit caller-supplied acceptance result; never inferred from role/task. */
130
+ /** Deprecated child self-attestation; never runtime-observed evidence. */
127
131
  acceptance?: AcceptanceLedger;
128
132
  /** Branch of the isolated worktree this agent ran in, when it holds commits. */
129
133
  worktreeBranch?: string;
@@ -56,7 +56,7 @@ export const WORKFLOW_TOOL_DESCRIPTION = [
56
56
  "Interactive sessions launch in the background by default and deliver completion later. Set wait: true only when this tool call must return the final result inline.",
57
57
  "Derive fan-out from independent verifiable work items and task difficulty. Concurrency is a runtime ceiling, not a target or the total-call limit; user cost, count, model, and effort constraints take precedence.",
58
58
  "For concurrent writers use isolation: 'worktree' and tell each agent to commit. Read-only work should normally stay in the shared checkout.",
59
- "Read the workflows Skill before a nontrivial script; it covers the restricted sandbox, full DSL, acceptance, result refs, replay, background lifecycle, limits, and examples.",
59
+ "Read the workflows Skill before a nontrivial script; it covers the restricted sandbox, full DSL, result refs, replay, background lifecycle, limits, and examples.",
60
60
  ].join("\n");
61
61
 
62
62
  /** Adds workflow orchestration primitives and background execution to the model's tool prompt. */
@@ -75,14 +75,6 @@ export function buildWorkflowAgentPrompt(prompt: string) {
75
75
  return prompt;
76
76
  }
77
77
 
78
- /** Instructs structured workflow children to terminate with exactly one structured_output call. */
79
- export const STRUCTURED_OUTPUT_SYSTEM_INSTRUCTION =
80
- "When your task is complete, call the `structured_output` tool exactly once as your final action, with fields matching the required schema. Do not write any other text after it.";
81
-
82
- /** Describes the terminating structured_output tool and its final-action contract. */
83
- export const STRUCTURED_OUTPUT_TOOL_DESCRIPTION =
84
- "Return your final result as structured data matching the required schema. Call this exactly once, as your last action; do not write any other text after it.";
85
-
86
78
  /** Builds the workflow completion report returned to the parent model. */
87
79
  export function buildWorkflowResultMessage(
88
80
  details: WorkflowDetails,
@@ -157,7 +149,9 @@ export function buildWorkflowResultMessage(
157
149
  : "running";
158
150
  lines.push(
159
151
  `- [${agent.label}]${agent.phase ? ` (${agent.phase})` : ""} ${status}` +
160
- (agent.acceptance ? ` · acceptance ${agent.acceptance.status}` : "") +
152
+ (agent.acceptance
153
+ ? ` · deprecated model self-attestation ${agent.acceptance.status}`
154
+ : "") +
161
155
  (agent.error ? ` — ${agent.error}` : ""),
162
156
  );
163
157
  }
@@ -1,8 +1,13 @@
1
- import type { WorkflowDetails } from "./model.ts";
1
+ import {
2
+ type CompletionEnvelope,
3
+ type CompletionOwner,
4
+ createCompletionInbox,
5
+ } from "../shared/completion-inbox.ts";
2
6
  import type {
3
7
  DurableResultDeliveryQueue,
4
8
  DurableResultDeliveryReceipt,
5
9
  } from "../shared/result-delivery.ts";
10
+ import type { WorkflowDetails } from "./model.ts";
6
11
 
7
12
  export interface WorkflowCompletionEnvelope {
8
13
  deliveryId: string;
@@ -12,6 +17,7 @@ export interface WorkflowCompletionEnvelope {
12
17
 
13
18
  export interface WorkflowResultDeliveryOptions {
14
19
  isIdle: () => boolean;
20
+ owner?: () => CompletionOwner | undefined;
15
21
  persist: (details: WorkflowDetails) => void;
16
22
  deliver: (
17
23
  envelopes: readonly WorkflowCompletionEnvelope[],
@@ -34,7 +40,9 @@ function errorText(error: unknown) {
34
40
  export function createWorkflowResultDelivery(
35
41
  options: WorkflowResultDeliveryOptions,
36
42
  ) {
37
- const pending = new Map<string, WorkflowCompletionEnvelope>();
43
+ const inbox = createCompletionInbox<WorkflowCompletionEnvelope>();
44
+ const currentOwner =
45
+ options.owner ?? (() => ({ sessionId: "test", epoch: 0 }));
38
46
  let flushing: Promise<void> | undefined;
39
47
  let flushRequested = false;
40
48
  let wakeRequested = false;
@@ -57,10 +65,44 @@ export function createWorkflowResultDelivery(
57
65
  options.persist(details);
58
66
  };
59
67
 
60
- const enqueue = (envelope: WorkflowCompletionEnvelope) => {
61
- pending.set(envelope.deliveryId, envelope);
68
+ const inboxEnvelope = (
69
+ envelope: WorkflowCompletionEnvelope,
70
+ ): CompletionEnvelope<WorkflowCompletionEnvelope> => {
71
+ const delivery = envelope.details.delivery;
72
+ const deliverySessionId = delivery?.ownerSessionId;
73
+ const detailsSessionId = envelope.details.sessionId;
74
+
75
+ // Conflicting owner fields invalidate delivery ownership fail-closed.
76
+ const hasConflict =
77
+ deliverySessionId !== undefined &&
78
+ detailsSessionId !== undefined &&
79
+ deliverySessionId !== detailsSessionId;
80
+
81
+ const ownerSessionId = hasConflict ? undefined : deliverySessionId;
82
+ return {
83
+ deliveryId: envelope.deliveryId,
84
+ owner: {
85
+ sessionId:
86
+ ownerSessionId && ownerSessionId !== "unowned"
87
+ ? ownerSessionId
88
+ : "unowned",
89
+ epoch: delivery?.ownerEpoch ?? 0,
90
+ },
91
+ producer: "workflow",
92
+ producerId: envelope.runId,
93
+ terminalRef: {
94
+ kind: "workflow-terminal",
95
+ runId: envelope.runId,
96
+ status: envelope.details.status,
97
+ },
98
+ wake: "producer-policy",
99
+ payload: envelope,
100
+ };
62
101
  };
63
102
 
103
+ const enqueue = (envelope: WorkflowCompletionEnvelope) =>
104
+ inbox.defer(inboxEnvelope(envelope), currentOwner());
105
+
64
106
  const retainPending = (
65
107
  envelope: WorkflowCompletionEnvelope,
66
108
  patch: Partial<NonNullable<WorkflowDetails["delivery"]>>,
@@ -69,7 +111,7 @@ export function createWorkflowResultDelivery(
69
111
  // Memory owns the retry before persistence is attempted. A broken disk
70
112
  // must not make this envelope, or any sibling after it, disappear from the
71
113
  // current process.
72
- enqueue(envelope);
114
+ const admitted = enqueue(envelope);
73
115
  try {
74
116
  persistState(envelope.details, "pending", patch);
75
117
  } catch (error) {
@@ -83,6 +125,7 @@ export function createWorkflowResultDelivery(
83
125
  };
84
126
  }
85
127
  }
128
+ return admitted;
86
129
  };
87
130
 
88
131
  const flush = async (wake: boolean) => {
@@ -91,17 +134,16 @@ export function createWorkflowResultDelivery(
91
134
  wakeRequested ||= wake;
92
135
  return flushing;
93
136
  }
94
- if (pending.size === 0) return;
137
+ if (inbox.size() === 0) return;
95
138
 
96
139
  flushing = (async () => {
97
140
  let passWake = wake;
98
- while (pending.size > 0) {
141
+ while (inbox.size() > 0) {
99
142
  flushRequested = false;
100
143
  wakeRequested = false;
101
- const envelopes = [...pending.values()];
102
- for (const envelope of envelopes) {
103
- pending.delete(envelope.deliveryId);
104
- }
144
+ const claimed = inbox.claim(currentOwner());
145
+ const envelopes = claimed.map((envelope) => envelope.payload);
146
+ if (envelopes.length === 0) break;
105
147
 
106
148
  let receipts: readonly DurableResultDeliveryReceipt[] | undefined;
107
149
  try {
@@ -109,6 +151,7 @@ export function createWorkflowResultDelivery(
109
151
  } catch (error) {
110
152
  const message = errorText(error);
111
153
  for (const envelope of envelopes) {
154
+ inbox.acknowledge([envelope.deliveryId]);
112
155
  retainPending(
113
156
  envelope,
114
157
  {
@@ -127,6 +170,7 @@ export function createWorkflowResultDelivery(
127
170
  for (const envelope of envelopes) {
128
171
  const receipt = byId.get(envelope.deliveryId);
129
172
  if (receipt?.delivered) {
173
+ inbox.acknowledge([envelope.deliveryId]);
130
174
  try {
131
175
  persistState(envelope.details, "delivered", {
132
176
  attempts: (envelope.details.delivery?.attempts ?? 0) + 1,
@@ -150,6 +194,7 @@ export function createWorkflowResultDelivery(
150
194
  }
151
195
  continue;
152
196
  }
197
+ inbox.acknowledge([envelope.deliveryId]);
153
198
  retainPending(
154
199
  envelope,
155
200
  {
@@ -183,7 +228,7 @@ export function createWorkflowResultDelivery(
183
228
 
184
229
  /** Terminal won the wait/abort arbitration and will be returned inline. */
185
230
  consumeInline(details: WorkflowDetails) {
186
- if (details.delivery) pending.delete(details.delivery.id);
231
+ if (details.delivery) inbox.consumeDeliveryIds([details.delivery.id]);
187
232
  persistState(details, "consumed-inline", {
188
233
  deliveredAt: Date.now(),
189
234
  lastError: undefined,
@@ -214,18 +259,46 @@ export function createWorkflowResultDelivery(
214
259
  restore(envelope: WorkflowCompletionEnvelope) {
215
260
  const state = envelope.details.delivery?.state;
216
261
  if (state !== "pending" && state !== "held-for-inline") return false;
217
- // A process restart cannot still own the inline waiter. Deterministically
218
- // reconstruct pending delivery from the terminal artifact.
219
- if (state === "held-for-inline") {
220
- retainPending(
262
+ const owner = currentOwner();
263
+ const deliverySessionId = envelope.details.delivery?.ownerSessionId;
264
+ const detailsSessionId = envelope.details.sessionId;
265
+ const hasConflict =
266
+ deliverySessionId !== undefined &&
267
+ detailsSessionId !== undefined &&
268
+ deliverySessionId !== detailsSessionId;
269
+ const storedSessionId = hasConflict
270
+ ? undefined
271
+ : (deliverySessionId ?? detailsSessionId);
272
+
273
+ // Restoring canonical producer state is the explicit owner-revival
274
+ // boundary. Rebind only the same transcript to this process-local
275
+ // SessionManager generation; a different Session still dead-letters.
276
+ if (
277
+ owner &&
278
+ storedSessionId !== undefined &&
279
+ storedSessionId === owner.sessionId
280
+ ) {
281
+ envelope.details.delivery = {
282
+ ...envelope.details.delivery!,
283
+ ownerSessionId: owner.sessionId,
284
+ ownerEpoch: owner.epoch,
285
+ };
286
+ return retainPending(
221
287
  envelope,
222
- { lastError: "Inline waiter was not active after session restart" },
288
+ {
289
+ ownerSessionId: owner.sessionId,
290
+ ownerEpoch: owner.epoch,
291
+ lastError:
292
+ state === "held-for-inline"
293
+ ? "Inline waiter was not active after session restart"
294
+ : undefined,
295
+ },
223
296
  "Restored delivery state persistence failed",
224
297
  );
225
- } else {
226
- enqueue(envelope);
227
298
  }
228
- return true;
299
+ // Keep the canonical terminal artifact pending, but record that this
300
+ // process is not its transcript owner instead of redirecting it.
301
+ return enqueue(envelope);
229
302
  },
230
303
 
231
304
  retryPending() {
@@ -242,12 +315,13 @@ export function createWorkflowResultDelivery(
242
315
  },
243
316
 
244
317
  size() {
245
- return pending.size;
318
+ return inbox.size();
246
319
  },
247
320
 
248
321
  clear() {
249
- pending.clear();
322
+ inbox.clear();
250
323
  },
324
+ inspectDeadLetters: inbox.inspectDeadLetters,
251
325
  };
252
326
  return queue satisfies DurableResultDeliveryQueue<WorkflowCompletionEnvelope>;
253
327
  }
@@ -237,6 +237,12 @@ function makeProjection(
237
237
  ? {
238
238
  delivery: {
239
239
  id: details.delivery.id,
240
+ ...(details.delivery.ownerSessionId
241
+ ? { ownerSessionId: details.delivery.ownerSessionId }
242
+ : {}),
243
+ ...(details.delivery.ownerEpoch !== undefined
244
+ ? { ownerEpoch: details.delivery.ownerEpoch }
245
+ : {}),
240
246
  state: details.delivery.state,
241
247
  attempts: details.delivery.attempts,
242
248
  updatedAt: details.delivery.updatedAt,
@@ -16,14 +16,12 @@ import {
16
16
  type AgentSessionEventListener,
17
17
  createAgentSession,
18
18
  DefaultResourceLoader,
19
- defineTool,
20
19
  type ExtensionAPI,
21
20
  type ExtensionContext,
22
21
  SessionManager,
23
22
  SettingsManager,
24
23
  type ToolDefinition,
25
24
  } from "@earendil-works/pi-coding-agent";
26
- import { type TSchema, Type } from "typebox";
27
25
  import { AgentToolRenderLedger } from "../shared/agent-tool-renderer.ts";
28
26
  import {
29
27
  bindChildSessionExtensions,
@@ -34,10 +32,11 @@ import {
34
32
  import { createToolCallTimeoutGuard } from "../shared/tool-call-timeout.ts";
35
33
  import { type AgentUsage, emptyUsage, type TranscriptEntry } from "./model.ts";
36
34
  import {
37
- buildWorkflowAgentPrompt,
35
+ childToolsWithStructuredOutput,
36
+ createStructuredOutputTool,
38
37
  STRUCTURED_OUTPUT_SYSTEM_INSTRUCTION,
39
- STRUCTURED_OUTPUT_TOOL_DESCRIPTION,
40
- } from "./prompt.ts";
38
+ } from "../shared/structured-output.ts";
39
+ import { buildWorkflowAgentPrompt } from "./prompt.ts";
41
40
  import {
42
41
  AgentProgressProjection,
43
42
  type ProgressAssistantMessage,
@@ -142,9 +141,7 @@ export function workflowChildTools(
142
141
  tools: readonly string[] | undefined,
143
142
  structured: boolean,
144
143
  ) {
145
- return tools
146
- ? [...new Set([...tools, ...(structured ? ["structured_output"] : [])])]
147
- : undefined;
144
+ return childToolsWithStructuredOutput(tools, structured);
148
145
  }
149
146
 
150
147
  interface WorkflowToolSession {
@@ -179,68 +176,6 @@ export function guardWorkflowChildTools(
179
176
  });
180
177
  }
181
178
 
182
- function isJsonSchema(value: unknown): value is TSchema {
183
- if (!value || typeof value !== "object" || Array.isArray(value)) return false;
184
- const seen = new WeakSet<object>();
185
- let nodes = 0;
186
- const validate = (current: unknown, depth: number): boolean => {
187
- if (++nodes > 10_000 || depth > 24) return false;
188
- if (
189
- current === null ||
190
- typeof current === "string" ||
191
- typeof current === "boolean"
192
- ) {
193
- return true;
194
- }
195
- if (typeof current === "number") return Number.isFinite(current);
196
- if (Array.isArray(current)) {
197
- return current.every((item) => validate(item, depth + 1));
198
- }
199
- if (typeof current !== "object") return false;
200
- if (seen.has(current)) return false;
201
- seen.add(current);
202
- return Object.keys(current).every((key) => {
203
- if (key === "__proto__" || key === "constructor" || key === "prototype") {
204
- return false;
205
- }
206
- return validate((current as Record<string, unknown>)[key], depth + 1);
207
- });
208
- };
209
- return validate(value, 0);
210
- }
211
-
212
- /** Preserve the caller's full JSON Schema instead of lossy keyword conversion. */
213
- function jsonSchemaToTypebox(schema: unknown): TSchema {
214
- if (!isJsonSchema(schema)) {
215
- throw new Error("structured output schema must be a bounded JSON object");
216
- }
217
- return Type.Unsafe(schema);
218
- }
219
-
220
- /**
221
- * One-shot terminating tool injected when a schema is supplied: the subagent
222
- * calls it as its final action and we capture the validated object.
223
- */
224
- function makeStructuredOutputTool(
225
- schema: unknown,
226
- capture: (value: unknown) => void,
227
- ): ToolDefinition {
228
- return defineTool({
229
- name: "structured_output",
230
- label: "Structured Output",
231
- description: STRUCTURED_OUTPUT_TOOL_DESCRIPTION,
232
- parameters: jsonSchemaToTypebox(schema),
233
- async execute(_toolCallId, params) {
234
- capture(params);
235
- return {
236
- content: [{ type: "text", text: "Recorded structured result." }],
237
- details: params,
238
- terminate: true,
239
- };
240
- },
241
- });
242
- }
243
-
244
179
  type AssistantMessage = ProgressAssistantMessage;
245
180
 
246
181
  export { transcriptFromMessages };
@@ -475,7 +410,7 @@ export async function runAgent(
475
410
  customTools =
476
411
  options.schema !== undefined
477
412
  ? [
478
- makeStructuredOutputTool(options.schema, (value) => {
413
+ createStructuredOutputTool(options.schema, (value) => {
479
414
  if (!settled) structured = value;
480
415
  }),
481
416
  ]
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tt-a1i/openpi",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "description": "OpenPI — a Pi-native multi-agent workbench with background execution, isolated subagents, replay-safe workflows, goals, tasks, and observable TUI",
5
5
  "license": "MIT",
6
6
  "author": "tt-a1i",
@@ -63,9 +63,9 @@
63
63
  },
64
64
  "devDependencies": {
65
65
  "@biomejs/biome": "2.5.8",
66
- "@earendil-works/pi-ai": "^0.84.1",
67
- "@earendil-works/pi-coding-agent": "^0.84.1",
68
- "@earendil-works/pi-tui": "^0.84.1",
66
+ "@earendil-works/pi-ai": "^0.85.1",
67
+ "@earendil-works/pi-coding-agent": "^0.85.1",
68
+ "@earendil-works/pi-tui": "^0.85.1",
69
69
  "@effect/tsgo": "^0.24.2",
70
70
  "@effect/vitest": "^4.0.0-beta.99",
71
71
  "@types/node": "^26.1.1",
@@ -75,9 +75,9 @@
75
75
  "vitest": "4.1.10"
76
76
  },
77
77
  "peerDependencies": {
78
- "@earendil-works/pi-ai": "*",
79
- "@earendil-works/pi-coding-agent": "*",
80
- "@earendil-works/pi-tui": "*",
78
+ "@earendil-works/pi-ai": ">=0.85.1",
79
+ "@earendil-works/pi-coding-agent": ">=0.85.1",
80
+ "@earendil-works/pi-tui": ">=0.85.1",
81
81
  "typebox": "*"
82
82
  },
83
83
  "type": "module",
@@ -153,8 +153,9 @@ So `tools: [read, grep, find, ls]` yields a child that genuinely has no
153
153
  `write`, `edit`, or `bash` tool to call — not one that has been asked not to.
154
154
  Parent-only names are removed before the generated roster and spawn result are
155
155
  shown, so a type that lists `subagent_spawn` never advertises it as usable.
156
- A structured Workflow child additionally receives only its terminating
157
- `structured_output` tool; this does not restore any denied repository tool.
156
+ A Workflow child with a schema, or a Direct Subagent spawned with
157
+ `output_schema`, additionally receives only its terminating `structured_output`
158
+ tool; this does not restore any denied repository tool.
158
159
 
159
160
  While `/plan` is armed, `isolation: "worktree"` is rejected before Git is
160
161
  changed. A selected type whose declared tools plan mode would narrow (such as
@@ -13,6 +13,7 @@ The tool definitions are canonical for parameters, limits, model syntax, isolati
13
13
  - Prefer a matching agent type when one exists; its tool restriction is enforced. Model precedence is explicit spawn override, selected type-file model, configured built-in role model, then parent model. Reasoning precedence is explicit spawn override, selected type default, then parent effort. Types live in `~/.pi/agent/agents/*.md` and, for trusted projects, `.pi/agents/*.md`; see [Agent types](REFERENCE.md).
14
14
  - Isolate concurrent writers in worktrees according to the `subagent_spawn` schema so they cannot overwrite one checkout or git index. While Plan Mode is active, use only read-only exploration types (or no type); worktree isolation and types narrowed by Plan Mode are rejected.
15
15
  - After spawning, continue useful parent work. In an interactive session, if none remains, tell the user the child is still running and end the turn; automatic result delivery will re-invoke the parent when it settles. Do not block merely because the next step depends on the result or because there is nothing else to do. Use `subagent_wait` only when the user explicitly asks to keep the current response open for the result, or when non-interactive automation must return it in the same invocation.
16
+ - Use optional `output_schema` when downstream work needs a machine-validated result rather than prose. The child then receives one terminating `structured_output` tool, and the run fails if it finishes without submitting a matching value. Keep schemas small and task-specific; the validated JSON is delivered to the parent and preserved in a private content-addressed artifact. Omit the option for ordinary text reports.
16
17
 
17
18
  ## Worktree isolation
18
19
 
@@ -12,13 +12,13 @@ The `workflow` script is an async JavaScript function body executed in a restric
12
12
 
13
13
  ## Agent calls
14
14
 
15
- `await agent(prompt, options)` runs one child and always resolves to `{ ok, output, structured?, ref?, acceptance?, error? }`. Check `ok` before reading output. Children receive normal trust-aware resources but cannot recursively orchestrate or ask the user.
15
+ `await agent(prompt, options)` runs one child and always resolves to `{ ok, output, structured?, ref?, acceptance?, acceptanceWarning?, error? }`. Check `ok` before reading output. Children receive normal trust-aware resources but cannot recursively orchestrate or ask the user.
16
16
 
17
- Useful options include `agent_type`, `label`, `phase`, `schema`, `acceptance`, `model`, `provider`, `effort`, `isolation`, `operator`, and `inputs`.
17
+ Useful options include `agent_type`, `label`, `phase`, `schema`, `model`, `provider`, `effort`, `isolation`, `operator`, and `inputs`. The legacy `acceptance` option remains readable only during the 0.x migration window described below.
18
18
 
19
19
  - Prefer a matching `agent_type`. Model precedence is explicit model/provider, type file, configured built-in role, then parent. Effort precedence is explicit effort, type default, then parent.
20
20
  - `schema` validates structured output. Use it whenever later workflow logic branches on fields.
21
- - `acceptance: { criteria: [{ id, description, requiredEvidence?: string[] }] }` requires the same child to return an evidence ledger. An invocation accepts 1–32 criteria; each criterion has a 1–500 character human-readable `description` and an optional `requiredEvidence` array of at most 16 concise string labels (up to 120 characters each). The child must return exact matching labels in `acceptance.criteria[].evidence`. A criterion without `requiredEvidence` is an attestation of its description, not an evidence-backed gate; criteria that need evidence-backed acceptance must declare the required labels. Missing, malformed, or rejected criteria make `ok:false` while preserving output and evidence.
21
+ - `acceptance` is deprecated since OpenPI 0.5 and scheduled for removal in 1.0. Compatibility calls still return the child-authored ledger with `authority: "model-self-attestation"` and a migration warning, but it never determines `ok`. Use ordinary `schema` for findings, then let the parent evaluate them alongside runtime-observed exit codes, test receipts, file fingerprints, and tool results. Old DSL, journals, and artifacts remain readable during 0.x.
22
22
  - `operator: "name"` reuses one in-memory child Session for serialized follow-ups inside the same run. Its model, role/tools, effort, structured mode, and cwd are frozen by the first activation. Operators cannot use per-call worktrees or replay, and do not survive restarts.
23
23
  - `inputs: [ref, ...]` accepts successful opaque refs from the same workflow run only. Each conclusion is bounded to 16 KiB and total injected input to 48 KiB. The total budget is fairly distributed, so a large fan-out cannot starve later results merely because of order; partial projections are labeled. Full successful child results remain in the run's `agent-results/` artifacts. Inputs are marked as untrusted data; the resulting graph is observability, not scheduling authority.
24
24
  - Fair projection preserves the head and tail of every partial result and names its run-relative `agent-results/agent-N.json` audit artifact. That path is provenance for the parent/operator, not a child-readable handle. Fair presence is not proof of full evidence coverage: for large fan-out, group source refs into local Report agents, then pass only their refs to a global Report. The workflow script—not Runtime—must state planned, selected, covered, failed, and deferred counts.
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: workflows
3
- description: Orchestrates multi-agent work with OpenPI's inline JavaScript Workflow DSL. Use when a task needs multi-phase fan-out, pipelines, barriers, structured handoffs, acceptance evidence, or resumable background orchestration.
3
+ description: Orchestrates multi-agent work with OpenPI's inline JavaScript Workflow DSL. Use when a task needs multi-phase fan-out, pipelines, barriers, structured handoffs, or resumable background orchestration.
4
4
  ---
5
5
 
6
6
  # Workflows
@@ -490,6 +490,9 @@ export class PiWebAdapter {
490
490
  status: this.runtime.isIdle()
491
491
  ? ("idle" as const)
492
492
  : ("running" as const),
493
+ ...(this.runtime.getActiveTurn()
494
+ ? { activeTurn: this.runtime.getActiveTurn() }
495
+ : {}),
493
496
  capabilities: webCapabilitySnapshot(this.runtime.sessionManager),
494
497
  },
495
498
  truncation: {
@@ -0,0 +1,162 @@
1
+ import { existsSync, readFileSync, realpathSync, statSync } from "node:fs";
2
+ import { basename, dirname, join } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+
5
+ export const PI_CODING_AGENT_PACKAGE = "@earendil-works/pi-coding-agent";
6
+ export const PI_CODING_AGENT_ENTRY_ENV = "OPENPI_PI_CODING_AGENT_ENTRY";
7
+ const PACKAGE_ROOT_SEARCH_DEPTH = 10;
8
+
9
+ type PackageManifest = {
10
+ name?: unknown;
11
+ main?: unknown;
12
+ exports?: Record<string, { import?: unknown } | string>;
13
+ };
14
+
15
+ export function findPackageRoot(realPath: string, packageName: string) {
16
+ let dir = dirname(realPath);
17
+ for (let depth = 0; depth < PACKAGE_ROOT_SEARCH_DEPTH; depth++) {
18
+ const manifestPath = join(dir, "package.json");
19
+ if (existsSync(manifestPath)) {
20
+ const manifest = readManifest(manifestPath);
21
+ if (manifest?.name === packageName) return dir;
22
+ }
23
+ const parent = dirname(dir);
24
+ if (parent === dir) break;
25
+ dir = parent;
26
+ }
27
+ return undefined;
28
+ }
29
+
30
+ function readManifest(manifestPath: string) {
31
+ try {
32
+ return JSON.parse(readFileSync(manifestPath, "utf8")) as PackageManifest;
33
+ } catch {
34
+ return undefined;
35
+ }
36
+ }
37
+
38
+ function officialEntry(root: string | undefined) {
39
+ if (!root) return undefined;
40
+ const manifest = readManifest(join(root, "package.json"));
41
+ const target = manifest?.exports?.["."];
42
+ const relative =
43
+ typeof target === "string"
44
+ ? target
45
+ : typeof target?.import === "string"
46
+ ? target.import
47
+ : typeof manifest?.main === "string"
48
+ ? manifest.main
49
+ : "dist/index.js";
50
+ const entry = join(root, relative);
51
+ try {
52
+ return existsSync(entry) && statSync(entry).isFile()
53
+ ? realpathSync(entry)
54
+ : undefined;
55
+ } catch {
56
+ return undefined;
57
+ }
58
+ }
59
+
60
+ function walkFromFile(file: string) {
61
+ try {
62
+ const real = realpathSync(file);
63
+ if (!statSync(real).isFile()) return undefined;
64
+ return officialEntry(findPackageRoot(real, PI_CODING_AGENT_PACKAGE));
65
+ } catch {
66
+ return undefined;
67
+ }
68
+ }
69
+
70
+ function fileFromUrl(fromUrl: string) {
71
+ return fromUrl.startsWith("file:") ? fileURLToPath(fromUrl) : fromUrl;
72
+ }
73
+
74
+ function nearestPackageRoot(file: string) {
75
+ let dir = dirname(file);
76
+ for (let depth = 0; depth < PACKAGE_ROOT_SEARCH_DEPTH; depth++) {
77
+ if (existsSync(join(dir, "package.json"))) return dir;
78
+ const parent = dirname(dir);
79
+ if (parent === dir) break;
80
+ dir = parent;
81
+ }
82
+ }
83
+
84
+ function peerAt(nodeModules: string) {
85
+ const root = join(nodeModules, ...PI_CODING_AGENT_PACKAGE.split("/"));
86
+ const manifest = readManifest(join(root, "package.json"));
87
+ return manifest?.name === PI_CODING_AGENT_PACKAGE
88
+ ? officialEntry(root)
89
+ : undefined;
90
+ }
91
+
92
+ function resolveFromInstall(fromUrl: string) {
93
+ let start = fileFromUrl(fromUrl);
94
+ try {
95
+ start = realpathSync(start);
96
+ } catch {
97
+ // Keep the unresolved path when the caller file is a test stub.
98
+ }
99
+ const packageRoot = nearestPackageRoot(start);
100
+ if (!packageRoot) return undefined;
101
+
102
+ const nested = peerAt(join(packageRoot, "node_modules"));
103
+ if (nested) return nested;
104
+
105
+ const parent = dirname(packageRoot);
106
+ const grandparent = dirname(parent);
107
+ const hoistedModules =
108
+ basename(parent).startsWith("@") && basename(grandparent) === "node_modules"
109
+ ? grandparent
110
+ : basename(parent) === "node_modules"
111
+ ? parent
112
+ : undefined;
113
+ return hoistedModules ? peerAt(hoistedModules) : undefined;
114
+ }
115
+
116
+ export function validatePiCodingAgentEntry(candidate: string | undefined) {
117
+ if (!candidate) return undefined;
118
+ return walkFromFile(candidate);
119
+ }
120
+
121
+ export function missingPiCodingAgentDiagnostic() {
122
+ return [
123
+ `OpenPI Web could not resolve ${PI_CODING_AGENT_PACKAGE} for this process.`,
124
+ "Host resolution uses only the current process argv identity and fail-closes if that path is not the official package.",
125
+ `${PI_CODING_AGENT_ENTRY_ENV} is an explicit standalone handoff, not a host fallback.`,
126
+ `Standalone openpi web uses that handoff when valid, then the installed nested or hoisted peer (npm install ${PI_CODING_AGENT_PACKAGE}).`,
127
+ "From a running Pi session use /web, which hands over the host Pi.",
128
+ "Supported package install is `pi install npm:@tt-a1i/openpi`.",
129
+ ].join(" ");
130
+ }
131
+
132
+ export function resolvePiCodingAgentEntry(options?: {
133
+ source?: "host" | "standalone";
134
+ env?: NodeJS.ProcessEnv;
135
+ argv1?: string | undefined;
136
+ fromUrl?: string;
137
+ }) {
138
+ const source = options?.source ?? "host";
139
+ if (source === "standalone") {
140
+ const env = options?.env ?? process.env;
141
+ const handed = validatePiCodingAgentEntry(env[PI_CODING_AGENT_ENTRY_ENV]);
142
+ if (handed) return handed;
143
+ return resolveFromInstall(options?.fromUrl ?? import.meta.url);
144
+ }
145
+
146
+ const argv1 = options?.argv1 === undefined ? process.argv[1] : options.argv1;
147
+ return argv1 ? walkFromFile(argv1) : undefined;
148
+ }
149
+
150
+ export function resolveStandaloneJitiAliases(options?: {
151
+ env?: NodeJS.ProcessEnv;
152
+ argv1?: string | undefined;
153
+ fromUrl?: string;
154
+ }) {
155
+ const fromUrl = options?.fromUrl ?? import.meta.url;
156
+ const entry = resolvePiCodingAgentEntry({
157
+ ...options,
158
+ fromUrl,
159
+ source: "standalone",
160
+ });
161
+ return entry ? { [PI_CODING_AGENT_PACKAGE]: entry } : {};
162
+ }