@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
@@ -69,6 +69,8 @@ export interface SpawnTask {
69
69
  readonly tools?: readonly string[];
70
70
  /** Agent type that supplied the above, for the session label. */
71
71
  readonly agentTypeName?: string;
72
+ /** Optional JSON Schema for one terminating, validated child result. */
73
+ readonly outputSchema?: unknown;
72
74
  /**
73
75
  * Isolated git worktree this child runs in, created by the tool layer. The
74
76
  * backend only reclaims it when the session scope closes; it does not know
@@ -142,7 +144,11 @@ export interface QueuedMessage {
142
144
  // --- Events ------------------------------------------------------------------
143
145
 
144
146
  export type RunOutcome =
145
- | { readonly _tag: "Completed"; readonly finalText: string }
147
+ | {
148
+ readonly _tag: "Completed";
149
+ readonly finalText: string;
150
+ readonly structuredResult?: StructuredSubagentResult;
151
+ }
146
152
  | {
147
153
  readonly _tag: "Failed";
148
154
  readonly errorText: string;
@@ -232,10 +238,19 @@ export interface SubagentSnapshot {
232
238
  readonly queued: ReadonlyArray<QueuedMessage>;
233
239
  /** Final text of the most recent completed run (v1 `finalOutput`). */
234
240
  readonly finalText: string;
241
+ /** Present only when this run supplied and satisfied output_schema. */
242
+ readonly structuredResult?: StructuredSubagentResult;
235
243
  /** Count of finalized assistant messages (for subagent_check). */
236
244
  readonly turns: number;
237
245
  }
238
246
 
247
+ export interface StructuredSubagentResult {
248
+ readonly value: unknown;
249
+ readonly json: string;
250
+ readonly byteLength: number;
251
+ readonly artifactPath: string;
252
+ }
253
+
239
254
  /** Final text, or the live streaming buffer while a run is active (v1 `latestOutput`). */
240
255
  export function latestText(snap: SubagentSnapshot) {
241
256
  const live = snap.liveAssistant?.text.trim();
@@ -123,6 +123,7 @@ interface MutableSnapshot {
123
123
  liveTools: LiveToolState[];
124
124
  queued: SubagentSnapshot["queued"];
125
125
  finalText: string;
126
+ structuredResult?: SubagentSnapshot["structuredResult"];
126
127
  turns: number;
127
128
  }
128
129
 
@@ -363,6 +364,7 @@ const makeManager = (config: SubagentManagerConfig = {}) =>
363
364
  s.outcome = "completed";
364
365
  s.errorText = undefined;
365
366
  s.finalText = outcome.finalText.slice(0, FINAL_TEXT_MAX_LENGTH);
367
+ s.structuredResult = outcome.structuredResult;
366
368
  break;
367
369
  case "Failed":
368
370
  s.status = "error";
@@ -373,6 +375,7 @@ const makeManager = (config: SubagentManagerConfig = {}) =>
373
375
  0,
374
376
  FINAL_TEXT_MAX_LENGTH,
375
377
  );
378
+ s.structuredResult = undefined;
376
379
  break;
377
380
  case "Interrupted":
378
381
  s.status = "error";
@@ -382,6 +385,7 @@ const makeManager = (config: SubagentManagerConfig = {}) =>
382
385
  0,
383
386
  FINAL_TEXT_MAX_LENGTH,
384
387
  );
388
+ s.structuredResult = undefined;
385
389
  break;
386
390
  }
387
391
  s.liveAssistant = undefined;
@@ -446,6 +450,7 @@ const makeManager = (config: SubagentManagerConfig = {}) =>
446
450
  s.outcome = undefined;
447
451
  s.settledAt = undefined;
448
452
  s.errorText = undefined;
453
+ s.structuredResult = undefined;
449
454
  armWatchdog(entry);
450
455
  break;
451
456
  case "RunSettled":
@@ -17,8 +17,8 @@ export const SUBAGENT_SCHEMA_BUDGETS = Object.freeze({
17
17
 
18
18
  /** Describes subagent_spawn, including the fixed concurrency cap. */
19
19
  export const SUBAGENT_SPAWN_TOOL_DESCRIPTION =
20
- "Spawn a background in-process Pi subagent with its own context, child-safe tools, and normal host permissions. Returns immediately; its final result is delivered automatically. The child cannot see this conversation, ask the user, or orchestrate agents/workflows. Use only trusted working directories. " +
21
- `Max ${MAX_RUNNING} subagents can be running at once.`;
20
+ "Spawn a background Pi subagent with isolated context and child-safe tools. Returns immediately; its result arrives automatically. It cannot see this chat, ask the user, or orchestrate. Use trusted directories. " +
21
+ `Max ${MAX_RUNNING} subagents can run at once.`;
22
22
 
23
23
  /** UTF-8 bounded, whitespace-normalized text for the parent-facing roster. */
24
24
  function boundedPurpose(description: string) {
@@ -152,6 +152,7 @@ export const SUBAGENT_SPAWN_PARAMETER_DESCRIPTIONS = {
152
152
  'Optional "provider/model-id" or current-provider model override. Omit to use the preset, configured role, or parent default. Never guess a model name.',
153
153
  reasoningEffort:
154
154
  "Optional child thinking level. Honor the user's requested level. Otherwise choose a level supported by the resolved child model based on the selected role and task difficulty. An explicit value overrides a role default.",
155
+ outputSchema: "Optional result JSON Schema.",
155
156
  };
156
157
 
157
158
  /** The exact name/description/wire-schema source used by registration/tests. */
@@ -193,6 +194,15 @@ export function createSubagentSpawnToolSurface(
193
194
  description: SUBAGENT_SPAWN_PARAMETER_DESCRIPTIONS.reasoningEffort,
194
195
  }),
195
196
  ),
197
+ output_schema: Type.Optional(
198
+ Type.Object(
199
+ {},
200
+ {
201
+ additionalProperties: true,
202
+ description: SUBAGENT_SPAWN_PARAMETER_DESCRIPTIONS.outputSchema,
203
+ },
204
+ ),
205
+ ),
196
206
  }),
197
207
  };
198
208
  }
@@ -207,6 +217,7 @@ export function buildSubagentSpawnResult(options: {
207
217
  agentTypeName?: string;
208
218
  tools?: readonly string[];
209
219
  worktreeBranch?: string;
220
+ structured?: boolean;
210
221
  }) {
211
222
  const typeNote = options.agentTypeName
212
223
  ? ` Agent type "${options.agentTypeName}" applied.`
@@ -226,8 +237,11 @@ export function buildSubagentSpawnResult(options: {
226
237
  const worktreeNote = options.worktreeBranch
227
238
  ? ` Isolated in its own worktree on branch "${options.worktreeBranch}" — its edits are invisible here until you merge that branch. The checkout stays available for later send/review and is reclaimed on Session retirement only when bounded inspection proves it empty.`
228
239
  : "";
240
+ const structuredNote = options.structured
241
+ ? " This run must finish with the requested validated structured result."
242
+ : "";
229
243
  return (
230
- `Spawned subagent ${options.id} "${options.title}" (${options.harness}: ${options.modelLabel}, ${options.cwd}).${typeNote}${toolNote}${worktreeNote}\n` +
244
+ `Spawned subagent ${options.id} "${options.title}" (${options.harness}: ${options.modelLabel}, ${options.cwd}).${typeNote}${toolNote}${worktreeNote}${structuredNote}\n` +
231
245
  `It runs in the background — keep working on independent work. If none remains in an interactive session, briefly tell the user it is still running and end your turn; its result is delivered automatically and you are automatically re-invoked when it finishes. Do not poll or call subagent_wait merely because a later step depends on it. ` +
232
246
  `Use subagent_wait(ids: ["${options.id}"]) only if the user explicitly asked you to keep the current response open for this result, or a non-interactive automation must return it in the same invocation; subagent_cancel stops it, subagent_check peeks at a running one, subagent_list shows all.`
233
247
  );
@@ -77,6 +77,38 @@ export function persistResultArtifact(agentDir: string, content: string) {
77
77
  return artifactPath;
78
78
  }
79
79
 
80
+ /** Persist one complete validated structured value under a JSON identity. */
81
+ export function persistStructuredResultArtifact(
82
+ agentDir: string,
83
+ content: string,
84
+ ) {
85
+ let directory = path.resolve(agentDir);
86
+ for (const segment of RESULT_ARTIFACT_DIR) {
87
+ directory = ensureDirectory(directory, segment);
88
+ }
89
+
90
+ const digest = createHash("sha256").update(content).digest("hex");
91
+ const artifactPath = path.join(directory, `${digest}.json`);
92
+ try {
93
+ writeFileSync(artifactPath, content, {
94
+ encoding: "utf8",
95
+ flag: "wx",
96
+ mode: 0o600,
97
+ });
98
+ } catch (error) {
99
+ if ((error as NodeJS.ErrnoException).code !== "EEXIST") throw error;
100
+ const stat = lstatSync(artifactPath);
101
+ if (
102
+ !stat.isFile() ||
103
+ stat.isSymbolicLink() ||
104
+ readFileSync(artifactPath, "utf8") !== content
105
+ ) {
106
+ throw new Error(`Structured result artifact collision: ${artifactPath}`);
107
+ }
108
+ }
109
+ return artifactPath;
110
+ }
111
+
80
112
  /**
81
113
  * Build the single model-visible projection used by automatic delivery and
82
114
  * explicit waits. Short answers pass through byte-for-byte. Long answers keep
@@ -1,10 +1,16 @@
1
1
  import type { ConsumableResultDeliveryQueue } from "../../shared/result-delivery.ts";
2
+ import {
3
+ type CompletionOwner,
4
+ createCompletionInbox,
5
+ } from "../../shared/completion-inbox.ts";
2
6
 
3
7
  export interface SubagentResultDeliveryOptions<T> {
4
8
  /** True only when the parent has no run or queued continuation in flight. */
5
9
  readonly isIdle: () => boolean;
6
10
  /** Deliver one drained batch and wake the parent. */
7
11
  readonly deliver: (results: readonly T[]) => void;
12
+ /** Current Pi Session transcript owner. */
13
+ readonly owner?: () => CompletionOwner | undefined;
8
14
  }
9
15
 
10
16
  /**
@@ -22,50 +28,63 @@ export interface SubagentResultDeliveryOptions<T> {
22
28
  * The parent boundary wakes even if an earlier extension handler has already
23
29
  * started another turn: Pi queues the follow-up into that active run.
24
30
  *
25
- * The Map is the one-shot gate: `subagent_wait` may consume a result before it
26
- * is delivered, and whichever path drains first prevents duplicate delivery.
31
+ * The shared inbox is the one-shot gate: `subagent_wait` may consume a result
32
+ * before it is delivered, and whichever path claims first prevents duplicate
33
+ * delivery.
27
34
  */
28
35
  export function createSubagentResultDelivery<T extends { id: string }>(
29
36
  options: SubagentResultDeliveryOptions<T>,
30
37
  ) {
31
- const pending = new Map<string, T>();
38
+ const inbox = createCompletionInbox<T>();
39
+ const owner = options.owner ?? (() => ({ sessionId: "test", epoch: 0 }));
32
40
 
33
41
  const flush = () => {
34
- if (pending.size === 0) return;
35
- const results = [...pending.values()];
36
- pending.clear();
42
+ const envelopes = inbox.claim(owner());
43
+ if (envelopes.length === 0) return;
44
+ const results = envelopes.map((envelope) => envelope.payload);
37
45
  try {
38
46
  options.deliver(results);
47
+ inbox.acknowledge(envelopes.map((envelope) => envelope.deliveryId));
39
48
  } catch (error) {
40
49
  // A synchronous session teardown may reject append/send. Preserve the
41
50
  // original batch ahead of anything deferred re-entrantly while delivery
42
51
  // ran, so a later boundary can retry without loss or reordering.
43
- const current = [...pending.values()];
44
- pending.clear();
45
- for (const result of results) pending.set(result.id, result);
46
- for (const result of current) pending.set(result.id, result);
52
+ inbox.retry(envelopes, owner());
47
53
  throw error;
48
54
  }
49
55
  };
50
56
 
51
57
  const queue = {
52
58
  defer(result: T) {
53
- pending.set(result.id, result);
59
+ const currentOwner = owner();
60
+ inbox.defer(
61
+ {
62
+ deliveryId: `subagent:${result.id}`,
63
+ owner: currentOwner ?? { sessionId: "unowned", epoch: 0 },
64
+ producer: "subagent",
65
+ producerId: result.id,
66
+ terminalRef: { kind: "subagent-snapshot", id: result.id },
67
+ wake: "follow-up",
68
+ payload: result,
69
+ },
70
+ currentOwner,
71
+ );
54
72
  if (options.isIdle()) flush();
55
73
  },
56
74
  consume(ids: Iterable<string>) {
57
- for (const id of ids) pending.delete(id);
75
+ inbox.consume("subagent", ids);
58
76
  },
59
77
  /** Flush at the authoritative parent boundary. */
60
78
  parentSettled() {
61
79
  flush();
62
80
  },
63
81
  clear() {
64
- pending.clear();
82
+ inbox.clear();
65
83
  },
66
84
  size() {
67
- return pending.size;
85
+ return inbox.size();
68
86
  },
87
+ inspectDeadLetters: inbox.inspectDeadLetters,
69
88
  };
70
89
  return queue satisfies ConsumableResultDeliveryQueue<T>;
71
90
  }
@@ -1,5 +1,5 @@
1
1
  import { homedir } from "node:os";
2
- import { relative } from "node:path";
2
+ import { posix, win32 } from "node:path";
3
3
  import type { Theme } from "@earendil-works/pi-coding-agent";
4
4
  import {
5
5
  getCapabilities,
@@ -117,10 +117,21 @@ export function formatTokens(tokens: number) {
117
117
  return `${(tokens / 1_000_000).toFixed(1)}m`;
118
118
  }
119
119
 
120
- export function formatDirectory(cwd: string) {
121
- const home = homedir();
122
- if (cwd === home) return "~";
123
- const display = cwd.startsWith(`${home}/`) ? `~/${relative(home, cwd)}` : cwd;
120
+ export function formatDirectory(
121
+ cwd: string,
122
+ home = homedir(),
123
+ pathModule = process.platform === "win32" ? win32 : posix,
124
+ ) {
125
+ const relativePath = pathModule.relative(home, cwd);
126
+ const outsideHome =
127
+ relativePath === ".." ||
128
+ relativePath.startsWith(`..${pathModule.sep}`) ||
129
+ pathModule.isAbsolute(relativePath);
130
+ const display = outsideHome
131
+ ? cwd
132
+ : relativePath
133
+ ? `~/${relativePath.replaceAll(pathModule.sep, "/")}`
134
+ : "~";
124
135
  return sanitizeTerminalLabel(display);
125
136
  }
126
137
 
@@ -40,8 +40,35 @@ type Segment =
40
40
  | { kind: "prose"; lines: string[] }
41
41
  | { kind: "code"; open: string; content: string[]; close: string };
42
42
 
43
- const FENCE_OPEN = /^ {0,3}`{3,}/;
44
- const FENCE_CLOSE = /^ {0,3}`{3,}[ \t]*\r?$/;
43
+ const FENCE_OPEN = /^ {0,3}(`{3,}|~{3,})/;
44
+
45
+ /**
46
+ * Parse an opening code fence (CommonMark §4.5): which character it uses and
47
+ * how long it is. A backtick fence's info string may not contain backticks.
48
+ */
49
+ function openFence(line: string) {
50
+ const match = FENCE_OPEN.exec(line);
51
+ if (!match) return undefined;
52
+ const fence = match[1];
53
+ if (fence[0] === "`" && line.slice(match[0].length).includes("`")) {
54
+ return undefined;
55
+ }
56
+ return { char: fence[0], length: fence.length };
57
+ }
58
+
59
+ /**
60
+ * A closing fence must use the same character as the opening fence and be at
61
+ * least as long: a ``` line does not close a ```` block, and backticks never
62
+ * close a tilde block.
63
+ */
64
+ function isCloseFence(line: string, open: { char: string; length: number }) {
65
+ const match = /^ {0,3}(`{3,}|~{3,})[ \t]*\r?$/.exec(line);
66
+ return (
67
+ match !== null &&
68
+ match[1][0] === open.char &&
69
+ match[1].length >= open.length
70
+ );
71
+ }
45
72
 
46
73
  function countLines(markdown: string) {
47
74
  const parts = markdown.split("\n");
@@ -60,7 +87,8 @@ function parseSegments(lines: string[]): Segment[] {
60
87
  let prose: string[] = [];
61
88
  let i = 0;
62
89
  while (i < lines.length) {
63
- if (!FENCE_OPEN.test(lines[i])) {
90
+ const fence = openFence(lines[i]);
91
+ if (!fence) {
64
92
  prose.push(lines[i]);
65
93
  i += 1;
66
94
  continue;
@@ -74,13 +102,21 @@ function parseSegments(lines: string[]): Segment[] {
74
102
  let close: string | undefined;
75
103
  let j = i + 1;
76
104
  while (j < lines.length && close === undefined) {
77
- if (FENCE_CLOSE.test(lines[j])) close = lines[j];
105
+ if (isCloseFence(lines[j], fence)) close = lines[j];
78
106
  else content.push(lines[j]);
79
107
  j += 1;
80
108
  }
81
109
  if (close === undefined) {
82
- // Unterminated fence: fold the whole message conservatively as text.
83
- return [{ kind: "prose", lines }];
110
+ // Unterminated fence: the block runs to the end of the message. Keep it
111
+ // as a code block with a synthesized closing fence so a folded preview
112
+ // never leaks an unclosed fence into the TUI.
113
+ segments.push({
114
+ kind: "code",
115
+ open,
116
+ content,
117
+ close: fence.char.repeat(fence.length),
118
+ });
119
+ return segments;
84
120
  }
85
121
  segments.push({ kind: "code", open, content, close });
86
122
  i = j;
@@ -5,6 +5,11 @@ import type {
5
5
  ExtensionAPI,
6
6
  ExtensionCommandContext,
7
7
  } from "@earendil-works/pi-coding-agent";
8
+ import {
9
+ missingPiCodingAgentDiagnostic,
10
+ PI_CODING_AGENT_ENTRY_ENV,
11
+ resolvePiCodingAgentEntry,
12
+ } from "../../web/host/pi-coding-agent-entry.ts";
8
13
 
9
14
  const DEFAULT_SHUTDOWN_TIMEOUT_MS = 5_000;
10
15
 
@@ -26,12 +31,20 @@ interface SpawnWebOptions {
26
31
  stdio: "inherit";
27
32
  }
28
33
 
29
- function webProcessEnvironment(cwd: string) {
34
+ function webProcessEnvironment(
35
+ cwd: string,
36
+ piCodingAgentEntry: string | undefined,
37
+ ) {
30
38
  const environment: NodeJS.ProcessEnv = { ...process.env, PWD: cwd };
31
39
  delete environment.OLDPWD;
32
40
  delete environment.INIT_CWD;
33
41
  delete environment.PI_SESSION_ID;
34
42
  delete environment.PI_SESSION_FILE;
43
+ if (piCodingAgentEntry) {
44
+ environment[PI_CODING_AGENT_ENTRY_ENV] = piCodingAgentEntry;
45
+ } else {
46
+ delete environment[PI_CODING_AGENT_ENTRY_ENV];
47
+ }
35
48
  return environment;
36
49
  }
37
50
 
@@ -40,6 +53,7 @@ export interface WebCommandDependencies {
40
53
  spawn(command: string, args: string[], options: SpawnWebOptions): WebProcess;
41
54
  clearTerminal(): void;
42
55
  holdParentSigint(): () => void;
56
+ resolvePiCodingAgentEntry(): string | undefined;
43
57
  shutdownTimeoutMs: number;
44
58
  }
45
59
 
@@ -65,6 +79,8 @@ const defaultDependencies: WebCommandDependencies = {
65
79
  process.on("SIGINT", keepPiAlive);
66
80
  return () => process.removeListener("SIGINT", keepPiAlive);
67
81
  },
82
+ resolvePiCodingAgentEntry: () =>
83
+ resolvePiCodingAgentEntry({ source: "host" }),
68
84
  shutdownTimeoutMs: DEFAULT_SHUTDOWN_TIMEOUT_MS,
69
85
  };
70
86
 
@@ -95,6 +111,7 @@ function runWebInForeground(
95
111
  dependencies: WebCommandDependencies,
96
112
  setActive: (active: ActiveWebProcess | undefined) => void,
97
113
  isShuttingDown: () => boolean,
114
+ piCodingAgentEntry: string,
98
115
  ) {
99
116
  return ctx.ui.custom<WebExit>((tui, _theme, _keybindings, done) => {
100
117
  let finished = false;
@@ -128,7 +145,7 @@ function runWebInForeground(
128
145
  [dependencies.entrypoint, "web", "--no-workspace"],
129
146
  {
130
147
  cwd: childCwd,
131
- env: webProcessEnvironment(childCwd),
148
+ env: webProcessEnvironment(childCwd, piCodingAgentEntry),
132
149
  shell: false,
133
150
  stdio: "inherit",
134
151
  },
@@ -191,6 +208,11 @@ export default function web(
191
208
  ctx.ui.notify("OpenPI Web Workbench is already running.", "warning");
192
209
  return;
193
210
  }
211
+ const piCodingAgentEntry = dependencies.resolvePiCodingAgentEntry();
212
+ if (!piCodingAgentEntry) {
213
+ ctx.ui.notify(missingPiCodingAgentDiagnostic(), "error");
214
+ return;
215
+ }
194
216
 
195
217
  running = true;
196
218
  try {
@@ -201,6 +223,7 @@ export default function web(
201
223
  active = next;
202
224
  },
203
225
  () => shuttingDown,
226
+ piCodingAgentEntry,
204
227
  );
205
228
  if (shuttingDown) return;
206
229
  if (result.kind === "error") {
@@ -22,6 +22,25 @@ export interface AcceptanceLedger {
22
22
  readonly status: "accepted" | "rejected" | "missing" | "malformed";
23
23
  readonly criteria: readonly AcceptanceCriterionResult[];
24
24
  readonly errors: readonly string[];
25
+ /** Child-authored judgment retained only for migration; never a runtime fact. */
26
+ readonly authority?: "model-self-attestation";
27
+ readonly deprecated?: {
28
+ readonly since: "0.5";
29
+ readonly removal: "1.0";
30
+ };
31
+ }
32
+
33
+ export const ACCEPTANCE_DEPRECATION_WARNING =
34
+ "acceptance is deprecated since OpenPI 0.5 and will be removed in 1.0; it is model self-attestation, not runtime-verified evidence, and does not determine ok";
35
+
36
+ function ledger(
37
+ value: Omit<AcceptanceLedger, "authority" | "deprecated">,
38
+ ): AcceptanceLedger {
39
+ return {
40
+ ...value,
41
+ authority: "model-self-attestation",
42
+ deprecated: { since: "0.5", removal: "1.0" },
43
+ };
25
44
  }
26
45
 
27
46
  const ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
@@ -169,6 +188,12 @@ export function isAcceptanceLedger(value: unknown): value is AcceptanceLedger {
169
188
  value.status === "rejected" ||
170
189
  value.status === "missing" ||
171
190
  value.status === "malformed") &&
191
+ (value.authority === undefined ||
192
+ value.authority === "model-self-attestation") &&
193
+ (value.deprecated === undefined ||
194
+ (record(value.deprecated) &&
195
+ value.deprecated.since === "0.5" &&
196
+ value.deprecated.removal === "1.0")) &&
172
197
  value.errors.every((error) => typeof error === "string") &&
173
198
  value.criteria.every(
174
199
  (criterion) =>
@@ -187,7 +212,7 @@ export function acceptanceInstruction(contract: AcceptanceContract) {
187
212
  `- ${criterion.id}: ${criterion.description}${criterion.requiredEvidence?.length ? `; required evidence labels: ${criterion.requiredEvidence.join(", ")}` : ""}`,
188
213
  );
189
214
  return [
190
- "Acceptance is explicit evidence, not a self-awarded success claim.",
215
+ "Deprecated compatibility protocol: this acceptance ledger is your own model self-attestation, not runtime-verified evidence, and it does not determine execution success.",
191
216
  "Include an `acceptance.criteria` array in structured_output with exactly these ids. Mark rejected when the criterion is not demonstrated. Evidence entries must be concise labels or concrete references; do not invent evidence.",
192
217
  ...criteria,
193
218
  ].join("\n");
@@ -198,19 +223,19 @@ export function evaluateAcceptance(
198
223
  structured: unknown,
199
224
  ): AcceptanceLedger {
200
225
  if (!record(structured) || !record(structured.acceptance)) {
201
- return {
226
+ return ledger({
202
227
  status: "missing",
203
228
  criteria: [],
204
229
  errors: ["structured result omitted acceptance"],
205
- };
230
+ });
206
231
  }
207
232
  const rawCriteria = structured.acceptance.criteria;
208
233
  if (!Array.isArray(rawCriteria)) {
209
- return {
234
+ return ledger({
210
235
  status: "malformed",
211
236
  criteria: [],
212
237
  errors: ["acceptance.criteria is not an array"],
213
- };
238
+ });
214
239
  }
215
240
  const errors: string[] = [];
216
241
  const byId = new Map<string, AcceptanceCriterionResult>();
@@ -265,14 +290,15 @@ export function evaluateAcceptance(
265
290
  errors.push(`unexpected acceptance criterion "${id}"`);
266
291
  }
267
292
  }
268
- if (errors.length) return { status: "malformed", criteria: results, errors };
269
- return {
293
+ if (errors.length)
294
+ return ledger({ status: "malformed", criteria: results, errors });
295
+ return ledger({
270
296
  status: results.every((result) => result.status === "accepted")
271
297
  ? "accepted"
272
298
  : "rejected",
273
299
  criteria: results,
274
300
  errors: [],
275
- };
301
+ });
276
302
  }
277
303
 
278
304
  export function applyAcceptance(options: {
@@ -284,15 +310,13 @@ export function applyAcceptance(options: {
284
310
  const ledger = options.contract
285
311
  ? evaluateAcceptance(options.contract, options.structured)
286
312
  : undefined;
287
- const acceptanceError =
288
- ledger && ledger.status !== "accepted"
289
- ? `Acceptance ${ledger.status}${ledger.errors.length ? `: ${ledger.errors.join("; ")}` : ": one or more criteria were rejected"}`
290
- : undefined;
291
- const ok = options.agentOk && !acceptanceError;
292
- const error = ok
293
- ? undefined
294
- : options.agentError
295
- ? `${options.agentError}${acceptanceError ? `; ${acceptanceError}` : ""}`
296
- : (acceptanceError ?? "Agent failed");
297
- return { ok, ...(ledger ? { ledger } : {}), ...(error ? { error } : {}) };
313
+ const ok = options.agentOk;
314
+ const error = ok ? undefined : (options.agentError ?? "Agent failed");
315
+ return {
316
+ ok,
317
+ ...(ledger
318
+ ? { ledger, acceptanceWarning: ACCEPTANCE_DEPRECATION_WARNING }
319
+ : {}),
320
+ ...(error ? { error } : {}),
321
+ };
298
322
  }
@@ -230,7 +230,9 @@ function buildOperatorReport(
230
230
  : "running";
231
231
  lines.push(
232
232
  `- [${agent.label}]${agent.phase ? ` (${agent.phase})` : ""} ${state}` +
233
- (agent.acceptance ? ` · acceptance ${agent.acceptance.status}` : "") +
233
+ (agent.acceptance
234
+ ? ` · deprecated model self-attestation ${agent.acceptance.status}`
235
+ : "") +
234
236
  (agent.error ? ` — ${agent.error}` : ""),
235
237
  );
236
238
  }
@@ -220,6 +220,14 @@ function normalizeDelivery(value: unknown): WorkflowDetails["delivery"] {
220
220
  : 0;
221
221
  return {
222
222
  id: sanitizeLine(record.id, 256),
223
+ ...(typeof record.ownerSessionId === "string" && record.ownerSessionId
224
+ ? { ownerSessionId: sanitizeLine(record.ownerSessionId, 256) }
225
+ : {}),
226
+ ...(typeof record.ownerEpoch === "number" &&
227
+ Number.isSafeInteger(record.ownerEpoch) &&
228
+ record.ownerEpoch >= 0
229
+ ? { ownerEpoch: record.ownerEpoch }
230
+ : {}),
223
231
  state,
224
232
  attempts,
225
233
  updatedAt,
@@ -55,6 +55,7 @@ import {
55
55
  import { fitNavigationSides } from "../shared/below-editor-navigation.ts";
56
56
  import { waitBounded } from "../shared/child-session.ts";
57
57
  import { contextPercent } from "../shared/context-utilization.ts";
58
+ import { completionOwnerFor } from "../shared/completion-inbox.ts";
58
59
  import {
59
60
  registerEditorLayer,
60
61
  removeEditorLayer,
@@ -537,6 +538,8 @@ interface ScriptAgentResult {
537
538
  /** Opaque same-run handle for bounded downstream handoff. */
538
539
  ref?: string;
539
540
  acceptance?: AgentRecord["acceptance"];
541
+ /** Present only for the deprecated model self-attestation compatibility path. */
542
+ acceptanceWarning?: string;
540
543
  error?: string;
541
544
  }
542
545
 
@@ -857,6 +860,8 @@ export default function workflows(
857
860
  };
858
861
  const resultDelivery = createWorkflowResultDelivery({
859
862
  isIdle: () => lastContext?.isIdle() ?? false,
863
+ owner: () =>
864
+ lastContext ? completionOwnerFor(lastContext.sessionManager) : undefined,
860
865
  persist: (details) => {
861
866
  if (!details.delivery)
862
867
  throw new Error("Workflow delivery identity is missing");
@@ -1276,6 +1281,8 @@ export default function workflows(
1276
1281
  agents: [],
1277
1282
  delivery: {
1278
1283
  id: `workflow:${runId}:terminal`,
1284
+ ownerSessionId: completionOwnerFor(ctx.sessionManager).sessionId,
1285
+ ownerEpoch: completionOwnerFor(ctx.sessionManager).epoch,
1279
1286
  state: launchMode === "inline" ? "held-for-inline" : "none",
1280
1287
  attempts: 0,
1281
1288
  updatedAt: now,
@@ -1880,6 +1887,9 @@ export default function workflows(
1880
1887
  : {}),
1881
1888
  ...(ref ? { ref } : {}),
1882
1889
  ...(record.acceptance ? { acceptance: record.acceptance } : {}),
1890
+ ...(judged.acceptanceWarning
1891
+ ? { acceptanceWarning: judged.acceptanceWarning }
1892
+ : {}),
1883
1893
  };
1884
1894
  }
1885
1895
 
@@ -2131,6 +2141,9 @@ export default function workflows(
2131
2141
  : {}),
2132
2142
  ...(ref ? { ref } : {}),
2133
2143
  ...(acceptance ? { acceptance } : {}),
2144
+ ...(judged.acceptanceWarning
2145
+ ? { acceptanceWarning: judged.acceptanceWarning }
2146
+ : {}),
2134
2147
  ...(record.error !== undefined ? { error: record.error } : {}),
2135
2148
  };
2136
2149
  } finally {