@tt-a1i/openpi 0.3.1 → 0.5.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 (129) hide show
  1. package/README.md +184 -59
  2. package/SETUP.md +23 -7
  3. package/assets/openpi-launch-card-v1.webp +0 -0
  4. package/bin/openpi.js +145 -0
  5. package/extensions/ask-user/index.ts +30 -14
  6. package/extensions/background-terminals/index.ts +30 -2
  7. package/extensions/background-terminals/src/domain.ts +2 -0
  8. package/extensions/background-terminals/src/manager.ts +486 -106
  9. package/extensions/background-terminals/src/output.ts +33 -0
  10. package/extensions/background-terminals/src/prompt.ts +14 -6
  11. package/extensions/background-terminals/src/result-delivery.ts +4 -1
  12. package/extensions/background-terminals/src/ui/ps.ts +132 -129
  13. package/extensions/capabilities/index.ts +30 -42
  14. package/extensions/capabilities/src/ui.ts +93 -0
  15. package/extensions/clear-context/index.ts +83 -0
  16. package/extensions/context-pivot/index.ts +16 -6
  17. package/extensions/cron/schedule.ts +7 -1
  18. package/extensions/file-mutation-display/index.ts +34 -76
  19. package/extensions/file-mutation-display/render.ts +146 -87
  20. package/extensions/file-search/index.ts +8 -7
  21. package/extensions/file-search/src/binaries.ts +75 -59
  22. package/extensions/git-info/src/changed-files-view.ts +47 -14
  23. package/extensions/git-read/index.ts +328 -0
  24. package/extensions/git-read/src/args.ts +171 -0
  25. package/extensions/git-read/src/process.ts +81 -0
  26. package/extensions/git-read/src/prompt.ts +56 -0
  27. package/extensions/model-info/index.ts +21 -33
  28. package/extensions/model-info/session-metrics.ts +96 -0
  29. package/extensions/plan-mode/bash-policy.ts +54 -9
  30. package/extensions/plan-mode/index.ts +7 -2
  31. package/extensions/post-edit/index.ts +16 -6
  32. package/extensions/sessions/git-stats.ts +258 -72
  33. package/extensions/sessions/index.ts +222 -140
  34. package/extensions/sessions/preview-cache.ts +104 -0
  35. package/extensions/sessions/preview-loader.ts +856 -0
  36. package/extensions/sessions/sessions.ts +43 -4
  37. package/extensions/setup/index.ts +127 -131
  38. package/extensions/shared/activity-status.ts +36 -5
  39. package/extensions/shared/agent-session-page.ts +319 -0
  40. package/extensions/shared/agent-tool-renderer.ts +218 -0
  41. package/extensions/shared/agent-transcript.ts +524 -0
  42. package/extensions/shared/below-editor-navigation.ts +26 -0
  43. package/extensions/shared/capability-intent.ts +53 -0
  44. package/extensions/shared/child-session.ts +444 -22
  45. package/extensions/shared/result-budget.ts +134 -0
  46. package/extensions/shared/result-delivery.ts +34 -0
  47. package/extensions/shared/screen-chrome.ts +133 -0
  48. package/extensions/shared/setup-config.ts +97 -38
  49. package/extensions/shared/setup-episode-state.ts +1 -1
  50. package/extensions/shared/spinner.ts +28 -0
  51. package/extensions/shared/terminal-text.ts +110 -23
  52. package/extensions/shared/text-projection.ts +113 -0
  53. package/extensions/shared/tool-activity.ts +382 -0
  54. package/extensions/shared/tool-surface.ts +42 -8
  55. package/extensions/shared/transcript-viewport.ts +46 -0
  56. package/extensions/shared/web-observer-registry.ts +390 -0
  57. package/extensions/shared/worktree.ts +11 -0
  58. package/extensions/subagents/index.ts +461 -186
  59. package/extensions/subagents/navigation.ts +86 -28
  60. package/extensions/subagents/src/agent-types.ts +37 -15
  61. package/extensions/subagents/src/backend.ts +12 -1
  62. package/extensions/subagents/src/backends/pi.ts +375 -66
  63. package/extensions/subagents/src/domain.ts +5 -0
  64. package/extensions/subagents/src/id-sequence.ts +84 -0
  65. package/extensions/subagents/src/manager.ts +651 -536
  66. package/extensions/subagents/src/prompt.ts +185 -42
  67. package/extensions/subagents/src/result-artifact.ts +146 -0
  68. package/extensions/subagents/src/result-delivery.ts +7 -1
  69. package/extensions/subagents/src/runtime.ts +23 -6
  70. package/extensions/subagents/src/ui/takeover.ts +128 -337
  71. package/extensions/subagents/src/ui/transcript.ts +38 -501
  72. package/extensions/subagents/src/ui/wait-result.ts +103 -15
  73. package/extensions/suggestions/src/ui.ts +10 -4
  74. package/extensions/tasks/index.ts +0 -3
  75. package/extensions/tasks/ui.ts +79 -62
  76. package/extensions/ui-customization/footer.ts +7 -44
  77. package/extensions/ui-customization/index.ts +0 -4
  78. package/extensions/user-input-fold/index.ts +185 -0
  79. package/extensions/web/index.ts +234 -0
  80. package/extensions/workflows/artifacts.ts +147 -22
  81. package/extensions/workflows/completion-projection.ts +457 -0
  82. package/extensions/workflows/controller.ts +14 -2
  83. package/extensions/workflows/coordinator.ts +62 -0
  84. package/extensions/workflows/dashboard.ts +458 -339
  85. package/extensions/workflows/handoff.ts +121 -25
  86. package/extensions/workflows/index.ts +1042 -492
  87. package/extensions/workflows/journal.ts +148 -13
  88. package/extensions/workflows/model.ts +131 -19
  89. package/extensions/workflows/navigation.ts +61 -18
  90. package/extensions/workflows/progress-projection.ts +306 -0
  91. package/extensions/workflows/prompt.ts +166 -10
  92. package/extensions/workflows/replay-safety.ts +58 -27
  93. package/extensions/workflows/result-delivery.ts +253 -0
  94. package/extensions/workflows/retention.ts +593 -0
  95. package/extensions/workflows/runner.ts +388 -279
  96. package/extensions/workflows/sandbox-child.cjs +36 -3
  97. package/extensions/workflows/sandbox.ts +62 -8
  98. package/extensions/workflows/serialization.ts +325 -17
  99. package/extensions/workflows/tool-renderer.ts +22 -0
  100. package/extensions/workflows/transcript.ts +149 -0
  101. package/extensions/workspace-cleanup-guard/index.ts +54 -0
  102. package/extensions/workspace-cleanup-guard/workspace-provenance.ts +563 -0
  103. package/package.json +28 -8
  104. package/skills/subagents/REFERENCE.md +189 -0
  105. package/skills/subagents/SKILL.md +2 -2
  106. package/skills/workflows/REFERENCE.md +10 -5
  107. package/skills/workflows/SKILL.md +53 -10
  108. package/web/adapter/pi-adapter.ts +661 -0
  109. package/web/host/browser-launcher.ts +20 -0
  110. package/web/host/static-assets.ts +4 -0
  111. package/web/host/terminal-status.ts +38 -0
  112. package/web/host/web-host.ts +789 -0
  113. package/web/http-dispatcher.ts +125 -0
  114. package/web/protocol/types.ts +462 -0
  115. package/web/runtime/pi-runtime.ts +991 -0
  116. package/web/runtime/types.ts +71 -0
  117. package/web/runtime/web-host-lease.ts +497 -0
  118. package/web/trace.ts +18 -0
  119. package/web/ui/app.js +1398 -0
  120. package/web/ui/index.html +139 -0
  121. package/web/ui/styles.css +598 -0
  122. package/web/vite.config.mjs +34 -0
  123. package/extensions/execution-convergence/active-evidence.ts +0 -129
  124. package/extensions/execution-convergence/index.ts +0 -442
  125. package/extensions/execution-convergence/workspace-provenance.ts +0 -338
  126. package/extensions/setup/intercom-fs-helper.cjs +0 -130
  127. package/extensions/setup/intercom.ts +0 -603
  128. package/extensions/subagents/src/backends/stub.ts +0 -296
  129. package/extensions/subagents/src/format.ts +0 -48
@@ -0,0 +1,457 @@
1
+ import { allocateResultBudgets } from "../shared/result-budget.ts";
2
+ import { sanitizeTerminalText } from "../shared/terminal-text.ts";
3
+ import { projectText } from "../shared/text-projection.ts";
4
+ import {
5
+ countStates,
6
+ formatElapsed,
7
+ resultJson,
8
+ sanitizeWorkflowDisplayLine,
9
+ shortenHome,
10
+ statusWord,
11
+ type WorkflowDetails,
12
+ type WorkflowLogEntry,
13
+ type WorkflowStatus,
14
+ } from "./model.ts";
15
+ import { safeStringify } from "./serialization.ts";
16
+
17
+ const MAX_DISPLAY_ENTRIES = 64;
18
+ const MAX_DISPLAY_BYTES = 64 * 1024;
19
+ const MAX_EXPANDED_ENTRY_BYTES = 48 * 1024;
20
+ const MAX_ALERTS = 16;
21
+ const MAX_FIELD_BYTES = 2 * 1024;
22
+
23
+ export interface WorkflowCompletionSourceEntry {
24
+ deliveryId: string;
25
+ details: WorkflowDetails;
26
+ runDir: string;
27
+ }
28
+
29
+ /** Bounded operator-facing facts. Runtime state remains in artifacts/model context. */
30
+ export interface WorkflowCompletionDisplayEntry {
31
+ deliveryId: string;
32
+ runId: string;
33
+ status: WorkflowStatus;
34
+ summary: string;
35
+ alerts: string[];
36
+ resultPreview?: string;
37
+ expanded: string;
38
+ }
39
+
40
+ export interface WorkflowCompletionDisplay {
41
+ version: 1;
42
+ /** Backward-compatible single-run identity for delivery observers. */
43
+ runId?: string;
44
+ entries: WorkflowCompletionDisplayEntry[];
45
+ omittedEntries?: number;
46
+ }
47
+
48
+ function boundedLine(value: string, maxBytes = MAX_FIELD_BYTES) {
49
+ return projectText(sanitizeWorkflowDisplayLine(value), {
50
+ maxBytes,
51
+ maxLines: 1,
52
+ recovery: "",
53
+ });
54
+ }
55
+
56
+ function labels(details: WorkflowDetails, state: "error" | "uncertain") {
57
+ return details.agents
58
+ .filter((agent) => agent.state === state)
59
+ .map((agent) => sanitizeWorkflowDisplayLine(agent.label));
60
+ }
61
+
62
+ function completionSummary(details: WorkflowDetails) {
63
+ const { done, failed } = countStates(details);
64
+ const elapsed = formatElapsed(details.startedAt, details.finishedAt);
65
+ return boundedLine(
66
+ `workflow ${details.name ?? details.runId} · ${done + failed}/${details.agents.length} agents · ${elapsed} · ${statusWord(details.status)}`,
67
+ );
68
+ }
69
+
70
+ function isDroppedWorkLog(entry: WorkflowLogEntry) {
71
+ if (entry.kind === "pipeline-drop") return true;
72
+ const text = entry.text;
73
+ const negated =
74
+ /\b(?:no|none|nothing|not|zero|0)\b.{0,48}\b(?:dropped|discarded|omitted)\b/iu.test(
75
+ text,
76
+ ) ||
77
+ /(?:没有|并未|未曾|未|无|零(?:个|项)?).{0,24}(?:丢弃|丢失|遗漏)/u.test(
78
+ text,
79
+ );
80
+ if (negated) return false;
81
+ return (
82
+ /\b(?:dropped|discarded|omitted)\b/iu.test(text) ||
83
+ /(?:被)?(?:丢弃|丢失|遗漏)/u.test(text)
84
+ );
85
+ }
86
+
87
+ /** Exceptional evidence that must remain visible while the report is collapsed. */
88
+ function completionAlerts(details: WorkflowDetails) {
89
+ const alerts: string[] = [];
90
+ if (details.error) alerts.push(`Error: ${details.error}`);
91
+
92
+ const failed = labels(details, "error");
93
+ if (failed.length > 0) alerts.push(`Failed agents: ${failed.join(", ")}`);
94
+ const uncertain = labels(details, "uncertain");
95
+ if (uncertain.length > 0) {
96
+ alerts.push(`Uncertain agents: ${uncertain.join(", ")}`);
97
+ }
98
+ if (details.logsDropped) {
99
+ alerts.push(`${details.logsDropped} earlier log line(s) dropped`);
100
+ }
101
+
102
+ for (const entry of details.logs ?? []) {
103
+ if (!isDroppedWorkLog(entry)) continue;
104
+ alerts.push(`Dropped work: ${sanitizeWorkflowDisplayLine(entry.text)}`);
105
+ }
106
+
107
+ for (const agent of details.agents) {
108
+ if (agent.worktreePath) {
109
+ const reason = agent.worktreeCleanup?.reason ?? "cleanup was unsafe";
110
+ alerts.push(
111
+ `Retained worktree [${sanitizeWorkflowDisplayLine(agent.label)}]: ${shortenHome(agent.worktreePath)} (${sanitizeWorkflowDisplayLine(reason)})${
112
+ agent.worktreeHandoffArtifact
113
+ ? `; handoff ${sanitizeWorkflowDisplayLine(agent.worktreeHandoffArtifact)}`
114
+ : ""
115
+ }`,
116
+ );
117
+ continue;
118
+ }
119
+ if (!agent.worktreeHandoffArtifact) continue;
120
+ const cleanup = agent.worktreeCleanup;
121
+ const work = cleanup?.commits
122
+ ? `${cleanup.commits} commit${cleanup.commits === 1 ? "" : "s"} on ${cleanup.branch}`
123
+ : agent.worktreeBranch
124
+ ? `branch ${agent.worktreeBranch}`
125
+ : "isolated work";
126
+ alerts.push(
127
+ `Worktree handoff [${sanitizeWorkflowDisplayLine(agent.label)}]: ${sanitizeWorkflowDisplayLine(work)}; ${sanitizeWorkflowDisplayLine(agent.worktreeHandoffArtifact)}`,
128
+ );
129
+ }
130
+
131
+ const unique = [...new Set(alerts)].map((alert) =>
132
+ sanitizeTerminalText(boundedLine(alert, 512)),
133
+ );
134
+ if (unique.length <= MAX_ALERTS) return unique;
135
+ return [
136
+ ...unique.slice(0, MAX_ALERTS - 1),
137
+ `${unique.length - MAX_ALERTS + 1} more exceptional item(s); expand for evidence`,
138
+ ];
139
+ }
140
+
141
+ /** Short return-value preview for collapsed success cards. */
142
+ function completionResultPreview(details: WorkflowDetails) {
143
+ if (details.result === undefined) return undefined;
144
+ if (typeof details.result === "string") return boundedLine(details.result);
145
+ const serialized = safeStringify(details.result, { maxBytes: 2 * 1024 });
146
+ try {
147
+ return boundedLine(JSON.stringify(JSON.parse(serialized)));
148
+ } catch {
149
+ return boundedLine(resultJson(details.result));
150
+ }
151
+ }
152
+
153
+ /** Operator evidence, intentionally independent from the model transport report. */
154
+ function buildOperatorReport(
155
+ details: WorkflowDetails,
156
+ runDir: string,
157
+ deliveryId: string,
158
+ ) {
159
+ const { done, failed, uncertain } = countStates(details);
160
+ const elapsed = formatElapsed(details.startedAt, details.finishedAt);
161
+ const lines = [
162
+ `Workflow ${details.name ? `"${details.name}"` : details.runId} ${details.status} — ${done}/${details.agents.length} agents ok${failed ? `, ${failed} failed` : ""}${uncertain ? `, ${uncertain} uncertain` : ""} across ${details.phases.length} phase(s) in ${elapsed}.`,
163
+ `Run dir: ${shortenHome(runDir)}`,
164
+ `Delivery id: ${deliveryId}`,
165
+ ];
166
+
167
+ const artifacts = [
168
+ details.resultArtifact ? `Result: ${details.resultArtifact}` : undefined,
169
+ details.transcriptArtifact
170
+ ? `Transcripts: ${details.transcriptArtifact}`
171
+ : undefined,
172
+ ].filter((entry): entry is string => entry !== undefined);
173
+ if (artifacts.length > 0) lines.push("", "Artifacts:", ...artifacts);
174
+
175
+ const replayed = details.agents.filter((agent) => agent.replayed).length;
176
+ if (details.resumedFrom) {
177
+ lines.push(
178
+ `Resumed from ${details.resumedFrom}: replayed ${replayed}/${details.agents.length} agent call(s), ran ${details.agents.length - replayed} for real.`,
179
+ );
180
+ }
181
+ if (details.resumeNote) lines.push(`Resume: ${details.resumeNote}`);
182
+ if (details.error) lines.push(`Error: ${details.error}`);
183
+
184
+ if (details.logs?.length) {
185
+ lines.push("", "Log:");
186
+ if (details.logsDropped) {
187
+ lines.push(` (${details.logsDropped} earlier line(s) dropped)`);
188
+ }
189
+ for (const entry of details.logs) lines.push(` ${entry.text}`);
190
+ }
191
+
192
+ const isolated = details.agents.filter(
193
+ (agent) => agent.worktreeBranch || agent.worktreePath,
194
+ );
195
+ if (isolated.length > 0) {
196
+ lines.push("", "Isolated worktrees:");
197
+ for (const agent of isolated) {
198
+ const cleanup = agent.worktreeCleanup;
199
+ const work = cleanup?.commits
200
+ ? `${cleanup.commits} commit${cleanup.commits === 1 ? "" : "s"} on ${cleanup.branch}`
201
+ : agent.worktreeBranch
202
+ ? `committed to branch ${agent.worktreeBranch}`
203
+ : "no commits";
204
+ lines.push(
205
+ `- [${agent.label}] ${work}${
206
+ agent.worktreePath
207
+ ? `; kept at ${shortenHome(agent.worktreePath)} (${cleanup?.reason ?? "uncommitted changes"})`
208
+ : cleanup?.branchDeleted
209
+ ? "; empty branch deleted"
210
+ : cleanup?.reason
211
+ ? `; cleanup warning: ${cleanup.reason}`
212
+ : ""
213
+ }${agent.worktreeHandoffArtifact ? `; handoff ${agent.worktreeHandoffArtifact}` : ""}`,
214
+ );
215
+ }
216
+ }
217
+
218
+ if (details.agents.length > 0) {
219
+ lines.push("", "Agents:");
220
+ for (const agent of details.agents) {
221
+ const state =
222
+ agent.state === "done"
223
+ ? agent.replayed
224
+ ? "ok (replayed)"
225
+ : "ok"
226
+ : agent.state === "error"
227
+ ? "FAILED"
228
+ : agent.state === "uncertain"
229
+ ? "UNCERTAIN"
230
+ : "running";
231
+ lines.push(
232
+ `- [${agent.label}]${agent.phase ? ` (${agent.phase})` : ""} ${state}` +
233
+ (agent.acceptance ? ` · acceptance ${agent.acceptance.status}` : "") +
234
+ (agent.error ? ` — ${agent.error}` : ""),
235
+ );
236
+ }
237
+ }
238
+ if (details.result !== undefined) {
239
+ lines.push("", "Result:", resultJson(details.result));
240
+ }
241
+ return sanitizeTerminalText(lines.join("\n"));
242
+ }
243
+
244
+ /** Build a byte-bounded display projection without retaining runtime objects. */
245
+ export function buildWorkflowCompletionDisplay(
246
+ sourceEntries: readonly WorkflowCompletionSourceEntry[],
247
+ ): WorkflowCompletionDisplay {
248
+ const selected = sourceEntries.slice(0, MAX_DISPLAY_ENTRIES);
249
+ const fixedEntry = ({
250
+ deliveryId,
251
+ details,
252
+ }: WorkflowCompletionSourceEntry) => ({
253
+ deliveryId: boundedLine(deliveryId, 512),
254
+ runId: boundedLine(details.runId, 512),
255
+ status: details.status,
256
+ summary: completionSummary(details),
257
+ alerts: completionAlerts(details),
258
+ resultPreview: completionResultPreview(details),
259
+ expanded: "",
260
+ });
261
+ let fixedEntries = selected.map(fixedEntry);
262
+ const fixedSize = () =>
263
+ Buffer.byteLength(
264
+ JSON.stringify({
265
+ version: 1,
266
+ ...(sourceEntries.length === 1
267
+ ? { runId: fixedEntries[0]?.runId ?? "" }
268
+ : {}),
269
+ entries: fixedEntries,
270
+ ...(sourceEntries.length > selected.length
271
+ ? { omittedEntries: sourceEntries.length - selected.length }
272
+ : {}),
273
+ }),
274
+ "utf8",
275
+ );
276
+ while (selected.length > 1 && fixedSize() > MAX_DISPLAY_BYTES / 2) {
277
+ selected.pop();
278
+ fixedEntries.pop();
279
+ }
280
+ const fixedBytes = fixedSize();
281
+ const reports = selected.map(({ deliveryId, details, runDir }) =>
282
+ buildOperatorReport(details, runDir, deliveryId),
283
+ );
284
+ const allocation = allocateResultBudgets(
285
+ reports.map((report) => Buffer.byteLength(report, "utf8")),
286
+ undefined,
287
+ {
288
+ maxBatchBytes: Math.max(0, MAX_DISPLAY_BYTES - fixedBytes),
289
+ maxResultBytes: MAX_EXPANDED_ENTRY_BYTES,
290
+ minResultBytes: 512,
291
+ headroomShare: 0,
292
+ estimatedBytesPerToken: 4,
293
+ },
294
+ );
295
+ let budgets = [...allocation.budgets];
296
+ const projectedEntries = () =>
297
+ fixedEntries.map((entry, index) => ({
298
+ ...entry,
299
+ expanded: projectText(reports[index]!, {
300
+ maxBytes: budgets[index] ?? 0,
301
+ maxLines: 600,
302
+ recovery: `Full workflow evidence is available in ${shortenHome(selected[index]!.runDir)}.`,
303
+ }),
304
+ }));
305
+ let entries = projectedEntries();
306
+ const projectedSize = () =>
307
+ Buffer.byteLength(
308
+ JSON.stringify({
309
+ version: 1,
310
+ ...(sourceEntries.length === 1
311
+ ? { runId: entries[0]?.runId ?? "" }
312
+ : {}),
313
+ entries,
314
+ ...(sourceEntries.length > selected.length
315
+ ? { omittedEntries: sourceEntries.length - selected.length }
316
+ : {}),
317
+ }),
318
+ "utf8",
319
+ );
320
+ for (let attempt = 0; projectedSize() > MAX_DISPLAY_BYTES; attempt++) {
321
+ if (attempt >= 8) {
322
+ budgets = budgets.map(() => 0);
323
+ } else {
324
+ const expandedBytes = budgets.reduce((sum, budget) => sum + budget, 0);
325
+ const overflow = projectedSize() - MAX_DISPLAY_BYTES;
326
+ const target = Math.max(
327
+ 0,
328
+ expandedBytes - overflow - entries.length * 16,
329
+ );
330
+ const scale = expandedBytes > 0 ? target / expandedBytes : 0;
331
+ budgets = budgets.map((budget) => Math.floor(budget * scale));
332
+ }
333
+ entries = projectedEntries();
334
+ }
335
+ const display: WorkflowCompletionDisplay = {
336
+ version: 1,
337
+ ...(sourceEntries.length === 1
338
+ ? { runId: entries[0]?.runId ?? sourceEntries[0]!.details.runId }
339
+ : {}),
340
+ entries,
341
+ ...(sourceEntries.length > selected.length
342
+ ? { omittedEntries: sourceEntries.length - selected.length }
343
+ : {}),
344
+ };
345
+ return display;
346
+ }
347
+
348
+ function isString(value: unknown, maxBytes = MAX_FIELD_BYTES) {
349
+ return (
350
+ typeof value === "string" && Buffer.byteLength(value, "utf8") <= maxBytes
351
+ );
352
+ }
353
+
354
+ function isStatus(value: unknown): value is WorkflowStatus {
355
+ return (
356
+ value === "running" ||
357
+ value === "completed" ||
358
+ value === "failed" ||
359
+ value === "aborted" ||
360
+ value === "uncertain"
361
+ );
362
+ }
363
+
364
+ function hasOnlyKeys(value: object, allowed: readonly string[]) {
365
+ const keys = new Set(allowed);
366
+ return Object.keys(value).every((key) => keys.has(key));
367
+ }
368
+
369
+ export function isWorkflowCompletionDisplay(
370
+ value: unknown,
371
+ ): value is WorkflowCompletionDisplay {
372
+ if (!value || typeof value !== "object") return false;
373
+ let bytes: number;
374
+ try {
375
+ bytes = Buffer.byteLength(JSON.stringify(value), "utf8");
376
+ } catch {
377
+ return false;
378
+ }
379
+ if (bytes > MAX_DISPLAY_BYTES) return false;
380
+ try {
381
+ const candidate = value as Partial<WorkflowCompletionDisplay>;
382
+ if (
383
+ !hasOnlyKeys(candidate, [
384
+ "version",
385
+ "runId",
386
+ "entries",
387
+ "omittedEntries",
388
+ ]) ||
389
+ candidate.version !== 1 ||
390
+ !Array.isArray(candidate.entries) ||
391
+ candidate.entries.length > MAX_DISPLAY_ENTRIES ||
392
+ (candidate.runId !== undefined && !isString(candidate.runId, 512)) ||
393
+ (candidate.omittedEntries !== undefined &&
394
+ (!Number.isSafeInteger(candidate.omittedEntries) ||
395
+ candidate.omittedEntries <= 0))
396
+ ) {
397
+ return false;
398
+ }
399
+ return candidate.entries.every((entry) => {
400
+ if (!entry || typeof entry !== "object") return false;
401
+ const record = entry as Partial<WorkflowCompletionDisplayEntry>;
402
+ return (
403
+ hasOnlyKeys(record, [
404
+ "deliveryId",
405
+ "runId",
406
+ "status",
407
+ "summary",
408
+ "alerts",
409
+ "resultPreview",
410
+ "expanded",
411
+ ]) &&
412
+ isString(record.deliveryId, 512) &&
413
+ isString(record.runId, 512) &&
414
+ isStatus(record.status) &&
415
+ isString(record.summary) &&
416
+ Array.isArray(record.alerts) &&
417
+ record.alerts.length <= MAX_ALERTS &&
418
+ record.alerts.every((alert) => isString(alert, 512)) &&
419
+ (record.resultPreview === undefined ||
420
+ isString(record.resultPreview)) &&
421
+ isString(record.expanded, MAX_EXPANDED_ENTRY_BYTES)
422
+ );
423
+ });
424
+ } catch {
425
+ return false;
426
+ }
427
+ }
428
+
429
+ export function workflowCompletionSummary(
430
+ entry: WorkflowCompletionDisplayEntry,
431
+ ) {
432
+ return entry.summary;
433
+ }
434
+
435
+ export function workflowCompletionAlerts(
436
+ entry: WorkflowCompletionDisplayEntry,
437
+ ) {
438
+ return entry.alerts;
439
+ }
440
+
441
+ export function workflowCompletionResultPreview(
442
+ entry: WorkflowCompletionDisplayEntry,
443
+ ) {
444
+ return entry.resultPreview;
445
+ }
446
+
447
+ export function buildExpandedWorkflowCompletion(
448
+ display: WorkflowCompletionDisplay,
449
+ ) {
450
+ const reports = display.entries.map((entry) => entry.expanded);
451
+ if (display.omittedEntries) {
452
+ reports.push(
453
+ `${display.omittedEntries} additional workflow completion(s) omitted from this display projection; their full evidence remains in workflow artifacts and model-visible delivery context.`,
454
+ );
455
+ }
456
+ return reports.join("\n\n");
457
+ }
@@ -83,6 +83,7 @@ export class RunController {
83
83
  private readonly abortController = new AbortController();
84
84
  private readonly semaphore: Semaphore;
85
85
  private readonly maxAgentCalls: number;
86
+ private readonly concurrency: number;
86
87
  private readonly tasks = new Set<Promise<unknown>>();
87
88
  private callCount = 0;
88
89
  private sealed = false;
@@ -95,9 +96,11 @@ export class RunController {
95
96
  concurrency = DEFAULT_WORKFLOW_CONCURRENCY,
96
97
  maxAgentCalls = DEFAULT_WORKFLOW_MAX_AGENT_CALLS,
97
98
  ) {
98
- this.semaphore = new Semaphore(
99
- Math.max(1, Math.min(MAX_WORKFLOW_CONCURRENCY, Math.floor(concurrency))),
99
+ this.concurrency = Math.max(
100
+ 1,
101
+ Math.min(MAX_WORKFLOW_CONCURRENCY, Math.floor(concurrency)),
100
102
  );
103
+ this.semaphore = new Semaphore(this.concurrency);
101
104
  this.maxAgentCalls = Math.max(
102
105
  1,
103
106
  Math.min(MAX_WORKFLOW_AGENT_CALLS, Math.floor(maxAgentCalls)),
@@ -121,6 +124,15 @@ export class RunController {
121
124
  return this.callCount;
122
125
  }
123
126
 
127
+ capacity() {
128
+ return {
129
+ concurrency: this.concurrency,
130
+ maxAgentCalls: this.maxAgentCalls,
131
+ callsUsed: this.callCount,
132
+ callsRemaining: Math.max(0, this.maxAgentCalls - this.callCount),
133
+ };
134
+ }
135
+
124
136
  schedule<T>(
125
137
  task: (signal: AbortSignal) => Promise<T>,
126
138
  invocationSignal?: AbortSignal,
@@ -0,0 +1,62 @@
1
+ export interface WorkflowLaunchPolicyInput {
2
+ wait?: boolean;
3
+ background?: boolean;
4
+ }
5
+
6
+ /**
7
+ * Resolve the caller's launch preference to one positive runtime mode.
8
+ * `background` remains only as the published inverse compatibility alias.
9
+ */
10
+ export function resolveWorkflowLaunchMode(
11
+ input: WorkflowLaunchPolicyInput,
12
+ canDeliverLater: boolean,
13
+ ) {
14
+ if (
15
+ input.wait !== undefined &&
16
+ input.background !== undefined &&
17
+ input.wait === input.background
18
+ ) {
19
+ throw new Error(
20
+ "wait and background conflict: background is the deprecated inverse of wait; remove background and provide only wait",
21
+ );
22
+ }
23
+ const wait =
24
+ input.wait ??
25
+ (input.background !== undefined ? !input.background : !canDeliverLater);
26
+ if (!wait && !canDeliverLater) {
27
+ throw new Error(
28
+ "This host cannot deliver a workflow result later; use wait: true",
29
+ );
30
+ }
31
+ return wait ? "inline" : "detached";
32
+ }
33
+
34
+ /**
35
+ * Arbitrate an inline wait against caller cancellation without transferring
36
+ * ownership of the workflow run to the wait signal.
37
+ */
38
+ export async function waitForWorkflowCompletion(
39
+ completion: Promise<unknown>,
40
+ signal?: AbortSignal,
41
+ ): Promise<"terminal" | "aborted"> {
42
+ if (!signal) {
43
+ await completion.catch(() => {});
44
+ return "terminal";
45
+ }
46
+ if (signal.aborted) return "aborted";
47
+
48
+ let onAbort: (() => void) | undefined;
49
+ const aborted = new Promise<"aborted">((resolve) => {
50
+ onAbort = () => resolve("aborted");
51
+ signal.addEventListener("abort", onAbort, { once: true });
52
+ });
53
+ const terminal = completion.then(
54
+ () => "terminal" as const,
55
+ () => "terminal" as const,
56
+ );
57
+ try {
58
+ return await Promise.race([terminal, aborted]);
59
+ } finally {
60
+ if (onAbort) signal.removeEventListener("abort", onAbort);
61
+ }
62
+ }