@tt-a1i/openpi 0.1.1 → 0.2.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 (43) hide show
  1. package/README.md +37 -22
  2. package/SETUP.md +8 -6
  3. package/extensions/ask-user/handoff.ts +5 -1
  4. package/extensions/ask-user/index.ts +44 -0
  5. package/extensions/background-terminals/index.ts +118 -29
  6. package/extensions/background-terminals/src/domain.ts +5 -1
  7. package/extensions/background-terminals/src/manager.ts +2 -1
  8. package/extensions/background-terminals/src/prompt.ts +35 -0
  9. package/extensions/background-terminals/src/result-delivery.ts +76 -3
  10. package/extensions/background-terminals/src/ui/tool-result.ts +52 -1
  11. package/extensions/capabilities/index.ts +198 -0
  12. package/extensions/context-pivot/index.ts +21 -0
  13. package/extensions/cron/index.ts +42 -15
  14. package/extensions/execution-convergence/active-evidence.ts +129 -0
  15. package/extensions/execution-convergence/index.ts +442 -0
  16. package/extensions/execution-convergence/workspace-provenance.ts +338 -0
  17. package/extensions/file-search/index.ts +8 -1
  18. package/extensions/file-search/src/binaries.ts +2 -1
  19. package/extensions/git-info/src/runtime.ts +1 -1
  20. package/extensions/goal/controller.ts +2 -1
  21. package/extensions/goal/index.ts +20 -1
  22. package/extensions/plan-mode/index.ts +12 -0
  23. package/extensions/setup/index.ts +93 -7
  24. package/extensions/shared/child-session.ts +40 -4
  25. package/extensions/shared/setup-config.ts +22 -0
  26. package/extensions/shared/setup-episode-state.ts +7 -0
  27. package/extensions/shared/tool-surface.ts +435 -0
  28. package/extensions/subagents/index.ts +15 -0
  29. package/extensions/subagents/src/manager.ts +13 -11
  30. package/extensions/subagents/src/prompt.ts +1 -1
  31. package/extensions/tasks/index.ts +39 -12
  32. package/extensions/ui-customization/footer.ts +6 -1
  33. package/extensions/workflows/graph-projection.ts +6 -4
  34. package/extensions/workflows/index.ts +16 -1
  35. package/extensions/workflows/invocation-ledger.ts +8 -2
  36. package/extensions/workflows/model.ts +5 -1
  37. package/extensions/workflows/prompt.ts +10 -40
  38. package/extensions/workflows/replay-safety.ts +9 -8
  39. package/package.json +10 -10
  40. package/skills/subagents/SKILL.md +6 -0
  41. package/skills/workflows/EXAMPLES.md +58 -0
  42. package/skills/workflows/REFERENCE.md +44 -0
  43. package/skills/workflows/SKILL.md +39 -0
@@ -0,0 +1,442 @@
1
+ import { createHash } from "node:crypto";
2
+ import {
3
+ type ExtensionAPI,
4
+ isToolCallEventType,
5
+ } from "@earendil-works/pi-coding-agent";
6
+ import {
7
+ DELAYED_ACTIVE_EVIDENCE_POLICY,
8
+ projectActiveEvidence,
9
+ } from "./active-evidence.ts";
10
+ import { createWorkspaceCleanupGuard } from "./workspace-provenance.ts";
11
+
12
+ const MAX_BASH_RESULT_CHARS = 8_192;
13
+ const BASH_RESULT_HEAD_CHARS = 1_024;
14
+ const BASH_RESULT_TAIL_CHARS = 6_144;
15
+ const DEFAULT_VALIDATION_TIMEOUT_SECONDS = 180;
16
+ const RETRY_VALIDATION_TIMEOUT_SECONDS = 60;
17
+ const FAILURE_RECOVERY_WINDOW = 8;
18
+ const FAILURE_RECOVERY_THRESHOLD = 3;
19
+ const FAILURE_RECOVERY_RESET_SUCCESSES = 3;
20
+ const FAILURE_RECOVERY_CHECKPOINT =
21
+ "[OpenPI recovery checkpoint: 3 of the last 8 tool attempts failed. Re-establish the authoritative current state with a read or listing before another mutation. Prefer workspace-relative paths over retyping temporary absolute paths. Do not delete files unless you verified this session created them or the task requires removal. Drop speculative side work, then make one minimal evidence-backed change.]";
22
+ const TRAJECTORY_BUDGET_TOOL_ATTEMPTS = 20;
23
+ const TRAJECTORY_BUDGET_CHECKPOINT =
24
+ "[OpenPI trajectory checkpoint: 20 tool attempts used. Re-read the user contract and inspect the current artifact. Stop speculative exploration. Run one focused validation that covers the remaining risk; if it passes and the artifact satisfies the contract, finish. Otherwise make one evidence-backed change.]";
25
+ const VALIDATION_COMMAND =
26
+ /\b(?:go\s+test|cargo\s+test|pytest|python(?:3)?\s+-m\s+(?:pytest|unittest)|(?:npm|pnpm|yarn|bun)\s+(?:(?:run|run-script)\s+)?test|vitest|jest|mvnw?\b[^\n;&|]*\btest|gradlew?\b[^\n;&|]*\btest)\b/iu;
27
+ const BENCHMARK_CONVERGENCE_PROFILE_ENV =
28
+ "OPENPI_BENCHMARK_EXECUTION_CONVERGENCE_PROFILE";
29
+ const BENCHMARK_AGENT_ROOT_ENV = "OPENPI_BENCHMARK_AGENT_ROOT";
30
+
31
+ function textHasFailureMarker(output: string) {
32
+ return /(?:^|\n)(?:--- )?FAIL(?::|\s|$)|(?:^|\n)[^\n]*(?:operation not permitted|permission denied)(?:\n|$)/i.test(
33
+ output,
34
+ );
35
+ }
36
+
37
+ function canonicalJson(value: unknown, seen = new Set<object>()): string {
38
+ if (value === null) return "null";
39
+ if (typeof value === "string" || typeof value === "boolean") {
40
+ return JSON.stringify(value);
41
+ }
42
+ if (typeof value === "number") {
43
+ if (!Number.isFinite(value)) throw new Error("non-finite tool argument");
44
+ return JSON.stringify(value);
45
+ }
46
+ if (typeof value !== "object") throw new Error("non-JSON tool argument");
47
+ if (seen.has(value)) throw new Error("cyclic tool argument");
48
+ seen.add(value);
49
+ try {
50
+ if (Array.isArray(value)) {
51
+ return `[${value.map((entry) => canonicalJson(entry, seen)).join(",")}]`;
52
+ }
53
+ const record = value as Record<string, unknown>;
54
+ return `{${Object.keys(record)
55
+ .sort((left, right) => left.localeCompare(right, "en"))
56
+ .map(
57
+ (key) => `${JSON.stringify(key)}:${canonicalJson(record[key], seen)}`,
58
+ )
59
+ .join(",")}}`;
60
+ } finally {
61
+ seen.delete(value);
62
+ }
63
+ }
64
+
65
+ function toolCallFingerprint(
66
+ toolName: string,
67
+ input: Record<string, unknown>,
68
+ toolCallId: string,
69
+ ) {
70
+ try {
71
+ return createHash("sha256")
72
+ .update(toolName)
73
+ .update("\0")
74
+ .update(canonicalJson(input))
75
+ .digest("hex");
76
+ } catch {
77
+ return `unhashable:${toolCallId}`;
78
+ }
79
+ }
80
+
81
+ function validationCommandFingerprint(command: string) {
82
+ return createHash("sha256").update(command).digest("hex");
83
+ }
84
+
85
+ export default function executionConvergence(pi: ExtensionAPI) {
86
+ const benchmarkProfile = process.env[BENCHMARK_AGENT_ROOT_ENV]
87
+ ? process.env[BENCHMARK_CONVERGENCE_PROFILE_ENV]
88
+ : undefined;
89
+ const v2Enabled = !(benchmarkProfile === "legacy");
90
+ const executionPolicyEnabled =
91
+ benchmarkProfile !== undefined &&
92
+ benchmarkProfile !== "pi-native-execution";
93
+ const activeEvidenceEnabled =
94
+ executionPolicyEnabled &&
95
+ v2Enabled &&
96
+ benchmarkProfile !== "no-active-evidence";
97
+ const activeEvidencePolicy =
98
+ benchmarkProfile === "delayed-active-evidence"
99
+ ? DELAYED_ACTIVE_EVIDENCE_POLICY
100
+ : undefined;
101
+ const modelHintsEnabled =
102
+ executionPolicyEnabled && benchmarkProfile !== "no-model-hints";
103
+ const projectedBashResultIds = new Set<string>();
104
+ const projectedActiveEvidenceEpochs = new Set<string>();
105
+ let confirmWorkspaceDelete:
106
+ | ((paths: readonly string[]) => Promise<boolean>)
107
+ | undefined;
108
+ const workspaceCleanup = createWorkspaceCleanupGuard({
109
+ confirmDelete: async (paths) =>
110
+ (await confirmWorkspaceDelete?.(paths)) ?? false,
111
+ });
112
+ const boundedValidationCalls = new Map<
113
+ string,
114
+ { commandFingerprint: string; timeoutSeconds: number }
115
+ >();
116
+ const timedOutValidationCommands = new Set<string>();
117
+ const pendingAttempts = new Map<
118
+ string,
119
+ { sequence: number; fingerprint: string }
120
+ >();
121
+ const settledAttempts = new Map<
122
+ number,
123
+ { fingerprint: string; failed: boolean }
124
+ >();
125
+ let nextAttemptSequence = 0;
126
+ let nextSettlementSequence = 0;
127
+ let lastFailedFingerprint: string | undefined;
128
+ let failedStreak = 0;
129
+ const recentAttemptFailures: boolean[] = [];
130
+ let recoveryHintLatched = false;
131
+ let successfulAttemptsSinceRecoveryHint = 0;
132
+ let settledToolAttemptCount = 0;
133
+ let trajectoryHintInjected = false;
134
+
135
+ const recordSettledAttempts = () => {
136
+ let injectRecoveryHint = false;
137
+ while (settledAttempts.has(nextSettlementSequence)) {
138
+ const settled = settledAttempts.get(nextSettlementSequence)!;
139
+ settledAttempts.delete(nextSettlementSequence);
140
+ nextSettlementSequence += 1;
141
+ settledToolAttemptCount += 1;
142
+ recentAttemptFailures.push(settled.failed);
143
+ if (recentAttemptFailures.length > FAILURE_RECOVERY_WINDOW) {
144
+ recentAttemptFailures.shift();
145
+ }
146
+ if (recoveryHintLatched) {
147
+ successfulAttemptsSinceRecoveryHint = settled.failed
148
+ ? 0
149
+ : successfulAttemptsSinceRecoveryHint + 1;
150
+ if (
151
+ successfulAttemptsSinceRecoveryHint >=
152
+ FAILURE_RECOVERY_RESET_SUCCESSES
153
+ ) {
154
+ recoveryHintLatched = false;
155
+ recentAttemptFailures.length = 0;
156
+ successfulAttemptsSinceRecoveryHint = 0;
157
+ }
158
+ } else if (
159
+ recentAttemptFailures.filter(Boolean).length >=
160
+ FAILURE_RECOVERY_THRESHOLD
161
+ ) {
162
+ recoveryHintLatched = true;
163
+ successfulAttemptsSinceRecoveryHint = 0;
164
+ injectRecoveryHint = true;
165
+ }
166
+ if (!settled.failed) {
167
+ lastFailedFingerprint = undefined;
168
+ failedStreak = 0;
169
+ } else if (settled.fingerprint === lastFailedFingerprint) {
170
+ failedStreak += 1;
171
+ } else {
172
+ lastFailedFingerprint = settled.fingerprint;
173
+ failedStreak = 1;
174
+ }
175
+ }
176
+ return injectRecoveryHint;
177
+ };
178
+
179
+ pi.on("tool_call", async (event, ctx) => {
180
+ const fingerprint = toolCallFingerprint(
181
+ event.toolName,
182
+ event.input,
183
+ event.toolCallId,
184
+ );
185
+ if (
186
+ executionPolicyEnabled &&
187
+ lastFailedFingerprint !== undefined &&
188
+ fingerprint !== lastFailedFingerprint
189
+ ) {
190
+ lastFailedFingerprint = undefined;
191
+ failedStreak = 0;
192
+ }
193
+ if (
194
+ executionPolicyEnabled &&
195
+ fingerprint === lastFailedFingerprint &&
196
+ failedStreak >= 2
197
+ ) {
198
+ pi.events.emit("openpi:execution-convergence", {
199
+ type: "loop_gate",
200
+ blockedRepeatedFailures: 1,
201
+ });
202
+ return {
203
+ block: true,
204
+ reason: `Blocked: this exact ${event.toolName} call with identical arguments has already failed repeatedly with no different step in between. Change the arguments or inspect new evidence before retrying.`,
205
+ };
206
+ }
207
+ if (v2Enabled && isToolCallEventType("write", event)) {
208
+ await workspaceCleanup.beforeWrite({
209
+ id: event.toolCallId,
210
+ path: event.input.path,
211
+ cwd: ctx.cwd,
212
+ });
213
+ }
214
+ if (v2Enabled && isToolCallEventType("bash", event)) {
215
+ confirmWorkspaceDelete = (paths) =>
216
+ ctx.ui.confirm(
217
+ "Delete pre-existing workspace files?",
218
+ `The command would delete files that existed before this agent changed them:\n\n${paths.map((candidate) => `- ${candidate}`).join("\n")}\n\nAllow this exact deletion?`,
219
+ );
220
+ let cleanupDecision;
221
+ try {
222
+ cleanupDecision = await workspaceCleanup.before({
223
+ id: event.toolCallId,
224
+ command: event.input.command,
225
+ cwd: ctx.cwd,
226
+ });
227
+ } finally {
228
+ confirmWorkspaceDelete = undefined;
229
+ }
230
+ if (cleanupDecision.kind === "block") {
231
+ pi.events.emit("openpi:execution-convergence", {
232
+ type: "workspace_cleanup_guard",
233
+ blockedPreExistingDeletes: cleanupDecision.protectedPaths.length,
234
+ });
235
+ return { block: true, reason: cleanupDecision.reason };
236
+ }
237
+ }
238
+ if (
239
+ executionPolicyEnabled &&
240
+ isToolCallEventType("bash", event) &&
241
+ event.input.timeout === undefined &&
242
+ VALIDATION_COMMAND.test(event.input.command)
243
+ ) {
244
+ const commandFingerprint = validationCommandFingerprint(
245
+ event.input.command,
246
+ );
247
+ const shortenedRetry = timedOutValidationCommands.has(commandFingerprint);
248
+ const timeoutSeconds = shortenedRetry
249
+ ? RETRY_VALIDATION_TIMEOUT_SECONDS
250
+ : DEFAULT_VALIDATION_TIMEOUT_SECONDS;
251
+ event.input.timeout = timeoutSeconds;
252
+ boundedValidationCalls.set(event.toolCallId, {
253
+ commandFingerprint,
254
+ timeoutSeconds,
255
+ });
256
+ pi.events.emit("openpi:execution-convergence", {
257
+ type: shortenedRetry
258
+ ? "validation_retry_timeout_default"
259
+ : "validation_timeout_default",
260
+ boundedValidationCalls: 1,
261
+ ...(shortenedRetry ? { shortenedValidationRetries: 1 } : {}),
262
+ timeoutSeconds,
263
+ });
264
+ }
265
+ if (executionPolicyEnabled) {
266
+ pendingAttempts.set(event.toolCallId, {
267
+ sequence: nextAttemptSequence,
268
+ fingerprint,
269
+ });
270
+ nextAttemptSequence += 1;
271
+ }
272
+ });
273
+
274
+ pi.on("tool_result", async (event) => {
275
+ if (
276
+ v2Enabled &&
277
+ (event.toolName === "bash" || event.toolName === "write")
278
+ ) {
279
+ await workspaceCleanup.after({
280
+ id: event.toolCallId,
281
+ isError: event.isError,
282
+ });
283
+ }
284
+ const output = event.content
285
+ .filter((block) => block.type === "text")
286
+ .map((block) => block.text)
287
+ .join("\n");
288
+ const boundedValidation = boundedValidationCalls.get(event.toolCallId);
289
+ if (boundedValidation) {
290
+ boundedValidationCalls.delete(event.toolCallId);
291
+ if (
292
+ event.isError &&
293
+ output.includes(
294
+ `Command timed out after ${boundedValidation.timeoutSeconds} seconds`,
295
+ )
296
+ ) {
297
+ timedOutValidationCommands.add(boundedValidation.commandFingerprint);
298
+ pi.events.emit("openpi:execution-convergence", {
299
+ type: "validation_timeout_triggered",
300
+ timedOutValidationCalls: 1,
301
+ timeoutSeconds: boundedValidation.timeoutSeconds,
302
+ });
303
+ }
304
+ }
305
+ const hasFailureMarker =
306
+ event.toolName === "bash" && textHasFailureMarker(output);
307
+ const attempt = pendingAttempts.get(event.toolCallId);
308
+ let injectRecoveryHint = false;
309
+ if (attempt) {
310
+ pendingAttempts.delete(event.toolCallId);
311
+ settledAttempts.set(attempt.sequence, {
312
+ fingerprint: attempt.fingerprint,
313
+ failed: event.isError || hasFailureMarker,
314
+ });
315
+ injectRecoveryHint = recordSettledAttempts();
316
+ }
317
+ const injectTrajectoryHint =
318
+ attempt !== undefined &&
319
+ !trajectoryHintInjected &&
320
+ settledToolAttemptCount >= TRAJECTORY_BUDGET_TOOL_ATTEMPTS;
321
+ if (injectTrajectoryHint) {
322
+ trajectoryHintInjected = true;
323
+ pi.events.emit("openpi:execution-convergence", {
324
+ type: modelHintsEnabled
325
+ ? "trajectory_budget_hint"
326
+ : "trajectory_budget_hint_suppressed",
327
+ ...(modelHintsEnabled
328
+ ? { injectedTrajectoryHints: 1 }
329
+ : { suppressedTrajectoryHints: 1 }),
330
+ toolAttempts: settledToolAttemptCount,
331
+ });
332
+ }
333
+ if (injectRecoveryHint) {
334
+ pi.events.emit("openpi:execution-convergence", {
335
+ type: modelHintsEnabled
336
+ ? "failure_recovery_hint"
337
+ : "failure_recovery_hint_suppressed",
338
+ ...(modelHintsEnabled
339
+ ? { injectedRecoveryHints: 1 }
340
+ : { suppressedRecoveryHints: 1 }),
341
+ });
342
+ }
343
+ const checkpoints = [
344
+ ...(modelHintsEnabled && injectRecoveryHint
345
+ ? [FAILURE_RECOVERY_CHECKPOINT]
346
+ : []),
347
+ ...(modelHintsEnabled && injectTrajectoryHint
348
+ ? [TRAJECTORY_BUDGET_CHECKPOINT]
349
+ : []),
350
+ ];
351
+ if (checkpoints.length === 0) return;
352
+ return {
353
+ content: [
354
+ ...event.content,
355
+ ...checkpoints.map((text) => ({ type: "text" as const, text })),
356
+ ],
357
+ };
358
+ });
359
+
360
+ pi.on("agent_settled", () => {
361
+ projectedBashResultIds.clear();
362
+ projectedActiveEvidenceEpochs.clear();
363
+ workspaceCleanup.reset();
364
+ boundedValidationCalls.clear();
365
+ timedOutValidationCommands.clear();
366
+ pendingAttempts.clear();
367
+ settledAttempts.clear();
368
+ nextAttemptSequence = 0;
369
+ nextSettlementSequence = 0;
370
+ lastFailedFingerprint = undefined;
371
+ failedStreak = 0;
372
+ recentAttemptFailures.length = 0;
373
+ recoveryHintLatched = false;
374
+ successfulAttemptsSinceRecoveryHint = 0;
375
+ settledToolAttemptCount = 0;
376
+ trajectoryHintInjected = false;
377
+ });
378
+
379
+ pi.on("context", (event) => {
380
+ if (!executionPolicyEnabled) return;
381
+ const activeEvidence = activeEvidenceEnabled
382
+ ? projectActiveEvidence(event.messages, activeEvidencePolicy)
383
+ : undefined;
384
+ const contextMessages = activeEvidence?.messages ?? event.messages;
385
+ if (activeEvidence) {
386
+ const isNewEpoch = !projectedActiveEvidenceEpochs.has(
387
+ activeEvidence.receipt.digest,
388
+ );
389
+ projectedActiveEvidenceEpochs.add(activeEvidence.receipt.digest);
390
+ pi.events.emit("openpi:execution-convergence", {
391
+ type: "active_evidence_projection",
392
+ projectedActiveEvidenceApplications: 1,
393
+ newActiveEvidenceEpochs: isNewEpoch ? 1 : 0,
394
+ closedToolTransactions: activeEvidence.receipt.closedTransactions,
395
+ activeEvidenceCharsRemoved:
396
+ activeEvidence.receipt.originalChars -
397
+ activeEvidence.receipt.projectedChars,
398
+ });
399
+ }
400
+ let projectedBashResultApplications = 0;
401
+ let newlyProjectedBashResults = 0;
402
+ const messages = contextMessages.map((message) => {
403
+ if (
404
+ message.role !== "toolResult" ||
405
+ message.toolName !== "bash" ||
406
+ message.isError ||
407
+ message.content.length !== 1 ||
408
+ message.content[0]?.type !== "text" ||
409
+ textHasFailureMarker(message.content[0].text) ||
410
+ message.content[0].text.length <= MAX_BASH_RESULT_CHARS
411
+ ) {
412
+ return message;
413
+ }
414
+
415
+ const text = message.content[0].text;
416
+ projectedBashResultApplications += 1;
417
+ if (!projectedBashResultIds.has(message.toolCallId)) {
418
+ projectedBashResultIds.add(message.toolCallId);
419
+ newlyProjectedBashResults += 1;
420
+ }
421
+ return {
422
+ ...message,
423
+ content: [
424
+ {
425
+ ...message.content[0],
426
+ text: `${text.slice(0, BASH_RESULT_HEAD_CHARS)}\n[OpenPI: Bash output bounded; head and tail retained; rerun a narrower command if the omitted middle is needed]\n${text.slice(-BASH_RESULT_TAIL_CHARS)}`,
427
+ },
428
+ ],
429
+ };
430
+ });
431
+
432
+ if (projectedBashResultApplications > 0) {
433
+ pi.events.emit("openpi:execution-convergence", {
434
+ type: "context_projection",
435
+ projectedBashResultApplications,
436
+ newlyProjectedBashResults,
437
+ });
438
+ }
439
+ if (!activeEvidence && projectedBashResultApplications === 0) return;
440
+ return { messages };
441
+ });
442
+ }