@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.
- package/README.md +184 -59
- package/SETUP.md +23 -7
- package/assets/openpi-launch-card-v1.webp +0 -0
- package/bin/openpi.js +145 -0
- package/extensions/ask-user/index.ts +30 -14
- package/extensions/background-terminals/index.ts +30 -2
- package/extensions/background-terminals/src/domain.ts +2 -0
- package/extensions/background-terminals/src/manager.ts +486 -106
- package/extensions/background-terminals/src/output.ts +33 -0
- package/extensions/background-terminals/src/prompt.ts +14 -6
- package/extensions/background-terminals/src/result-delivery.ts +4 -1
- package/extensions/background-terminals/src/ui/ps.ts +132 -129
- package/extensions/capabilities/index.ts +30 -42
- package/extensions/capabilities/src/ui.ts +93 -0
- package/extensions/clear-context/index.ts +83 -0
- package/extensions/context-pivot/index.ts +16 -6
- package/extensions/cron/schedule.ts +7 -1
- package/extensions/file-mutation-display/index.ts +34 -76
- package/extensions/file-mutation-display/render.ts +146 -87
- package/extensions/file-search/index.ts +8 -7
- package/extensions/file-search/src/binaries.ts +75 -59
- package/extensions/git-info/src/changed-files-view.ts +47 -14
- package/extensions/git-read/index.ts +328 -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/model-info/index.ts +21 -33
- package/extensions/model-info/session-metrics.ts +96 -0
- package/extensions/plan-mode/bash-policy.ts +54 -9
- package/extensions/plan-mode/index.ts +7 -2
- package/extensions/post-edit/index.ts +16 -6
- package/extensions/sessions/git-stats.ts +258 -72
- package/extensions/sessions/index.ts +222 -140
- package/extensions/sessions/preview-cache.ts +104 -0
- package/extensions/sessions/preview-loader.ts +856 -0
- package/extensions/sessions/sessions.ts +43 -4
- package/extensions/setup/index.ts +127 -131
- package/extensions/shared/activity-status.ts +36 -5
- package/extensions/shared/agent-session-page.ts +319 -0
- package/extensions/shared/agent-tool-renderer.ts +218 -0
- package/extensions/shared/agent-transcript.ts +524 -0
- package/extensions/shared/below-editor-navigation.ts +26 -0
- package/extensions/shared/capability-intent.ts +53 -0
- package/extensions/shared/child-session.ts +444 -22
- package/extensions/shared/result-budget.ts +134 -0
- package/extensions/shared/result-delivery.ts +34 -0
- package/extensions/shared/screen-chrome.ts +133 -0
- package/extensions/shared/setup-config.ts +97 -38
- package/extensions/shared/setup-episode-state.ts +1 -1
- package/extensions/shared/spinner.ts +28 -0
- package/extensions/shared/terminal-text.ts +110 -23
- package/extensions/shared/text-projection.ts +113 -0
- package/extensions/shared/tool-activity.ts +382 -0
- package/extensions/shared/tool-surface.ts +42 -8
- package/extensions/shared/transcript-viewport.ts +46 -0
- package/extensions/shared/web-observer-registry.ts +390 -0
- package/extensions/shared/worktree.ts +11 -0
- package/extensions/subagents/index.ts +461 -186
- package/extensions/subagents/navigation.ts +86 -28
- package/extensions/subagents/src/agent-types.ts +37 -15
- package/extensions/subagents/src/backend.ts +12 -1
- package/extensions/subagents/src/backends/pi.ts +375 -66
- package/extensions/subagents/src/domain.ts +5 -0
- package/extensions/subagents/src/id-sequence.ts +84 -0
- package/extensions/subagents/src/manager.ts +651 -536
- package/extensions/subagents/src/prompt.ts +185 -42
- package/extensions/subagents/src/result-artifact.ts +146 -0
- package/extensions/subagents/src/result-delivery.ts +7 -1
- package/extensions/subagents/src/runtime.ts +23 -6
- package/extensions/subagents/src/ui/takeover.ts +128 -337
- package/extensions/subagents/src/ui/transcript.ts +38 -501
- package/extensions/subagents/src/ui/wait-result.ts +103 -15
- package/extensions/suggestions/src/ui.ts +10 -4
- package/extensions/tasks/index.ts +0 -3
- package/extensions/tasks/ui.ts +79 -62
- package/extensions/ui-customization/footer.ts +7 -44
- package/extensions/ui-customization/index.ts +0 -4
- package/extensions/user-input-fold/index.ts +185 -0
- package/extensions/web/index.ts +234 -0
- package/extensions/workflows/artifacts.ts +147 -22
- package/extensions/workflows/completion-projection.ts +457 -0
- package/extensions/workflows/controller.ts +14 -2
- package/extensions/workflows/coordinator.ts +62 -0
- package/extensions/workflows/dashboard.ts +458 -339
- package/extensions/workflows/handoff.ts +121 -25
- package/extensions/workflows/index.ts +1042 -492
- package/extensions/workflows/journal.ts +148 -13
- package/extensions/workflows/model.ts +131 -19
- package/extensions/workflows/navigation.ts +61 -18
- package/extensions/workflows/progress-projection.ts +306 -0
- package/extensions/workflows/prompt.ts +166 -10
- package/extensions/workflows/replay-safety.ts +58 -27
- package/extensions/workflows/result-delivery.ts +253 -0
- package/extensions/workflows/retention.ts +593 -0
- package/extensions/workflows/runner.ts +388 -279
- package/extensions/workflows/sandbox-child.cjs +36 -3
- package/extensions/workflows/sandbox.ts +62 -8
- package/extensions/workflows/serialization.ts +325 -17
- package/extensions/workflows/tool-renderer.ts +22 -0
- package/extensions/workflows/transcript.ts +149 -0
- package/extensions/workspace-cleanup-guard/index.ts +54 -0
- package/extensions/workspace-cleanup-guard/workspace-provenance.ts +563 -0
- package/package.json +28 -8
- package/skills/subagents/REFERENCE.md +189 -0
- package/skills/subagents/SKILL.md +2 -2
- package/skills/workflows/REFERENCE.md +10 -5
- package/skills/workflows/SKILL.md +53 -10
- package/web/adapter/pi-adapter.ts +661 -0
- package/web/host/browser-launcher.ts +20 -0
- package/web/host/static-assets.ts +4 -0
- package/web/host/terminal-status.ts +38 -0
- package/web/host/web-host.ts +789 -0
- package/web/http-dispatcher.ts +125 -0
- package/web/protocol/types.ts +462 -0
- package/web/runtime/pi-runtime.ts +991 -0
- package/web/runtime/types.ts +71 -0
- package/web/runtime/web-host-lease.ts +497 -0
- package/web/trace.ts +18 -0
- package/web/ui/app.js +1398 -0
- package/web/ui/index.html +139 -0
- package/web/ui/styles.css +598 -0
- package/web/vite.config.mjs +34 -0
- package/extensions/execution-convergence/active-evidence.ts +0 -129
- package/extensions/execution-convergence/index.ts +0 -442
- package/extensions/execution-convergence/workspace-provenance.ts +0 -338
- package/extensions/setup/intercom-fs-helper.cjs +0 -130
- package/extensions/setup/intercom.ts +0 -603
- package/extensions/subagents/src/backends/stub.ts +0 -296
- package/extensions/subagents/src/format.ts +0 -48
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
import {
|
|
2
|
+
calculateContextTokens,
|
|
3
|
+
type AgentSession,
|
|
4
|
+
estimateTokens,
|
|
5
|
+
} from "@earendil-works/pi-coding-agent";
|
|
6
|
+
import { type AgentUsage, emptyUsage, type TranscriptEntry } from "./model.ts";
|
|
7
|
+
import { safeStringify, truncateUtf8 } from "./serialization.ts";
|
|
8
|
+
|
|
9
|
+
const TRANSCRIPT_ENTRY_MAX_BYTES = 16 * 1024;
|
|
10
|
+
const TRANSCRIPT_TOTAL_MAX_BYTES = 256 * 1024;
|
|
11
|
+
const TRANSCRIPT_MAX_ENTRIES = 200;
|
|
12
|
+
|
|
13
|
+
type AgentMessage = AgentSession["messages"][number];
|
|
14
|
+
export type ProgressAssistantMessage = Extract<
|
|
15
|
+
AgentMessage,
|
|
16
|
+
{ role: "assistant" }
|
|
17
|
+
>;
|
|
18
|
+
|
|
19
|
+
export interface ProgressToolTiming {
|
|
20
|
+
startedAt?: number;
|
|
21
|
+
finishedAt?: number;
|
|
22
|
+
durationMs?: number;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
type ContextTokens = number | null | undefined;
|
|
26
|
+
|
|
27
|
+
interface ProjectedTranscriptEntry {
|
|
28
|
+
role: TranscriptEntry["role"];
|
|
29
|
+
text: string;
|
|
30
|
+
sourceTextTruncated: boolean;
|
|
31
|
+
name?: string;
|
|
32
|
+
sourceToolCallId?: string;
|
|
33
|
+
isError?: boolean;
|
|
34
|
+
timestamp?: number;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface AgentProgressProjectionSnapshot {
|
|
38
|
+
preview: string;
|
|
39
|
+
usage: AgentUsage;
|
|
40
|
+
latestAssistant?: ProgressAssistantMessage;
|
|
41
|
+
transcript: TranscriptEntry[];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function validAssistantContextTokens(message: ProgressAssistantMessage) {
|
|
45
|
+
if (
|
|
46
|
+
message.stopReason === "aborted" ||
|
|
47
|
+
message.stopReason === "error" ||
|
|
48
|
+
!message.usage
|
|
49
|
+
) {
|
|
50
|
+
return undefined;
|
|
51
|
+
}
|
|
52
|
+
const tokens = calculateContextTokens(message.usage);
|
|
53
|
+
return tokens > 0 ? tokens : undefined;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function boundedSourceText(text: string) {
|
|
57
|
+
const bounded = truncateUtf8(text, TRANSCRIPT_ENTRY_MAX_BYTES);
|
|
58
|
+
return { text: bounded, sourceTextTruncated: bounded !== text };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function safeJson(value: unknown) {
|
|
62
|
+
return safeStringify(value, {
|
|
63
|
+
maxBytes: TRANSCRIPT_ENTRY_MAX_BYTES,
|
|
64
|
+
maxDepth: 12,
|
|
65
|
+
maxNodes: 2_000,
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function messageEntries(message: AgentMessage): ProjectedTranscriptEntry[] {
|
|
70
|
+
if (message.role === "user") {
|
|
71
|
+
const source =
|
|
72
|
+
typeof message.content === "string"
|
|
73
|
+
? message.content
|
|
74
|
+
: message.content
|
|
75
|
+
.map((part) =>
|
|
76
|
+
part.type === "text" ? part.text : `[image: ${part.mimeType}]`,
|
|
77
|
+
)
|
|
78
|
+
.join("\n");
|
|
79
|
+
if (!source.trim()) return [];
|
|
80
|
+
return [
|
|
81
|
+
{
|
|
82
|
+
role: "user",
|
|
83
|
+
...boundedSourceText(source),
|
|
84
|
+
timestamp: message.timestamp,
|
|
85
|
+
},
|
|
86
|
+
];
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (message.role === "assistant") {
|
|
90
|
+
const entries: ProjectedTranscriptEntry[] = [];
|
|
91
|
+
for (const part of message.content) {
|
|
92
|
+
if (part.type === "text" && part.text.trim()) {
|
|
93
|
+
entries.push({
|
|
94
|
+
role: "assistant",
|
|
95
|
+
...boundedSourceText(part.text),
|
|
96
|
+
timestamp: message.timestamp,
|
|
97
|
+
});
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
if (part.type === "thinking" && part.thinking.trim()) {
|
|
101
|
+
entries.push({
|
|
102
|
+
role: "thinking",
|
|
103
|
+
...boundedSourceText(part.thinking),
|
|
104
|
+
timestamp: message.timestamp,
|
|
105
|
+
});
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
if (part.type === "toolCall") {
|
|
109
|
+
entries.push({
|
|
110
|
+
role: "tool",
|
|
111
|
+
...boundedSourceText(safeJson(part.arguments)),
|
|
112
|
+
name: part.name,
|
|
113
|
+
sourceToolCallId: part.id,
|
|
114
|
+
timestamp: message.timestamp,
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return entries;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (message.role !== "toolResult") return [];
|
|
122
|
+
const source = message.content
|
|
123
|
+
.map((part) =>
|
|
124
|
+
part.type === "text" ? part.text : `[image: ${part.mimeType}]`,
|
|
125
|
+
)
|
|
126
|
+
.join("\n");
|
|
127
|
+
return [
|
|
128
|
+
{
|
|
129
|
+
role: "toolResult",
|
|
130
|
+
...boundedSourceText(source),
|
|
131
|
+
name: message.toolName,
|
|
132
|
+
sourceToolCallId: message.toolCallId,
|
|
133
|
+
isError: message.isError,
|
|
134
|
+
timestamp: message.timestamp,
|
|
135
|
+
},
|
|
136
|
+
];
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function assistantText(message: ProgressAssistantMessage) {
|
|
140
|
+
return message.content
|
|
141
|
+
.filter((part) => part.type === "text")
|
|
142
|
+
.map((part) => part.text)
|
|
143
|
+
.join("\n")
|
|
144
|
+
.trim();
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function addAssistantUsage(
|
|
148
|
+
usage: AgentUsage,
|
|
149
|
+
message: ProgressAssistantMessage,
|
|
150
|
+
) {
|
|
151
|
+
usage.turns++;
|
|
152
|
+
const current = message.usage;
|
|
153
|
+
if (!current) return;
|
|
154
|
+
usage.input += current.input || 0;
|
|
155
|
+
usage.output += current.output || 0;
|
|
156
|
+
usage.cacheRead += current.cacheRead || 0;
|
|
157
|
+
usage.cacheWrite += current.cacheWrite || 0;
|
|
158
|
+
usage.cost += current.cost?.total || 0;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function toolMetadata(
|
|
162
|
+
toolCallId: string,
|
|
163
|
+
timings: ReadonlyMap<string, ProgressToolTiming>,
|
|
164
|
+
) {
|
|
165
|
+
const timing = timings.get(toolCallId);
|
|
166
|
+
return {
|
|
167
|
+
toolCallId: truncateUtf8(toolCallId, 1024),
|
|
168
|
+
...(timing?.startedAt === undefined ? {} : { startedAt: timing.startedAt }),
|
|
169
|
+
...(timing?.finishedAt === undefined
|
|
170
|
+
? {}
|
|
171
|
+
: { finishedAt: timing.finishedAt }),
|
|
172
|
+
...(timing?.durationMs === undefined
|
|
173
|
+
? {}
|
|
174
|
+
: { durationMs: timing.durationMs }),
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Bounded, ephemeral projection of Pi's active child messages.
|
|
180
|
+
*
|
|
181
|
+
* Pi remains canonical. Normal finalized messages are folded once; startup,
|
|
182
|
+
* compaction, and terminal settlement replace this state from Pi's messages.
|
|
183
|
+
*/
|
|
184
|
+
export class AgentProgressProjection {
|
|
185
|
+
private usage = emptyUsage();
|
|
186
|
+
private preview = "";
|
|
187
|
+
private latestAssistant?: ProgressAssistantMessage;
|
|
188
|
+
private firstEntry?: ProjectedTranscriptEntry;
|
|
189
|
+
private tailEntries: ProjectedTranscriptEntry[] = [];
|
|
190
|
+
private totalEntries = 0;
|
|
191
|
+
private contextTokens: ContextTokens;
|
|
192
|
+
|
|
193
|
+
constructor(
|
|
194
|
+
messages: readonly AgentMessage[] = [],
|
|
195
|
+
contextTokens?: ContextTokens,
|
|
196
|
+
) {
|
|
197
|
+
this.replace(messages, contextTokens);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
replace(messages: readonly AgentMessage[], contextTokens?: ContextTokens) {
|
|
201
|
+
this.usage = emptyUsage();
|
|
202
|
+
this.preview = "";
|
|
203
|
+
this.latestAssistant = undefined;
|
|
204
|
+
this.firstEntry = undefined;
|
|
205
|
+
this.tailEntries = [];
|
|
206
|
+
this.totalEntries = 0;
|
|
207
|
+
this.contextTokens = contextTokens;
|
|
208
|
+
for (const message of messages) this.ingest(message, false);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
append(message: AgentMessage) {
|
|
212
|
+
this.ingest(message, true);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
private ingest(message: AgentMessage, updateContext: boolean) {
|
|
216
|
+
if (message.role === "assistant") {
|
|
217
|
+
addAssistantUsage(this.usage, message);
|
|
218
|
+
this.latestAssistant = message;
|
|
219
|
+
const text = assistantText(message);
|
|
220
|
+
if (text) this.preview = text;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
if (updateContext && this.contextTokens !== undefined) {
|
|
224
|
+
const assistantTokens =
|
|
225
|
+
message.role === "assistant"
|
|
226
|
+
? validAssistantContextTokens(message)
|
|
227
|
+
: undefined;
|
|
228
|
+
if (assistantTokens !== undefined) {
|
|
229
|
+
this.contextTokens = assistantTokens;
|
|
230
|
+
} else if (this.contextTokens !== null) {
|
|
231
|
+
this.contextTokens += estimateTokens(message);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
for (const entry of messageEntries(message)) this.pushEntry(entry);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
private pushEntry(entry: ProjectedTranscriptEntry) {
|
|
239
|
+
this.totalEntries++;
|
|
240
|
+
if (!this.firstEntry) {
|
|
241
|
+
this.firstEntry = entry;
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
this.tailEntries.push(entry);
|
|
245
|
+
if (this.tailEntries.length >= TRANSCRIPT_MAX_ENTRIES) {
|
|
246
|
+
this.tailEntries.shift();
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
snapshot(
|
|
251
|
+
toolTimings: ReadonlyMap<string, ProgressToolTiming> = new Map(),
|
|
252
|
+
): AgentProgressProjectionSnapshot {
|
|
253
|
+
const selected = this.firstEntry
|
|
254
|
+
? [this.firstEntry, ...this.tailEntries]
|
|
255
|
+
: [];
|
|
256
|
+
const transcript: TranscriptEntry[] = [];
|
|
257
|
+
let totalBytes = 0;
|
|
258
|
+
for (const entry of selected) {
|
|
259
|
+
const remaining = TRANSCRIPT_TOTAL_MAX_BYTES - totalBytes;
|
|
260
|
+
if (remaining <= 0) break;
|
|
261
|
+
const text = truncateUtf8(
|
|
262
|
+
entry.text,
|
|
263
|
+
Math.min(TRANSCRIPT_ENTRY_MAX_BYTES, remaining),
|
|
264
|
+
);
|
|
265
|
+
totalBytes += Buffer.byteLength(text, "utf8");
|
|
266
|
+
const truncated = entry.sourceTextTruncated || text !== entry.text;
|
|
267
|
+
transcript.push({
|
|
268
|
+
role: entry.role,
|
|
269
|
+
text: truncated ? `${text}\n[transcript entry truncated]` : text,
|
|
270
|
+
...(entry.name === undefined ? {} : { name: entry.name }),
|
|
271
|
+
...(entry.sourceToolCallId === undefined
|
|
272
|
+
? {}
|
|
273
|
+
: toolMetadata(entry.sourceToolCallId, toolTimings)),
|
|
274
|
+
...(entry.isError === undefined ? {} : { isError: entry.isError }),
|
|
275
|
+
...(entry.timestamp === undefined
|
|
276
|
+
? {}
|
|
277
|
+
: { timestamp: entry.timestamp }),
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
if (transcript.length < this.totalEntries) {
|
|
281
|
+
transcript.push({
|
|
282
|
+
role: "toolResult",
|
|
283
|
+
name: "transcript",
|
|
284
|
+
text: `[transcript truncated: retained ${transcript.length} of ${this.totalEntries} entries]`,
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
return {
|
|
288
|
+
preview: this.preview,
|
|
289
|
+
usage: {
|
|
290
|
+
...this.usage,
|
|
291
|
+
...(typeof this.contextTokens === "number"
|
|
292
|
+
? { contextTokens: this.contextTokens }
|
|
293
|
+
: {}),
|
|
294
|
+
},
|
|
295
|
+
latestAssistant: this.latestAssistant,
|
|
296
|
+
transcript,
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
export function transcriptFromMessages(
|
|
302
|
+
messages: AgentMessage[],
|
|
303
|
+
toolTimings: ReadonlyMap<string, ProgressToolTiming> = new Map(),
|
|
304
|
+
) {
|
|
305
|
+
return new AgentProgressProjection(messages).snapshot(toolTimings).transcript;
|
|
306
|
+
}
|
|
@@ -1,4 +1,9 @@
|
|
|
1
|
+
import {
|
|
2
|
+
allocateResultBudgets,
|
|
3
|
+
type ParentContextUsage,
|
|
4
|
+
} from "../shared/result-budget.ts";
|
|
1
5
|
import { sanitizeTerminalText } from "../shared/terminal-text.ts";
|
|
6
|
+
import { projectText } from "../shared/text-projection.ts";
|
|
2
7
|
import {
|
|
3
8
|
countStates,
|
|
4
9
|
formatElapsed,
|
|
@@ -7,20 +12,21 @@ 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 for published callers only; new calls must use wait. Replace true with wait=false and false with wait=true. Do not provide both fields. The alias will be removed in the next announced breaking release.",
|
|
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
|
};
|
|
20
26
|
|
|
21
|
-
/** Describes stopping a running
|
|
27
|
+
/** Describes stopping a running workflow, mirroring subagent_cancel/bg_kill. */
|
|
22
28
|
export const WORKFLOW_STOP_TOOL_DESCRIPTION =
|
|
23
|
-
"Cancel a running
|
|
29
|
+
"Cancel a running workflow by its run id (from the workflow launch result). This aborts its remaining agents and settles the run; partial results and artifacts are preserved.";
|
|
24
30
|
|
|
25
31
|
/** Model-facing schema description for the workflow run id to stop. */
|
|
26
32
|
export const WORKFLOW_STOP_PARAMETER_DESCRIPTIONS = {
|
|
@@ -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,107 @@ 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 deliveryFacts = JSON.stringify(
|
|
218
|
+
entries.map(({ deliveryId, details, runDir }) => ({
|
|
219
|
+
deliveryId,
|
|
220
|
+
runId: details.runId,
|
|
221
|
+
evidence: shortenHome(runDir),
|
|
222
|
+
})),
|
|
223
|
+
);
|
|
224
|
+
const manifest =
|
|
225
|
+
"Workflow completion delivery facts (stable across retries; deduplicate by deliveryId):\n" +
|
|
226
|
+
deliveryFacts;
|
|
227
|
+
const full = entries.map(({ deliveryId, details, runDir }) =>
|
|
228
|
+
buildBackgroundWorkflowFollowUp({
|
|
229
|
+
runId: details.runId,
|
|
230
|
+
name: details.name,
|
|
231
|
+
status: details.status,
|
|
232
|
+
result: `${buildWorkflowResultMessage(details, runDir)}\n\nDelivery id: ${deliveryId}`,
|
|
233
|
+
}),
|
|
234
|
+
);
|
|
235
|
+
const separatorBytes = Math.max(0, entries.length - 1) * 2;
|
|
236
|
+
const manifestSeparatorBytes = entries.length > 0 ? 2 : 0;
|
|
237
|
+
const fixedBytes =
|
|
238
|
+
Buffer.byteLength(manifest, "utf8") +
|
|
239
|
+
separatorBytes +
|
|
240
|
+
manifestSeparatorBytes;
|
|
241
|
+
const bodyBudget = 48 * 1024 - fixedBytes;
|
|
242
|
+
if (bodyBudget < 0) {
|
|
243
|
+
throw new Error(
|
|
244
|
+
"Workflow completion delivery facts exceed the transport payload limit",
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
const allocation = allocateResultBudgets(
|
|
248
|
+
full.map((message) => Buffer.byteLength(message, "utf8")),
|
|
249
|
+
usage,
|
|
250
|
+
{
|
|
251
|
+
maxBatchBytes: bodyBudget,
|
|
252
|
+
maxResultBytes: 48 * 1024,
|
|
253
|
+
minResultBytes: 1024,
|
|
254
|
+
headroomShare: 0.25,
|
|
255
|
+
estimatedBytesPerToken: 4,
|
|
256
|
+
fixedBytes,
|
|
257
|
+
},
|
|
258
|
+
);
|
|
259
|
+
const projected = full
|
|
260
|
+
.map((message, index) =>
|
|
261
|
+
projectText(message, {
|
|
262
|
+
maxBytes: allocation.budgets[index] ?? 1024,
|
|
263
|
+
maxLines: 400,
|
|
264
|
+
recovery: `Full workflow evidence is available in ${shortenHome(entries[index]!.runDir)}; duplicate deliveries carry the same delivery id.`,
|
|
265
|
+
}),
|
|
266
|
+
)
|
|
267
|
+
.join("\n\n");
|
|
268
|
+
return projected ? `${manifest}\n\n${projected}` : manifest;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/** Split transport batches so every manifest and projected body fit together. */
|
|
272
|
+
export function buildProjectedWorkflowCompletionBatches(
|
|
273
|
+
entries: readonly {
|
|
274
|
+
deliveryId: string;
|
|
275
|
+
details: WorkflowDetails;
|
|
276
|
+
runDir: string;
|
|
277
|
+
}[],
|
|
278
|
+
usage?: ParentContextUsage | null,
|
|
279
|
+
) {
|
|
280
|
+
const batches: Array<{
|
|
281
|
+
entries: (typeof entries)[number][];
|
|
282
|
+
content: string;
|
|
283
|
+
}> = [];
|
|
284
|
+
let current: (typeof entries)[number][] = [];
|
|
285
|
+
for (const entry of entries) {
|
|
286
|
+
const candidate = [...current, entry];
|
|
287
|
+
try {
|
|
288
|
+
buildProjectedWorkflowCompletionBatch(candidate, usage);
|
|
289
|
+
current = candidate;
|
|
290
|
+
} catch (error) {
|
|
291
|
+
if (current.length === 0) throw error;
|
|
292
|
+
batches.push({
|
|
293
|
+
entries: current,
|
|
294
|
+
content: buildProjectedWorkflowCompletionBatch(current, usage),
|
|
295
|
+
});
|
|
296
|
+
current = [entry];
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
if (current.length > 0) {
|
|
300
|
+
batches.push({
|
|
301
|
+
entries: current,
|
|
302
|
+
content: buildProjectedWorkflowCompletionBatch(current, usage),
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
return batches;
|
|
171
306
|
}
|
|
172
307
|
|
|
173
308
|
/** Builds the background-launch result and tells the parent model how to inspect or stop the run. */
|
|
@@ -182,3 +317,24 @@ export function buildBackgroundWorkflowLaunchResult(options: {
|
|
|
182
317
|
`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
318
|
].join("\n");
|
|
184
319
|
}
|
|
320
|
+
|
|
321
|
+
/** Pure observation: bounded status/coverage without replaying final output. */
|
|
322
|
+
export function buildWorkflowStatusSummary(
|
|
323
|
+
details: WorkflowDetails,
|
|
324
|
+
runDir: string,
|
|
325
|
+
) {
|
|
326
|
+
const { done, failed, uncertain } = countStates(details);
|
|
327
|
+
const settled = done + failed;
|
|
328
|
+
return [
|
|
329
|
+
`Workflow ${details.name ? `"${details.name}"` : details.runId} ${details.status}.`,
|
|
330
|
+
details.status === "uncertain"
|
|
331
|
+
? "Recovery warning: the prior owner disappeared without terminal evidence; some external effects may have occurred."
|
|
332
|
+
: undefined,
|
|
333
|
+
`Coverage: ${settled}/${details.agents.length} agents settled (${done} ok, ${failed} failed)${uncertain ? `; ${uncertain} uncertain` : ""}.`,
|
|
334
|
+
details.currentPhase ? `Current phase: ${details.currentPhase}` : undefined,
|
|
335
|
+
details.delivery ? `Delivery: ${details.delivery.state}.` : undefined,
|
|
336
|
+
`Artifacts: ${shortenHome(runDir)}`,
|
|
337
|
+
]
|
|
338
|
+
.filter(Boolean)
|
|
339
|
+
.join("\n");
|
|
340
|
+
}
|
|
@@ -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
|
|
|
@@ -413,13 +423,44 @@ function canonicalPath(value: string) {
|
|
|
413
423
|
return fs.realpathSync.native(value);
|
|
414
424
|
}
|
|
415
425
|
|
|
416
|
-
function
|
|
426
|
+
function deadlineBoundGitRunner() {
|
|
427
|
+
// One budget per fingerprint call: every git invocation shares a single
|
|
428
|
+
// deadline.
|
|
417
429
|
const deadline = Date.now() + REPLAY_IDENTITY_TIMEOUT_MS;
|
|
418
|
-
|
|
430
|
+
return (gitCwd: string, args: readonly string[]) =>
|
|
419
431
|
boundedGit(gitCwd, args, deadline);
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
export function repositoryFingerprint(
|
|
435
|
+
cwd: string,
|
|
436
|
+
runGit: (
|
|
437
|
+
gitCwd: string,
|
|
438
|
+
args: readonly string[],
|
|
439
|
+
) => string = deadlineBoundGitRunner(),
|
|
440
|
+
) {
|
|
441
|
+
const root = canonicalPath(
|
|
442
|
+
runGit(cwd, ["rev-parse", "--show-toplevel"]).trim(),
|
|
443
|
+
);
|
|
444
|
+
const assertNoIgnoredFiles = () => {
|
|
445
|
+
// Ignored files are observable to a read-capable child but are commonly
|
|
446
|
+
// too large or secret-bearing to hash (node_modules, .env, build output).
|
|
447
|
+
// A complete identity cannot pretend they do not exist, so disable replay.
|
|
448
|
+
const ignoredOutput = runGit(root, [
|
|
449
|
+
"ls-files",
|
|
450
|
+
"--others",
|
|
451
|
+
"--ignored",
|
|
452
|
+
"--exclude-standard",
|
|
453
|
+
"--directory",
|
|
454
|
+
"-z",
|
|
455
|
+
]);
|
|
456
|
+
if (ignoredOutput.length > 0) {
|
|
457
|
+
throw new Error("ignored files make the replay identity incomplete");
|
|
458
|
+
}
|
|
459
|
+
};
|
|
460
|
+
// The common disqualifying case exits before the expensive binary diff.
|
|
461
|
+
assertNoIgnoredFiles();
|
|
462
|
+
const head = runGit(root, ["rev-parse", "--verify", "HEAD"]).trim();
|
|
463
|
+
const diff = runGit(root, [
|
|
423
464
|
"diff",
|
|
424
465
|
"--no-ext-diff",
|
|
425
466
|
"--no-textconv",
|
|
@@ -427,7 +468,7 @@ function repositoryFingerprint(cwd: string) {
|
|
|
427
468
|
"HEAD",
|
|
428
469
|
"--",
|
|
429
470
|
]);
|
|
430
|
-
const trackedModes =
|
|
471
|
+
const trackedModes = runGit(root, ["ls-files", "-s", "-z"]);
|
|
431
472
|
// Tracked symlinks can expose changing content outside Git, while gitlinks
|
|
432
473
|
// can expose submodule state absent from the parent diff. Neither has a
|
|
433
474
|
// complete bounded identity here, so execute instead of replaying.
|
|
@@ -440,21 +481,11 @@ function repositoryFingerprint(cwd: string) {
|
|
|
440
481
|
) {
|
|
441
482
|
throw new Error("symlinks and gitlinks make replay identity incomplete");
|
|
442
483
|
}
|
|
443
|
-
//
|
|
444
|
-
//
|
|
445
|
-
//
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
"--others",
|
|
449
|
-
"--ignored",
|
|
450
|
-
"--exclude-standard",
|
|
451
|
-
"--directory",
|
|
452
|
-
"-z",
|
|
453
|
-
]);
|
|
454
|
-
if (ignoredOutput.length > 0) {
|
|
455
|
-
throw new Error("ignored files make the replay identity incomplete");
|
|
456
|
-
}
|
|
457
|
-
const untrackedOutput = git(root, [
|
|
484
|
+
// Preserve the original post-diff safety observation: an ignored file can
|
|
485
|
+
// appear while the synchronous diff is running, including from another
|
|
486
|
+
// process. The early check is only an optimization, never the final gate.
|
|
487
|
+
assertNoIgnoredFiles();
|
|
488
|
+
const untrackedOutput = runGit(root, [
|
|
458
489
|
"ls-files",
|
|
459
490
|
"--others",
|
|
460
491
|
"--exclude-standard",
|