@tt-a1i/openpi 0.3.0 → 0.4.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.
- package/README.md +87 -24
- package/SETUP.md +3 -3
- package/extensions/ask-user/index.ts +30 -14
- package/extensions/background-terminals/src/prompt.ts +1 -1
- package/extensions/background-terminals/src/ui/ps.ts +132 -129
- package/extensions/capabilities/index.ts +35 -44
- package/extensions/capabilities/src/ui.ts +93 -0
- package/extensions/file-mutation-display/index.ts +34 -76
- package/extensions/file-mutation-display/render.ts +387 -88
- package/extensions/file-search/index.ts +8 -7
- package/extensions/file-search/src/binaries.ts +18 -18
- package/extensions/git-info/src/changed-files-view.ts +47 -14
- package/extensions/git-read/index.ts +330 -0
- package/extensions/git-read/src/args.ts +171 -0
- package/extensions/git-read/src/process.ts +81 -0
- package/extensions/git-read/src/prompt.ts +56 -0
- package/extensions/sessions/index.ts +70 -55
- package/extensions/setup/index.ts +6 -6
- package/extensions/shared/activity-status.ts +6 -5
- package/extensions/shared/below-editor-navigation.ts +26 -0
- package/extensions/shared/capability-intent.ts +53 -0
- package/extensions/shared/child-session.ts +7 -1
- package/extensions/shared/result-budget.ts +134 -0
- package/extensions/shared/screen-chrome.ts +133 -0
- package/extensions/shared/setup-config.ts +24 -5
- package/extensions/shared/spinner.ts +28 -0
- package/extensions/shared/text-projection.ts +56 -0
- package/extensions/shared/tool-surface.ts +13 -6
- package/extensions/subagents/index.ts +216 -170
- package/extensions/subagents/navigation.ts +52 -23
- package/extensions/subagents/src/agent-types.ts +37 -15
- package/extensions/subagents/src/backends/stub.ts +7 -0
- package/extensions/subagents/src/id-sequence.ts +84 -0
- package/extensions/subagents/src/manager.ts +620 -537
- package/extensions/subagents/src/prompt.ts +153 -38
- package/extensions/subagents/src/result-artifact.ts +142 -0
- package/extensions/subagents/src/result-delivery.ts +50 -5
- package/extensions/subagents/src/runtime.ts +8 -5
- package/extensions/subagents/src/ui/takeover.ts +84 -109
- package/extensions/subagents/src/ui/transcript.ts +76 -42
- package/extensions/subagents/src/ui/wait-result.ts +1 -1
- package/extensions/tasks/ui.ts +79 -62
- package/extensions/ui-customization/footer.ts +7 -4
- package/extensions/user-input-fold/index.ts +185 -0
- package/extensions/workflows/artifacts.ts +35 -0
- package/extensions/workflows/controller.ts +14 -2
- package/extensions/workflows/coordinator.ts +64 -0
- package/extensions/workflows/dashboard.ts +353 -173
- package/extensions/workflows/handoff.ts +62 -20
- package/extensions/workflows/index.ts +647 -387
- package/extensions/workflows/model.ts +57 -15
- package/extensions/workflows/navigation.ts +33 -14
- package/extensions/workflows/prompt.ts +104 -8
- package/extensions/workflows/replay-safety.ts +16 -6
- package/extensions/workflows/result-delivery.ts +189 -0
- package/extensions/workflows/sandbox-child.cjs +11 -0
- package/package.json +1 -1
- package/skills/subagents/SKILL.md +2 -2
- package/skills/workflows/REFERENCE.md +7 -4
- package/skills/workflows/SKILL.md +53 -10
- package/extensions/subagents/src/format.ts +0 -48
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
type ExtensionContext,
|
|
10
10
|
} from "@earendil-works/pi-coding-agent";
|
|
11
11
|
import { formatContextUtilization } from "../shared/context-utilization.ts";
|
|
12
|
+
import { spinnerFrame } from "../shared/spinner.ts";
|
|
12
13
|
import { sanitizeTerminalText } from "../shared/terminal-text.ts";
|
|
13
14
|
import type { WorktreeCleanup } from "../shared/worktree.ts";
|
|
14
15
|
import type { AcceptanceLedger } from "./acceptance.ts";
|
|
@@ -47,8 +48,31 @@ export function emptyUsage(): AgentUsage {
|
|
|
47
48
|
};
|
|
48
49
|
}
|
|
49
50
|
|
|
50
|
-
export type AgentState = "running" | "done" | "error";
|
|
51
|
-
export type WorkflowStatus =
|
|
51
|
+
export type AgentState = "running" | "done" | "error" | "uncertain";
|
|
52
|
+
export type WorkflowStatus =
|
|
53
|
+
| "running"
|
|
54
|
+
| "completed"
|
|
55
|
+
| "failed"
|
|
56
|
+
| "aborted"
|
|
57
|
+
| "uncertain";
|
|
58
|
+
|
|
59
|
+
export type WorkflowDeliveryState =
|
|
60
|
+
| "none"
|
|
61
|
+
| "held-for-inline"
|
|
62
|
+
| "pending"
|
|
63
|
+
| "delivered"
|
|
64
|
+
| "consumed-inline";
|
|
65
|
+
|
|
66
|
+
/** Durable completion-delivery plane, independent from execution status. */
|
|
67
|
+
export interface WorkflowDelivery {
|
|
68
|
+
/** Stable per-run idempotency identity, never a transport-batch id. */
|
|
69
|
+
id: string;
|
|
70
|
+
state: WorkflowDeliveryState;
|
|
71
|
+
attempts: number;
|
|
72
|
+
updatedAt: number;
|
|
73
|
+
deliveredAt?: number;
|
|
74
|
+
lastError?: string;
|
|
75
|
+
}
|
|
52
76
|
|
|
53
77
|
export type TranscriptRole =
|
|
54
78
|
| "user"
|
|
@@ -84,6 +108,8 @@ export interface AgentRecord {
|
|
|
84
108
|
inputCallIds?: string[];
|
|
85
109
|
/** Opaque same-run reference returned to the workflow script. */
|
|
86
110
|
resultRef?: string;
|
|
111
|
+
/** Run-directory-relative authoritative result captured before projection. */
|
|
112
|
+
resultArtifact?: string;
|
|
87
113
|
label: string;
|
|
88
114
|
phase?: string;
|
|
89
115
|
state: AgentState;
|
|
@@ -124,6 +150,8 @@ export interface WorkflowDetails {
|
|
|
124
150
|
name?: string;
|
|
125
151
|
description?: string;
|
|
126
152
|
background: boolean;
|
|
153
|
+
/** Whether this run's terminal result is pending, delivered, or inline. */
|
|
154
|
+
delivery?: WorkflowDelivery;
|
|
127
155
|
status: WorkflowStatus;
|
|
128
156
|
startedAt: number;
|
|
129
157
|
finishedAt?: number;
|
|
@@ -334,19 +362,31 @@ export function createUsageReader(agents: readonly AgentRecord[]) {
|
|
|
334
362
|
};
|
|
335
363
|
}
|
|
336
364
|
|
|
337
|
-
/**
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
365
|
+
/**
|
|
366
|
+
* One status indicator per state, shared by the transcript card, the
|
|
367
|
+
* dashboard, and the strips. Running spins on the package-wide cadence so
|
|
368
|
+
* every live view animates in step.
|
|
369
|
+
*/
|
|
370
|
+
export function stateGlyph(
|
|
371
|
+
state: AgentState,
|
|
372
|
+
theme: Theme,
|
|
373
|
+
now: number,
|
|
374
|
+
): string {
|
|
375
|
+
if (state === "done") return theme.fg("success", "✓");
|
|
376
|
+
if (state === "error") return theme.fg("error", "✗");
|
|
377
|
+
if (state === "uncertain") return theme.fg("warning", "?");
|
|
378
|
+
return theme.fg("warning", spinnerFrame(now));
|
|
344
379
|
}
|
|
345
380
|
|
|
346
|
-
export function
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
381
|
+
export function statusGlyph(
|
|
382
|
+
status: WorkflowStatus,
|
|
383
|
+
theme: Theme,
|
|
384
|
+
now: number,
|
|
385
|
+
): string {
|
|
386
|
+
if (status === "completed") return theme.fg("success", "✓");
|
|
387
|
+
if (status === "running") return theme.fg("warning", spinnerFrame(now));
|
|
388
|
+
if (status === "uncertain") return theme.fg("warning", "?");
|
|
389
|
+
return theme.fg("error", "✗");
|
|
350
390
|
}
|
|
351
391
|
|
|
352
392
|
export function statusWord(status: WorkflowStatus): string {
|
|
@@ -357,7 +397,7 @@ export function statusColor(
|
|
|
357
397
|
status: WorkflowStatus,
|
|
358
398
|
): "success" | "warning" | "error" {
|
|
359
399
|
if (status === "completed") return "success";
|
|
360
|
-
if (status === "running") return "warning";
|
|
400
|
+
if (status === "running" || status === "uncertain") return "warning";
|
|
361
401
|
return "error";
|
|
362
402
|
}
|
|
363
403
|
|
|
@@ -420,13 +460,15 @@ export function aggregateUsage(agents: AgentRecord[]): AgentUsage {
|
|
|
420
460
|
export function countStates(details: WorkflowDetails) {
|
|
421
461
|
let done = 0;
|
|
422
462
|
let failed = 0;
|
|
463
|
+
let uncertain = 0;
|
|
423
464
|
let running = 0;
|
|
424
465
|
for (const agent of details.agents) {
|
|
425
466
|
if (agent.state === "done") done++;
|
|
426
467
|
else if (agent.state === "error") failed++;
|
|
468
|
+
else if (agent.state === "uncertain") uncertain++;
|
|
427
469
|
else running++;
|
|
428
470
|
}
|
|
429
|
-
return { done, failed, running };
|
|
471
|
+
return { done, failed, uncertain, running };
|
|
430
472
|
}
|
|
431
473
|
|
|
432
474
|
export interface PhaseGroup {
|
|
@@ -4,7 +4,9 @@ import {
|
|
|
4
4
|
BelowEditorStripState,
|
|
5
5
|
belowEditorStripInput,
|
|
6
6
|
fitNavigationSides,
|
|
7
|
+
renderNavigationMetrics,
|
|
7
8
|
} from "../shared/below-editor-navigation.ts";
|
|
9
|
+
import { SPINNER_INTERVAL_MS, spinnerFrame } from "../shared/spinner.ts";
|
|
8
10
|
import { sanitizeTerminalText } from "../shared/terminal-text.ts";
|
|
9
11
|
import {
|
|
10
12
|
aggregateUsage,
|
|
@@ -12,9 +14,9 @@ import {
|
|
|
12
14
|
formatElapsed,
|
|
13
15
|
formatTokens,
|
|
14
16
|
statusColor,
|
|
15
|
-
statusSquare,
|
|
16
17
|
type Theme,
|
|
17
18
|
type WorkflowDetails,
|
|
19
|
+
type WorkflowStatus,
|
|
18
20
|
} from "./model.ts";
|
|
19
21
|
|
|
20
22
|
/** Workflow-named aliases preserve the public seam while sharing interaction. */
|
|
@@ -33,6 +35,17 @@ function cleanLine(value: string) {
|
|
|
33
35
|
return sanitizeTerminalText(value).replace(/\s+/g, " ").trim();
|
|
34
36
|
}
|
|
35
37
|
|
|
38
|
+
/**
|
|
39
|
+
* One status indicator per run state; doubles as the focus marker when
|
|
40
|
+
* selected. Running spins, in step with the dashboard and takeover headers.
|
|
41
|
+
*/
|
|
42
|
+
function statusGlyph(status: WorkflowStatus, theme: Theme, now: number) {
|
|
43
|
+
if (status === "completed") return theme.fg("success", "✓");
|
|
44
|
+
if (status === "running") return theme.fg("warning", spinnerFrame(now));
|
|
45
|
+
if (status === "uncertain") return theme.fg("warning", "?");
|
|
46
|
+
return theme.fg("error", "✗");
|
|
47
|
+
}
|
|
48
|
+
|
|
36
49
|
/** Live, one-line Claude-style workflow entry rendered below the editor. */
|
|
37
50
|
export class WorkflowStripWidget {
|
|
38
51
|
private readonly timer: ReturnType<typeof setInterval>;
|
|
@@ -51,7 +64,10 @@ export class WorkflowStripWidget {
|
|
|
51
64
|
this.theme = theme;
|
|
52
65
|
this.strip = strip;
|
|
53
66
|
this.getEntry = getEntry;
|
|
54
|
-
this.timer = setInterval(
|
|
67
|
+
this.timer = setInterval(
|
|
68
|
+
() => this.tui.requestRender(),
|
|
69
|
+
SPINNER_INTERVAL_MS,
|
|
70
|
+
);
|
|
55
71
|
this.timer.unref?.();
|
|
56
72
|
}
|
|
57
73
|
|
|
@@ -65,29 +81,32 @@ export class WorkflowStripWidget {
|
|
|
65
81
|
const entry = this.getEntry();
|
|
66
82
|
if (!entry || width <= 0) return [];
|
|
67
83
|
const details = entry.details;
|
|
68
|
-
const { done, failed } = countStates(details);
|
|
84
|
+
const { done, failed, uncertain } = countStates(details);
|
|
69
85
|
const settled = done + failed;
|
|
70
86
|
const usage = aggregateUsage(details.agents);
|
|
71
87
|
const tokenCount = usage.input + usage.output;
|
|
72
|
-
const
|
|
88
|
+
const glyph = this.strip.focused
|
|
73
89
|
? this.theme.fg("accent", "❯")
|
|
74
|
-
: this.theme.
|
|
90
|
+
: statusGlyph(details.status, this.theme, Date.now());
|
|
75
91
|
const displayName = cleanLine(details.name ?? entry.runId) || entry.runId;
|
|
76
92
|
const name = this.strip.focused
|
|
77
93
|
? this.theme.bold(this.theme.fg("accent", displayName))
|
|
78
94
|
: this.theme.fg("text", displayName);
|
|
79
95
|
const rawContext = details.currentPhase ?? details.description;
|
|
80
96
|
const context = rawContext ? cleanLine(rawContext) : undefined;
|
|
81
|
-
const left = ` ${
|
|
82
|
-
const
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
97
|
+
const left = ` ${glyph} ${name}${context ? this.theme.fg("dim", ` · ${context}`) : ""}`;
|
|
98
|
+
const right = renderNavigationMetrics(
|
|
99
|
+
this.theme,
|
|
100
|
+
[
|
|
101
|
+
details.agents.length > 0
|
|
102
|
+
? `${settled}/${details.agents.length} agents${uncertain ? ` · ${uncertain} uncertain` : ""}`
|
|
103
|
+
: undefined,
|
|
104
|
+
formatElapsed(details.startedAt, details.finishedAt),
|
|
105
|
+
tokenCount > 0 ? `${formatTokens(tokenCount)} tokens` : undefined,
|
|
106
|
+
],
|
|
86
107
|
this.strip.focused ? "enter open · ↑ back" : "↓ to manage",
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
.join(" · ");
|
|
90
|
-
const right = this.theme.fg(statusColor(details.status), metrics);
|
|
108
|
+
details.status === "running" ? undefined : statusColor(details.status),
|
|
109
|
+
);
|
|
91
110
|
return [fitNavigationSides(left, right, width)];
|
|
92
111
|
}
|
|
93
112
|
}
|
|
@@ -1,4 +1,9 @@
|
|
|
1
1
|
import { sanitizeTerminalText } from "../shared/terminal-text.ts";
|
|
2
|
+
import {
|
|
3
|
+
allocateResultBudgets,
|
|
4
|
+
type ParentContextUsage,
|
|
5
|
+
} from "../shared/result-budget.ts";
|
|
6
|
+
import { projectText } from "../shared/text-projection.ts";
|
|
2
7
|
import {
|
|
3
8
|
countStates,
|
|
4
9
|
formatElapsed,
|
|
@@ -7,13 +12,14 @@ import {
|
|
|
7
12
|
type WorkflowDetails,
|
|
8
13
|
} from "./model.ts";
|
|
9
14
|
|
|
10
|
-
/** Model-facing schema descriptions for workflow source
|
|
15
|
+
/** Model-facing schema descriptions for workflow source and launch policy. */
|
|
11
16
|
export const WORKFLOW_PARAMETER_DESCRIPTIONS = {
|
|
12
17
|
script:
|
|
13
18
|
"JavaScript workflow script. May start with `export const meta = {...}`, then use phase(), agent(), parallel(), args, and a final `return`.",
|
|
14
19
|
args: "Optional JSON string exposed to the script as `args` (parsed when valid JSON, otherwise passed through as the raw string).",
|
|
15
20
|
background:
|
|
16
|
-
"
|
|
21
|
+
"Deprecated compatibility alias: true means wait=false; false means wait=true. Do not provide both background and wait.",
|
|
22
|
+
wait: "Wait for the final result in this tool call. Interactive sessions default to false and deliver completion later; print/automation defaults to true. Interrupting the wait does not cancel the workflow.",
|
|
17
23
|
resumeFromRunId:
|
|
18
24
|
"Optional prior run id or unique suffix for safe read-only replay. See the workflows Skill for matching rules.",
|
|
19
25
|
};
|
|
@@ -29,7 +35,7 @@ export const WORKFLOW_STOP_PARAMETER_DESCRIPTIONS = {
|
|
|
29
35
|
|
|
30
36
|
/** Describes nonblocking inspection of workflow runs, mirroring subagent_check/subagent_list. */
|
|
31
37
|
export const WORKFLOW_STATUS_TOOL_DESCRIPTION =
|
|
32
|
-
"Peek at
|
|
38
|
+
"Peek at workflow runs without blocking. With a run id, returns a bounded status and coverage summary plus the artifact location; without one, lists this session's active and recently finished runs. Does not wait, consume a completion, or repeat the full final result.";
|
|
33
39
|
|
|
34
40
|
/** Model-facing schema description for the optional workflow run id to inspect. */
|
|
35
41
|
export const WORKFLOW_STATUS_PARAMETER_DESCRIPTIONS = {
|
|
@@ -44,9 +50,11 @@ export const WORKFLOW_LIFECYCLE_PROMPT_SNIPPET =
|
|
|
44
50
|
/** Compact resident contract; the workflows Skill carries the complete guide. */
|
|
45
51
|
export const WORKFLOW_TOOL_DESCRIPTION = [
|
|
46
52
|
"Use the workflow tool when the user explicitly requests a workflow run or when the task clearly requires multi-phase dynamic orchestration.",
|
|
47
|
-
"Write an async JavaScript body using optional meta, phase(), log(), usage(), agent(), pipeline(), parallel(), args, and a JSON-serializable return.",
|
|
53
|
+
"Write an async JavaScript body using optional meta, phase(), log(), usage(), agent(), pipeline(), parallel(), args, and a JSON-serializable return. usage().limits reports the resolved concurrency and remaining call capacity.",
|
|
48
54
|
"agent() returns { ok, output, structured?, ref?, error? }; always check `.ok`, use a schema for branching, and surface failed or null results.",
|
|
49
55
|
"Prefer pipeline() for independent multi-stage items. Use parallel() only for a real barrier where the next step needs every prior result.",
|
|
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
|
+
"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.",
|
|
50
58
|
"For concurrent writers use isolation: 'worktree' and tell each agent to commit. Read-only work should normally stay in the shared checkout.",
|
|
51
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.",
|
|
52
60
|
].join("\n");
|
|
@@ -80,11 +88,11 @@ export function buildWorkflowResultMessage(
|
|
|
80
88
|
details: WorkflowDetails,
|
|
81
89
|
runDir: string,
|
|
82
90
|
) {
|
|
83
|
-
const { done, failed } = countStates(details);
|
|
91
|
+
const { done, failed, uncertain } = countStates(details);
|
|
84
92
|
const elapsed = formatElapsed(details.startedAt, details.finishedAt);
|
|
85
93
|
const lines = [
|
|
86
94
|
`Workflow ${details.name ? `"${details.name}"` : details.runId} ${details.status} — ` +
|
|
87
|
-
`${done}/${details.agents.length} agents ok${failed ? `, ${failed} failed` : ""} ` +
|
|
95
|
+
`${done}/${details.agents.length} agents ok${failed ? `, ${failed} failed` : ""}${uncertain ? `, ${uncertain} uncertain` : ""} ` +
|
|
88
96
|
`across ${details.phases.length} phase(s) in ${elapsed}.`,
|
|
89
97
|
`Run dir: ${shortenHome(runDir)}`,
|
|
90
98
|
];
|
|
@@ -144,7 +152,9 @@ export function buildWorkflowResultMessage(
|
|
|
144
152
|
: "ok"
|
|
145
153
|
: agent.state === "error"
|
|
146
154
|
? "FAILED"
|
|
147
|
-
: "
|
|
155
|
+
: agent.state === "uncertain"
|
|
156
|
+
? "UNCERTAIN"
|
|
157
|
+
: "running";
|
|
148
158
|
lines.push(
|
|
149
159
|
`- [${agent.label}]${agent.phase ? ` (${agent.phase})` : ""} ${status}` +
|
|
150
160
|
(agent.acceptance ? ` · acceptance ${agent.acceptance.status}` : "") +
|
|
@@ -157,6 +167,31 @@ export function buildWorkflowResultMessage(
|
|
|
157
167
|
return sanitizeTerminalText(lines.join("\n"));
|
|
158
168
|
}
|
|
159
169
|
|
|
170
|
+
/** One bounded model projection; exact run/agent results remain in artifacts. */
|
|
171
|
+
export function buildProjectedWorkflowResultMessage(
|
|
172
|
+
details: WorkflowDetails,
|
|
173
|
+
runDir: string,
|
|
174
|
+
usage?: ParentContextUsage | null,
|
|
175
|
+
) {
|
|
176
|
+
const full = buildWorkflowResultMessage(details, runDir);
|
|
177
|
+
const allocation = allocateResultBudgets(
|
|
178
|
+
[Buffer.byteLength(full, "utf8")],
|
|
179
|
+
usage,
|
|
180
|
+
{
|
|
181
|
+
maxBatchBytes: 48 * 1024,
|
|
182
|
+
maxResultBytes: 48 * 1024,
|
|
183
|
+
minResultBytes: 8 * 1024,
|
|
184
|
+
headroomShare: 0.25,
|
|
185
|
+
estimatedBytesPerToken: 4,
|
|
186
|
+
},
|
|
187
|
+
);
|
|
188
|
+
return projectText(full, {
|
|
189
|
+
maxBytes: allocation.budgets[0] ?? 8 * 1024,
|
|
190
|
+
maxLines: 400,
|
|
191
|
+
recovery: `Full workflow evidence is available in ${shortenHome(runDir)}.`,
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
|
|
160
195
|
/** Builds the follow-up message that delivers a settled background workflow to the parent model. */
|
|
161
196
|
export function buildBackgroundWorkflowFollowUp(options: {
|
|
162
197
|
runId: string;
|
|
@@ -167,7 +202,47 @@ export function buildBackgroundWorkflowFollowUp(options: {
|
|
|
167
202
|
// Sentence lead-in matching the subagent/terminal completion messages.
|
|
168
203
|
const label = options.name ? `"${options.name}"` : options.runId;
|
|
169
204
|
const verb = options.status === "completed" ? "finished" : options.status;
|
|
170
|
-
return `Background workflow ${label} (${options.runId}) ${verb}.\n\n${options.result}`;
|
|
205
|
+
return `Background workflow ${label} (${options.runId}) ${verb}.\n\n${options.result}\n\n(This result is already shown to the user. Act on it and relay only the decisions or next steps — do not repeat it verbatim.)`;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/** Fairly project one transport batch against the parent's current headroom. */
|
|
209
|
+
export function buildProjectedWorkflowCompletionBatch(
|
|
210
|
+
entries: readonly {
|
|
211
|
+
deliveryId: string;
|
|
212
|
+
details: WorkflowDetails;
|
|
213
|
+
runDir: string;
|
|
214
|
+
}[],
|
|
215
|
+
usage?: ParentContextUsage | null,
|
|
216
|
+
) {
|
|
217
|
+
const full = entries.map(({ deliveryId, details, runDir }) =>
|
|
218
|
+
buildBackgroundWorkflowFollowUp({
|
|
219
|
+
runId: details.runId,
|
|
220
|
+
name: details.name,
|
|
221
|
+
status: details.status,
|
|
222
|
+
result: `${buildWorkflowResultMessage(details, runDir)}\n\nDelivery id: ${deliveryId}`,
|
|
223
|
+
}),
|
|
224
|
+
);
|
|
225
|
+
const separatorBytes = Math.max(0, entries.length - 1) * 2;
|
|
226
|
+
const allocation = allocateResultBudgets(
|
|
227
|
+
full.map((message) => Buffer.byteLength(message, "utf8")),
|
|
228
|
+
usage,
|
|
229
|
+
{
|
|
230
|
+
maxBatchBytes: 48 * 1024 - separatorBytes,
|
|
231
|
+
maxResultBytes: 48 * 1024,
|
|
232
|
+
minResultBytes: 1024,
|
|
233
|
+
headroomShare: 0.25,
|
|
234
|
+
estimatedBytesPerToken: 4,
|
|
235
|
+
},
|
|
236
|
+
);
|
|
237
|
+
return full
|
|
238
|
+
.map((message, index) =>
|
|
239
|
+
projectText(message, {
|
|
240
|
+
maxBytes: allocation.budgets[index] ?? 1024,
|
|
241
|
+
maxLines: 400,
|
|
242
|
+
recovery: `Full workflow evidence is available in ${shortenHome(entries[index]!.runDir)}; duplicate deliveries carry the same delivery id.`,
|
|
243
|
+
}),
|
|
244
|
+
)
|
|
245
|
+
.join("\n\n");
|
|
171
246
|
}
|
|
172
247
|
|
|
173
248
|
/** Builds the background-launch result and tells the parent model how to inspect or stop the run. */
|
|
@@ -182,3 +257,24 @@ export function buildBackgroundWorkflowLaunchResult(options: {
|
|
|
182
257
|
`Its result will be delivered to you when it finishes, or use workflow_status(runId: "${options.runId}") to peek and workflow_stop(runId: "${options.runId}") to cancel; /workflows shows progress.`,
|
|
183
258
|
].join("\n");
|
|
184
259
|
}
|
|
260
|
+
|
|
261
|
+
/** Pure observation: bounded status/coverage without replaying final output. */
|
|
262
|
+
export function buildWorkflowStatusSummary(
|
|
263
|
+
details: WorkflowDetails,
|
|
264
|
+
runDir: string,
|
|
265
|
+
) {
|
|
266
|
+
const { done, failed, uncertain } = countStates(details);
|
|
267
|
+
const settled = done + failed;
|
|
268
|
+
return [
|
|
269
|
+
`Workflow ${details.name ? `"${details.name}"` : details.runId} ${details.status}.`,
|
|
270
|
+
details.status === "uncertain"
|
|
271
|
+
? "Recovery warning: the prior owner disappeared without terminal evidence; some external effects may have occurred."
|
|
272
|
+
: undefined,
|
|
273
|
+
`Coverage: ${settled}/${details.agents.length} agents settled (${done} ok, ${failed} failed)${uncertain ? `; ${uncertain} uncertain` : ""}.`,
|
|
274
|
+
details.currentPhase ? `Current phase: ${details.currentPhase}` : undefined,
|
|
275
|
+
details.delivery ? `Delivery: ${details.delivery.state}.` : undefined,
|
|
276
|
+
`Artifacts: ${shortenHome(runDir)}`,
|
|
277
|
+
]
|
|
278
|
+
.filter(Boolean)
|
|
279
|
+
.join("\n");
|
|
280
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { createHash } from "node:crypto";
|
|
2
1
|
import { execFileSync } from "node:child_process";
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
3
|
import * as fs from "node:fs";
|
|
4
4
|
import { homedir } from "node:os";
|
|
5
5
|
import * as path from "node:path";
|
|
@@ -17,6 +17,9 @@ const REPLAY_FILESYSTEM_TOOL_NAMES = new Set([
|
|
|
17
17
|
"ls",
|
|
18
18
|
"fd",
|
|
19
19
|
"rg",
|
|
20
|
+
"git_show",
|
|
21
|
+
"git_diff",
|
|
22
|
+
"git_log",
|
|
20
23
|
]);
|
|
21
24
|
const REPLAY_BUILTIN_FILESYSTEM_TOOL_NAMES = new Set([
|
|
22
25
|
"read",
|
|
@@ -24,7 +27,13 @@ const REPLAY_BUILTIN_FILESYSTEM_TOOL_NAMES = new Set([
|
|
|
24
27
|
"find",
|
|
25
28
|
"ls",
|
|
26
29
|
]);
|
|
27
|
-
const REPLAY_PACKAGE_FILESYSTEM_TOOL_NAMES = new Set([
|
|
30
|
+
const REPLAY_PACKAGE_FILESYSTEM_TOOL_NAMES = new Set([
|
|
31
|
+
"fd",
|
|
32
|
+
"rg",
|
|
33
|
+
"git_show",
|
|
34
|
+
"git_diff",
|
|
35
|
+
"git_log",
|
|
36
|
+
]);
|
|
28
37
|
const GIT_OUTPUT_LIMIT = 32 * 1024 * 1024;
|
|
29
38
|
const REPLAY_IDENTITY_TIMEOUT_MS = 5_000;
|
|
30
39
|
const REPLAY_RESOURCE_FILE_LIMIT = 8 * 1024 * 1024;
|
|
@@ -179,12 +188,13 @@ function hasKnownReplayFilesystemImplementation(tool: {
|
|
|
179
188
|
);
|
|
180
189
|
}
|
|
181
190
|
if (!REPLAY_PACKAGE_FILESYSTEM_TOOL_NAMES.has(tool.name)) return false;
|
|
191
|
+
// fd/rg come from file-search; the read-only git tools from git-read.
|
|
192
|
+
const ownerSourceSuffix = tool.name.startsWith("git_")
|
|
193
|
+
? "/extensions/git-read/index.ts"
|
|
194
|
+
: "/extensions/file-search/index.ts";
|
|
182
195
|
return (
|
|
183
196
|
tool.sourceInfo?.origin === "package" &&
|
|
184
|
-
tool.sourceInfo.path
|
|
185
|
-
.split(path.sep)
|
|
186
|
-
.join("/")
|
|
187
|
-
.endsWith("/extensions/file-search/index.ts")
|
|
197
|
+
tool.sourceInfo.path.split(path.sep).join("/").endsWith(ownerSourceSuffix)
|
|
188
198
|
);
|
|
189
199
|
}
|
|
190
200
|
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import type { WorkflowDetails } from "./model.ts";
|
|
2
|
+
|
|
3
|
+
export interface WorkflowCompletionEnvelope {
|
|
4
|
+
deliveryId: string;
|
|
5
|
+
runId: string;
|
|
6
|
+
details: WorkflowDetails;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface WorkflowDeliveryReceipt {
|
|
10
|
+
deliveryId: string;
|
|
11
|
+
delivered: boolean;
|
|
12
|
+
error?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface WorkflowResultDeliveryOptions {
|
|
16
|
+
isIdle: () => boolean;
|
|
17
|
+
persist: (details: WorkflowDetails) => void;
|
|
18
|
+
deliver: (
|
|
19
|
+
envelopes: readonly WorkflowCompletionEnvelope[],
|
|
20
|
+
wake: boolean,
|
|
21
|
+
) => Promise<readonly WorkflowDeliveryReceipt[]>;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function errorText(error: unknown) {
|
|
25
|
+
return error instanceof Error ? error.message : String(error);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Durable, per-run workflow completion delivery.
|
|
30
|
+
*
|
|
31
|
+
* Execution status remains authoritative in WorkflowDetails. This module owns
|
|
32
|
+
* only the orthogonal delivery plane. A transport batch is an optimization:
|
|
33
|
+
* every run keeps its own stable delivery id and receipt so partial success
|
|
34
|
+
* can be retried without duplicating siblings.
|
|
35
|
+
*/
|
|
36
|
+
export function createWorkflowResultDelivery(
|
|
37
|
+
options: WorkflowResultDeliveryOptions,
|
|
38
|
+
) {
|
|
39
|
+
const pending = new Map<string, WorkflowCompletionEnvelope>();
|
|
40
|
+
let flushing: Promise<void> | undefined;
|
|
41
|
+
|
|
42
|
+
const persistState = (
|
|
43
|
+
details: WorkflowDetails,
|
|
44
|
+
state: NonNullable<WorkflowDetails["delivery"]>["state"],
|
|
45
|
+
patch: Partial<NonNullable<WorkflowDetails["delivery"]>> = {},
|
|
46
|
+
) => {
|
|
47
|
+
const delivery = details.delivery;
|
|
48
|
+
if (!delivery) throw new Error("Workflow delivery identity is missing");
|
|
49
|
+
const next = {
|
|
50
|
+
...delivery,
|
|
51
|
+
...patch,
|
|
52
|
+
state,
|
|
53
|
+
updatedAt: Date.now(),
|
|
54
|
+
};
|
|
55
|
+
if (patch.lastError === undefined) delete next.lastError;
|
|
56
|
+
details.delivery = next;
|
|
57
|
+
options.persist(details);
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const enqueue = (envelope: WorkflowCompletionEnvelope) => {
|
|
61
|
+
pending.set(envelope.deliveryId, envelope);
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const flush = async (wake: boolean) => {
|
|
65
|
+
if (flushing) return flushing;
|
|
66
|
+
if (pending.size === 0) return;
|
|
67
|
+
const envelopes = [...pending.values()];
|
|
68
|
+
for (const envelope of envelopes) pending.delete(envelope.deliveryId);
|
|
69
|
+
|
|
70
|
+
flushing = (async () => {
|
|
71
|
+
let receipts: readonly WorkflowDeliveryReceipt[];
|
|
72
|
+
try {
|
|
73
|
+
receipts = await options.deliver(envelopes, wake);
|
|
74
|
+
} catch (error) {
|
|
75
|
+
const message = errorText(error);
|
|
76
|
+
for (const envelope of envelopes) {
|
|
77
|
+
enqueue(envelope);
|
|
78
|
+
persistState(envelope.details, "pending", {
|
|
79
|
+
attempts: (envelope.details.delivery?.attempts ?? 0) + 1,
|
|
80
|
+
lastError: message,
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const byId = new Map(
|
|
87
|
+
receipts.map((receipt) => [receipt.deliveryId, receipt] as const),
|
|
88
|
+
);
|
|
89
|
+
for (const envelope of envelopes) {
|
|
90
|
+
const receipt = byId.get(envelope.deliveryId);
|
|
91
|
+
if (receipt?.delivered) {
|
|
92
|
+
try {
|
|
93
|
+
persistState(envelope.details, "delivered", {
|
|
94
|
+
attempts: (envelope.details.delivery?.attempts ?? 0) + 1,
|
|
95
|
+
deliveredAt: Date.now(),
|
|
96
|
+
lastError: undefined,
|
|
97
|
+
});
|
|
98
|
+
} catch (error) {
|
|
99
|
+
// The transport already accepted the message, but the durable
|
|
100
|
+
// receipt did not commit. Retain it for at-least-once recovery;
|
|
101
|
+
// the visible stable id lets the parent recognize a rare replay.
|
|
102
|
+
enqueue(envelope);
|
|
103
|
+
const delivery = envelope.details.delivery;
|
|
104
|
+
if (delivery) {
|
|
105
|
+
envelope.details.delivery = {
|
|
106
|
+
...delivery,
|
|
107
|
+
state: "pending",
|
|
108
|
+
updatedAt: Date.now(),
|
|
109
|
+
lastError: `Delivery receipt persistence failed: ${errorText(error)}`,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
enqueue(envelope);
|
|
116
|
+
persistState(envelope.details, "pending", {
|
|
117
|
+
attempts: (envelope.details.delivery?.attempts ?? 0) + 1,
|
|
118
|
+
lastError:
|
|
119
|
+
receipt?.error ?? "Completion delivery was not acknowledged",
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
})().finally(() => {
|
|
123
|
+
flushing = undefined;
|
|
124
|
+
});
|
|
125
|
+
return flushing;
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
return {
|
|
129
|
+
/** Register inline interest before the run starts. */
|
|
130
|
+
holdInline(details: WorkflowDetails) {
|
|
131
|
+
persistState(details, "held-for-inline");
|
|
132
|
+
},
|
|
133
|
+
|
|
134
|
+
/** Terminal won the wait/abort arbitration and will be returned inline. */
|
|
135
|
+
consumeInline(details: WorkflowDetails) {
|
|
136
|
+
if (details.delivery) pending.delete(details.delivery.id);
|
|
137
|
+
persistState(details, "consumed-inline", {
|
|
138
|
+
deliveredAt: Date.now(),
|
|
139
|
+
lastError: undefined,
|
|
140
|
+
});
|
|
141
|
+
},
|
|
142
|
+
|
|
143
|
+
/** Abort won the wait/terminal arbitration; deliver the result later. */
|
|
144
|
+
releaseInline(envelope: WorkflowCompletionEnvelope) {
|
|
145
|
+
persistState(envelope.details, "pending", { lastError: undefined });
|
|
146
|
+
enqueue(envelope);
|
|
147
|
+
if (options.isIdle()) void flush(true);
|
|
148
|
+
},
|
|
149
|
+
|
|
150
|
+
/** Queue a detached run after terminal status and pending are persisted. */
|
|
151
|
+
defer(envelope: WorkflowCompletionEnvelope) {
|
|
152
|
+
persistState(envelope.details, "pending", { lastError: undefined });
|
|
153
|
+
enqueue(envelope);
|
|
154
|
+
if (options.isIdle()) void flush(true);
|
|
155
|
+
},
|
|
156
|
+
|
|
157
|
+
/** Restore only explicitly pending/held new-format runs. */
|
|
158
|
+
restore(envelope: WorkflowCompletionEnvelope) {
|
|
159
|
+
const state = envelope.details.delivery?.state;
|
|
160
|
+
if (state !== "pending" && state !== "held-for-inline") return false;
|
|
161
|
+
// A process restart cannot still own the inline waiter. Deterministically
|
|
162
|
+
// reconstruct pending delivery from the terminal artifact.
|
|
163
|
+
if (state === "held-for-inline") {
|
|
164
|
+
persistState(envelope.details, "pending", {
|
|
165
|
+
lastError: "Inline waiter was not active after session restart",
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
enqueue(envelope);
|
|
169
|
+
return true;
|
|
170
|
+
},
|
|
171
|
+
|
|
172
|
+
parentSettled() {
|
|
173
|
+
return flush(true);
|
|
174
|
+
},
|
|
175
|
+
|
|
176
|
+
flushIfIdle() {
|
|
177
|
+
if (!options.isIdle()) return Promise.resolve();
|
|
178
|
+
return flush(true) ?? Promise.resolve();
|
|
179
|
+
},
|
|
180
|
+
|
|
181
|
+
size() {
|
|
182
|
+
return pending.size;
|
|
183
|
+
},
|
|
184
|
+
|
|
185
|
+
clear() {
|
|
186
|
+
pending.clear();
|
|
187
|
+
},
|
|
188
|
+
};
|
|
189
|
+
}
|
|
@@ -207,6 +207,11 @@ const BOOTSTRAP = String.raw`
|
|
|
207
207
|
const value = raw && typeof raw === "object" ? raw[key] : undefined;
|
|
208
208
|
return typeof value === "number" && Number.isFinite(value) ? value : 0;
|
|
209
209
|
};
|
|
210
|
+
const readLimit = (key) => {
|
|
211
|
+
const limits = raw && typeof raw === "object" ? raw.limits : undefined;
|
|
212
|
+
const value = limits && typeof limits === "object" ? limits[key] : undefined;
|
|
213
|
+
return typeof value === "number" && Number.isFinite(value) ? value : 0;
|
|
214
|
+
};
|
|
210
215
|
// Every field is coerced, so a missing or malformed snapshot reads as zero
|
|
211
216
|
// rather than undefined: a script doing arithmetic on it gets 0, not NaN.
|
|
212
217
|
return deepFreeze({
|
|
@@ -217,6 +222,12 @@ const BOOTSTRAP = String.raw`
|
|
|
217
222
|
total: read("total"),
|
|
218
223
|
cost: read("cost"),
|
|
219
224
|
agents: read("agents"),
|
|
225
|
+
limits: {
|
|
226
|
+
concurrency: readLimit("concurrency"),
|
|
227
|
+
maxAgentCalls: readLimit("maxAgentCalls"),
|
|
228
|
+
callsUsed: readLimit("callsUsed"),
|
|
229
|
+
callsRemaining: readLimit("callsRemaining"),
|
|
230
|
+
},
|
|
220
231
|
});
|
|
221
232
|
}
|
|
222
233
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tt-a1i/openpi",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.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",
|