@tt-a1i/openpi 0.4.0 → 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 +102 -40
- package/SETUP.md +22 -6
- package/assets/openpi-launch-card-v1.webp +0 -0
- package/bin/openpi.js +145 -0
- 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 +13 -5
- package/extensions/background-terminals/src/result-delivery.ts +4 -1
- 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/render.ts +17 -257
- package/extensions/file-search/src/binaries.ts +57 -41
- package/extensions/git-read/index.ts +1 -3
- 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 +153 -86
- 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 +123 -127
- package/extensions/shared/activity-status.ts +30 -0
- 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/capability-intent.ts +1 -1
- package/extensions/shared/child-session.ts +437 -21
- package/extensions/shared/result-delivery.ts +34 -0
- package/extensions/shared/setup-config.ts +73 -33
- package/extensions/shared/setup-episode-state.ts +1 -1
- package/extensions/shared/terminal-text.ts +110 -23
- package/extensions/shared/text-projection.ts +72 -15
- package/extensions/shared/tool-activity.ts +382 -0
- package/extensions/shared/tool-surface.ts +29 -2
- 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 +270 -59
- package/extensions/subagents/navigation.ts +34 -5
- 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/manager.ts +34 -2
- package/extensions/subagents/src/prompt.ts +32 -4
- package/extensions/subagents/src/result-artifact.ts +4 -0
- package/extensions/subagents/src/result-delivery.ts +7 -1
- package/extensions/subagents/src/runtime.ts +15 -1
- package/extensions/subagents/src/ui/takeover.ts +73 -257
- package/extensions/subagents/src/ui/transcript.ts +38 -535
- 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/ui-customization/footer.ts +0 -40
- package/extensions/ui-customization/index.ts +0 -4
- package/extensions/user-input-fold/index.ts +1 -1
- package/extensions/web/index.ts +234 -0
- package/extensions/workflows/artifacts.ts +137 -47
- package/extensions/workflows/completion-projection.ts +457 -0
- package/extensions/workflows/coordinator.ts +8 -10
- package/extensions/workflows/dashboard.ts +167 -228
- package/extensions/workflows/handoff.ts +70 -16
- package/extensions/workflows/index.ts +488 -198
- package/extensions/workflows/journal.ts +148 -13
- package/extensions/workflows/model.ts +74 -4
- package/extensions/workflows/navigation.ts +32 -8
- package/extensions/workflows/progress-projection.ts +306 -0
- package/extensions/workflows/prompt.ts +66 -6
- package/extensions/workflows/replay-safety.ts +42 -21
- package/extensions/workflows/result-delivery.ts +128 -64
- package/extensions/workflows/retention.ts +593 -0
- package/extensions/workflows/runner.ts +388 -279
- package/extensions/workflows/sandbox-child.cjs +25 -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 +1 -1
- package/skills/workflows/REFERENCE.md +4 -2
- 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 -303
|
@@ -48,20 +48,29 @@ import {
|
|
|
48
48
|
truncateToWidth,
|
|
49
49
|
} from "@earendil-works/pi-tui";
|
|
50
50
|
import { type Static, Type } from "typebox";
|
|
51
|
-
import {
|
|
51
|
+
import {
|
|
52
|
+
createStatusWriter,
|
|
53
|
+
formatActivityStatus,
|
|
54
|
+
} from "../shared/activity-status.ts";
|
|
55
|
+
import { fitNavigationSides } from "../shared/below-editor-navigation.ts";
|
|
52
56
|
import { waitBounded } from "../shared/child-session.ts";
|
|
53
57
|
import { contextPercent } from "../shared/context-utilization.ts";
|
|
54
58
|
import {
|
|
55
59
|
registerEditorLayer,
|
|
56
60
|
removeEditorLayer,
|
|
57
61
|
} from "../shared/editor-layers.ts";
|
|
58
|
-
import { fitNavigationSides } from "../shared/below-editor-navigation.ts";
|
|
59
62
|
import { loadSetupConfig } from "../shared/setup-config.ts";
|
|
60
63
|
import { SPINNER_INTERVAL_MS } from "../shared/spinner.ts";
|
|
61
64
|
import {
|
|
62
65
|
OPENPI_TOOL_SURFACE,
|
|
63
66
|
patchOwnedTools,
|
|
64
67
|
} from "../shared/tool-surface.ts";
|
|
68
|
+
import {
|
|
69
|
+
notifyWebCapabilities,
|
|
70
|
+
projectWorkflowCapability,
|
|
71
|
+
registerWebCapability,
|
|
72
|
+
type WebCapabilityScope,
|
|
73
|
+
} from "../shared/web-observer-registry.ts";
|
|
65
74
|
import {
|
|
66
75
|
createWorktree,
|
|
67
76
|
reclaimWorktree,
|
|
@@ -78,18 +87,27 @@ import {
|
|
|
78
87
|
acceptanceInstruction,
|
|
79
88
|
acceptanceSchema,
|
|
80
89
|
applyAcceptance,
|
|
81
|
-
evaluateAcceptance,
|
|
82
90
|
parseAcceptanceContract,
|
|
83
91
|
} from "./acceptance.ts";
|
|
84
92
|
import {
|
|
85
93
|
createWorkflowPersistence,
|
|
86
94
|
loadJournal,
|
|
87
95
|
persistWorkflowAgentResult,
|
|
96
|
+
persistWorkflowDeliveryState,
|
|
88
97
|
persistWorkflowJson,
|
|
98
|
+
persistWorkflowTerminalState,
|
|
89
99
|
} from "./artifacts.ts";
|
|
100
|
+
import {
|
|
101
|
+
buildExpandedWorkflowCompletion,
|
|
102
|
+
buildWorkflowCompletionDisplay,
|
|
103
|
+
isWorkflowCompletionDisplay,
|
|
104
|
+
workflowCompletionAlerts,
|
|
105
|
+
workflowCompletionResultPreview,
|
|
106
|
+
workflowCompletionSummary,
|
|
107
|
+
} from "./completion-projection.ts";
|
|
90
108
|
import { RunController } from "./controller.ts";
|
|
91
109
|
import {
|
|
92
|
-
|
|
110
|
+
resolveWorkflowLaunchMode,
|
|
93
111
|
waitForWorkflowCompletion,
|
|
94
112
|
} from "./coordinator.ts";
|
|
95
113
|
import {
|
|
@@ -108,8 +126,8 @@ import {
|
|
|
108
126
|
} from "./invocation-ledger.ts";
|
|
109
127
|
import {
|
|
110
128
|
agentCallKey,
|
|
129
|
+
createJournalAccumulator,
|
|
111
130
|
createReplayCache,
|
|
112
|
-
type JournalEntry,
|
|
113
131
|
type ReplayCache,
|
|
114
132
|
} from "./journal.ts";
|
|
115
133
|
import {
|
|
@@ -122,6 +140,7 @@ import {
|
|
|
122
140
|
agentContext,
|
|
123
141
|
aggregateUsage,
|
|
124
142
|
appendLog,
|
|
143
|
+
compactWorkflowToolDetails,
|
|
125
144
|
countStates,
|
|
126
145
|
createUsageReader,
|
|
127
146
|
emptyUsage,
|
|
@@ -146,15 +165,15 @@ import {
|
|
|
146
165
|
type WorkflowStripEntry,
|
|
147
166
|
WorkflowStripState,
|
|
148
167
|
WorkflowStripWidget,
|
|
168
|
+
workflowStripEntryKey,
|
|
149
169
|
} from "./navigation.ts";
|
|
150
170
|
import {
|
|
151
171
|
normalizeWorkflowOperatorKey,
|
|
152
172
|
WorkflowOperatorRegistry,
|
|
153
173
|
} from "./operator.ts";
|
|
154
174
|
import {
|
|
155
|
-
buildBackgroundWorkflowFollowUp,
|
|
156
175
|
buildBackgroundWorkflowLaunchResult,
|
|
157
|
-
|
|
176
|
+
buildProjectedWorkflowCompletionBatches,
|
|
158
177
|
buildProjectedWorkflowResultMessage,
|
|
159
178
|
buildWorkflowAgentPrompt,
|
|
160
179
|
buildWorkflowResultMessage,
|
|
@@ -169,15 +188,20 @@ import {
|
|
|
169
188
|
WORKFLOW_STOP_TOOL_DESCRIPTION,
|
|
170
189
|
WORKFLOW_TOOL_DESCRIPTION,
|
|
171
190
|
} from "./prompt.ts";
|
|
172
|
-
import {
|
|
173
|
-
createWorkflowResultDelivery,
|
|
174
|
-
type WorkflowCompletionEnvelope,
|
|
175
|
-
} from "./result-delivery.ts";
|
|
176
191
|
import {
|
|
177
192
|
beginProcessReplayWorkspaceLease,
|
|
178
193
|
createReplayIdentity,
|
|
179
194
|
isReplaySafeAgentCall,
|
|
180
195
|
} from "./replay-safety.ts";
|
|
196
|
+
import {
|
|
197
|
+
createWorkflowResultDelivery,
|
|
198
|
+
type WorkflowCompletionEnvelope,
|
|
199
|
+
} from "./result-delivery.ts";
|
|
200
|
+
import {
|
|
201
|
+
createWorkflowSettledRunRetention,
|
|
202
|
+
projectWorkflowDetails,
|
|
203
|
+
type WorkflowSettledRunRetentionOptions,
|
|
204
|
+
} from "./retention.ts";
|
|
181
205
|
import {
|
|
182
206
|
createWorkflowResources,
|
|
183
207
|
runAgent,
|
|
@@ -186,7 +210,7 @@ import {
|
|
|
186
210
|
type WorkflowModel,
|
|
187
211
|
} from "./runner.ts";
|
|
188
212
|
import { runWorkflowSandbox } from "./sandbox.ts";
|
|
189
|
-
import {
|
|
213
|
+
import { writeFileAtomic } from "./serialization.ts";
|
|
190
214
|
import {
|
|
191
215
|
finalizeWorktreeHandoff,
|
|
192
216
|
prepareWorktreeHandoff,
|
|
@@ -203,7 +227,7 @@ function runHeader(
|
|
|
203
227
|
) {
|
|
204
228
|
const { done, failed, uncertain } = countStates(details);
|
|
205
229
|
const settled = done + failed;
|
|
206
|
-
const elapsed = formatElapsed(details.startedAt, details.finishedAt);
|
|
230
|
+
const elapsed = formatElapsed(details.startedAt, details.finishedAt, now);
|
|
207
231
|
// A just-launched run has no agents and a 0s clock; the metrics join in
|
|
208
232
|
// once there is something real to report.
|
|
209
233
|
const counts =
|
|
@@ -264,7 +288,7 @@ function buildCollapsedRows(
|
|
|
264
288
|
? percent === undefined
|
|
265
289
|
? undefined
|
|
266
290
|
: `${percent}%`
|
|
267
|
-
: formatElapsed(agent.startedAt, agent.finishedAt);
|
|
291
|
+
: formatElapsed(agent.startedAt, agent.finishedAt, now);
|
|
268
292
|
const left = ` ${stateGlyph(agent.state, theme, now)} ${theme.fg(
|
|
269
293
|
"accent",
|
|
270
294
|
sanitizeWorkflowDisplayLine(agent.label),
|
|
@@ -366,7 +390,7 @@ function buildExpandedWorkflow(
|
|
|
366
390
|
sanitizeWorkflowDisplayLine(agent.label),
|
|
367
391
|
)} ${theme.fg(
|
|
368
392
|
"dim",
|
|
369
|
-
[context, formatElapsed(agent.startedAt, agent.finishedAt)]
|
|
393
|
+
[context, formatElapsed(agent.startedAt, agent.finishedAt, now)]
|
|
370
394
|
.filter(Boolean)
|
|
371
395
|
.join(" · "),
|
|
372
396
|
)}`;
|
|
@@ -532,31 +556,35 @@ interface AgentCallOptions {
|
|
|
532
556
|
inputs?: unknown;
|
|
533
557
|
}
|
|
534
558
|
|
|
535
|
-
const WorkflowParams = Type.Object(
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
args: Type.Optional(
|
|
540
|
-
Type.String({
|
|
541
|
-
description: WORKFLOW_PARAMETER_DESCRIPTIONS.args,
|
|
542
|
-
}),
|
|
543
|
-
),
|
|
544
|
-
background: Type.Optional(
|
|
545
|
-
Type.Boolean({
|
|
546
|
-
description: WORKFLOW_PARAMETER_DESCRIPTIONS.background,
|
|
559
|
+
const WorkflowParams = Type.Object(
|
|
560
|
+
{
|
|
561
|
+
script: Type.String({
|
|
562
|
+
description: WORKFLOW_PARAMETER_DESCRIPTIONS.script,
|
|
547
563
|
}),
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
564
|
+
args: Type.Optional(
|
|
565
|
+
Type.String({
|
|
566
|
+
description: WORKFLOW_PARAMETER_DESCRIPTIONS.args,
|
|
567
|
+
}),
|
|
568
|
+
),
|
|
569
|
+
background: Type.Optional(
|
|
570
|
+
Type.Boolean({
|
|
571
|
+
deprecated: true,
|
|
572
|
+
description: WORKFLOW_PARAMETER_DESCRIPTIONS.background,
|
|
573
|
+
}),
|
|
574
|
+
),
|
|
575
|
+
wait: Type.Optional(
|
|
576
|
+
Type.Boolean({
|
|
577
|
+
description: WORKFLOW_PARAMETER_DESCRIPTIONS.wait,
|
|
578
|
+
}),
|
|
579
|
+
),
|
|
580
|
+
resume_from_run_id: Type.Optional(
|
|
581
|
+
Type.String({
|
|
582
|
+
description: WORKFLOW_PARAMETER_DESCRIPTIONS.resumeFromRunId,
|
|
583
|
+
}),
|
|
584
|
+
),
|
|
585
|
+
},
|
|
586
|
+
{ additionalProperties: false },
|
|
587
|
+
);
|
|
560
588
|
|
|
561
589
|
type WorkflowInput = Static<typeof WorkflowParams>;
|
|
562
590
|
|
|
@@ -594,6 +622,25 @@ function errorText(error: unknown): string {
|
|
|
594
622
|
);
|
|
595
623
|
}
|
|
596
624
|
|
|
625
|
+
function isWorkflowRenderDetails(value: unknown): value is WorkflowDetails {
|
|
626
|
+
if (!value || typeof value !== "object") return false;
|
|
627
|
+
const details = value as Partial<WorkflowDetails>;
|
|
628
|
+
return (
|
|
629
|
+
typeof details.runId === "string" &&
|
|
630
|
+
details.runId.length > 0 &&
|
|
631
|
+
typeof details.background === "boolean" &&
|
|
632
|
+
(details.status === "running" ||
|
|
633
|
+
details.status === "completed" ||
|
|
634
|
+
details.status === "failed" ||
|
|
635
|
+
details.status === "aborted" ||
|
|
636
|
+
details.status === "uncertain") &&
|
|
637
|
+
typeof details.startedAt === "number" &&
|
|
638
|
+
Number.isFinite(details.startedAt) &&
|
|
639
|
+
Array.isArray(details.phases) &&
|
|
640
|
+
Array.isArray(details.agents)
|
|
641
|
+
);
|
|
642
|
+
}
|
|
643
|
+
|
|
597
644
|
function summaryLine(details: WorkflowDetails): string {
|
|
598
645
|
const { done, failed, uncertain } = countStates(details);
|
|
599
646
|
const settled = done + failed;
|
|
@@ -613,20 +660,16 @@ function writeRunFile(runDir: string, name: string, content: string) {
|
|
|
613
660
|
writeFileAtomic(path.join(runDir, name), content);
|
|
614
661
|
}
|
|
615
662
|
|
|
616
|
-
function
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
: {}),
|
|
626
|
-
agents: details.agents.map((agent) => ({ ...agent, transcript: [] })),
|
|
627
|
-
};
|
|
663
|
+
function appendArtifactPersistenceFailure(
|
|
664
|
+
details: WorkflowDetails,
|
|
665
|
+
error: unknown,
|
|
666
|
+
) {
|
|
667
|
+
const persistenceFailure = `Artifact persistence failed: ${errorText(error)}`;
|
|
668
|
+
if (details.status !== "aborted") details.status = "failed";
|
|
669
|
+
details.error = details.error
|
|
670
|
+
? `${details.error}; ${persistenceFailure}`
|
|
671
|
+
: persistenceFailure;
|
|
628
672
|
}
|
|
629
|
-
|
|
630
673
|
export interface ActiveWorkflowRunLifecycle {
|
|
631
674
|
details: WorkflowDetails;
|
|
632
675
|
controller: Pick<RunController, "abort" | "settle">;
|
|
@@ -634,6 +677,21 @@ export interface ActiveWorkflowRunLifecycle {
|
|
|
634
677
|
forceSettle(error: string): void;
|
|
635
678
|
}
|
|
636
679
|
|
|
680
|
+
interface WorkflowLifecycleTestHooks {
|
|
681
|
+
readonly persistWorkflow?: typeof persistWorkflowJson;
|
|
682
|
+
readonly reclaimWorktree?: typeof reclaimWorktree;
|
|
683
|
+
readonly onRunStarted?: (run: ActiveWorkflowRunLifecycle) => void;
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
let workflowLifecycleTestHooks: WorkflowLifecycleTestHooks | undefined;
|
|
687
|
+
|
|
688
|
+
/** Test-only control for deterministic lifecycle race coverage. */
|
|
689
|
+
export function __setWorkflowTestLifecycleHooks(
|
|
690
|
+
hooks: WorkflowLifecycleTestHooks | undefined,
|
|
691
|
+
) {
|
|
692
|
+
workflowLifecycleTestHooks = hooks;
|
|
693
|
+
}
|
|
694
|
+
|
|
637
695
|
/** Abort every live child and bound the whole session-shutdown barrier once. */
|
|
638
696
|
export async function shutdownActiveWorkflowRuns(
|
|
639
697
|
runs: readonly ActiveWorkflowRunLifecycle[],
|
|
@@ -738,19 +796,38 @@ function runDetailText(
|
|
|
738
796
|
return `Run ${run.runId} — ${run.status}`;
|
|
739
797
|
}
|
|
740
798
|
|
|
741
|
-
export
|
|
799
|
+
export interface WorkflowExtensionOptions {
|
|
800
|
+
/** Test/configuration seam for the settled session-memory projection. */
|
|
801
|
+
readonly settledRetention?: WorkflowSettledRunRetentionOptions;
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
const WORKFLOW_DELIVERY_DETAILS_MAX_BYTES = 128 * 1024;
|
|
805
|
+
|
|
806
|
+
export default function workflows(
|
|
807
|
+
pi: ExtensionAPI,
|
|
808
|
+
options: WorkflowExtensionOptions = {},
|
|
809
|
+
) {
|
|
742
810
|
/** Live background runs, for /workflows and shutdown cleanup. */
|
|
743
811
|
const activeRuns = new Map<string, ActiveWorkflowRunLifecycle>();
|
|
812
|
+
let unregisterWebCapability: (() => void) | undefined;
|
|
813
|
+
let webCapabilityScope: WebCapabilityScope | undefined;
|
|
744
814
|
const activeDetails = () =>
|
|
745
815
|
new Map(
|
|
746
816
|
[...activeRuns].map(([runId, run]) => [runId, run.details] as const),
|
|
747
817
|
);
|
|
748
|
-
const settledRuns =
|
|
818
|
+
const settledRuns = createWorkflowSettledRunRetention(
|
|
819
|
+
options.settledRetention,
|
|
820
|
+
);
|
|
821
|
+
/** Disk remains canonical; retained projections cover transient read failures. */
|
|
822
|
+
const dashboardDetails = () => activeDetails();
|
|
823
|
+
const dashboardRetainedDetails = () =>
|
|
824
|
+
new Map<string, WorkflowDetails>(settledRuns.entriesArray());
|
|
749
825
|
const registerStableToolFamily = () =>
|
|
750
826
|
patchOwnedTools(pi, "workflows", {
|
|
751
827
|
enable: OPENPI_TOOL_SURFACE.workflows.entry,
|
|
752
828
|
});
|
|
753
829
|
const stripState = new WorkflowStripState();
|
|
830
|
+
const statusWriter = createStatusWriter("workflows");
|
|
754
831
|
const widgetKey = "workflow-navigation";
|
|
755
832
|
|
|
756
833
|
/**
|
|
@@ -763,41 +840,61 @@ export default function workflows(pi: ExtensionAPI) {
|
|
|
763
840
|
): WorkflowCompletionEnvelope => {
|
|
764
841
|
const deliveryId = details.delivery?.id;
|
|
765
842
|
if (!deliveryId) throw new Error("Workflow delivery identity is missing");
|
|
843
|
+
const projection = projectWorkflowDetails(
|
|
844
|
+
details,
|
|
845
|
+
WORKFLOW_DELIVERY_DETAILS_MAX_BYTES,
|
|
846
|
+
);
|
|
847
|
+
if (!projection) {
|
|
848
|
+
throw new Error(
|
|
849
|
+
`Workflow ${details.runId} cannot create a bounded completion projection`,
|
|
850
|
+
);
|
|
851
|
+
}
|
|
766
852
|
return {
|
|
767
853
|
deliveryId,
|
|
768
854
|
runId: details.runId,
|
|
769
|
-
details,
|
|
855
|
+
details: projection,
|
|
770
856
|
};
|
|
771
857
|
};
|
|
772
858
|
const resultDelivery = createWorkflowResultDelivery({
|
|
773
859
|
isIdle: () => lastContext?.isIdle() ?? false,
|
|
774
|
-
persist: (details) =>
|
|
775
|
-
|
|
860
|
+
persist: (details) => {
|
|
861
|
+
if (!details.delivery)
|
|
862
|
+
throw new Error("Workflow delivery identity is missing");
|
|
863
|
+
persistWorkflowDeliveryState(
|
|
776
864
|
path.join(getAgentDir(), "workflows", details.runId),
|
|
777
|
-
details,
|
|
778
|
-
)
|
|
865
|
+
details.delivery,
|
|
866
|
+
);
|
|
867
|
+
},
|
|
779
868
|
deliver: async (envelopes, wake) => {
|
|
780
|
-
const
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
869
|
+
const hydrated = envelopes.map((envelope) => ({
|
|
870
|
+
...envelope,
|
|
871
|
+
details:
|
|
872
|
+
readPersistedWorkflowDetails(envelope.runId, {
|
|
873
|
+
hydrateArtifacts: true,
|
|
874
|
+
}) ?? envelope.details,
|
|
875
|
+
}));
|
|
876
|
+
const sourceEntries = hydrated.map((envelope) => ({
|
|
877
|
+
deliveryId: envelope.deliveryId,
|
|
878
|
+
details: envelope.details,
|
|
879
|
+
runDir: path.join(getAgentDir(), "workflows", envelope.runId),
|
|
880
|
+
}));
|
|
881
|
+
const batches = buildProjectedWorkflowCompletionBatches(
|
|
882
|
+
sourceEntries,
|
|
786
883
|
lastContext?.getContextUsage?.(),
|
|
787
884
|
);
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
885
|
+
for (const batch of batches) {
|
|
886
|
+
pi.sendMessage(
|
|
887
|
+
{
|
|
888
|
+
customType: "workflow-result",
|
|
889
|
+
content: batch.content,
|
|
890
|
+
display: true,
|
|
891
|
+
details: buildWorkflowCompletionDisplay(batch.entries),
|
|
892
|
+
},
|
|
893
|
+
wake
|
|
894
|
+
? { deliverAs: "followUp", triggerTurn: true }
|
|
895
|
+
: { deliverAs: "nextTurn" },
|
|
896
|
+
);
|
|
897
|
+
}
|
|
801
898
|
return envelopes.map((envelope) => ({
|
|
802
899
|
deliveryId: envelope.deliveryId,
|
|
803
900
|
delivered: true,
|
|
@@ -807,6 +904,7 @@ export default function workflows(pi: ExtensionAPI) {
|
|
|
807
904
|
let completedRuns = 0;
|
|
808
905
|
let failedRuns = 0;
|
|
809
906
|
let widgetVisible = false;
|
|
907
|
+
let widgetEntryKey: string | undefined;
|
|
810
908
|
let requestWidgetRender: (() => void) | undefined;
|
|
811
909
|
let navigationLayerRegistered = false;
|
|
812
910
|
let dashboardOpen = false;
|
|
@@ -837,16 +935,25 @@ export default function workflows(pi: ExtensionAPI) {
|
|
|
837
935
|
const running = newestEntry(
|
|
838
936
|
[...activeRuns].map(([runId, run]) => [runId, run.details] as const),
|
|
839
937
|
);
|
|
840
|
-
return running ?? newestEntry(settledRuns);
|
|
938
|
+
return running ?? newestEntry(settledRuns.entriesArray());
|
|
841
939
|
};
|
|
842
940
|
|
|
843
941
|
const updateWorkflowWidget = () => {
|
|
844
942
|
const ctx = lastContext;
|
|
845
943
|
if (!ctx || ctx.mode !== "tui") return;
|
|
846
|
-
const
|
|
847
|
-
|
|
944
|
+
const entry = stripEntry();
|
|
945
|
+
const visible = Boolean(entry);
|
|
946
|
+
const entryKey = workflowStripEntryKey(entry);
|
|
947
|
+
if (visible === widgetVisible) {
|
|
948
|
+
if (visible && entryKey !== widgetEntryKey) {
|
|
949
|
+
widgetEntryKey = entryKey;
|
|
950
|
+
requestWidgetRender?.();
|
|
951
|
+
}
|
|
952
|
+
return;
|
|
953
|
+
}
|
|
848
954
|
if (!visible) {
|
|
849
955
|
stripState.focused = false;
|
|
956
|
+
widgetEntryKey = undefined;
|
|
850
957
|
requestWidgetRender = undefined;
|
|
851
958
|
ctx.ui.setWidget(widgetKey, undefined);
|
|
852
959
|
widgetVisible = false;
|
|
@@ -861,25 +968,25 @@ export default function workflows(pi: ExtensionAPI) {
|
|
|
861
968
|
{ placement: "belowEditor" },
|
|
862
969
|
);
|
|
863
970
|
widgetVisible = true;
|
|
971
|
+
widgetEntryKey = entryKey;
|
|
864
972
|
};
|
|
865
973
|
|
|
866
974
|
const updateIndicator = () => {
|
|
975
|
+
if (webCapabilityScope) notifyWebCapabilities(webCapabilityScope);
|
|
867
976
|
const ctx = lastContext;
|
|
868
977
|
if (!ctx) return;
|
|
869
978
|
try {
|
|
870
979
|
const running = activeRuns.size;
|
|
871
|
-
|
|
872
|
-
ctx.ui
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
"workflows",
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
);
|
|
882
|
-
}
|
|
980
|
+
statusWriter.write(
|
|
981
|
+
ctx.ui,
|
|
982
|
+
running === 0 && completedRuns === 0 && failedRuns === 0
|
|
983
|
+
? undefined
|
|
984
|
+
: formatActivityStatus(ctx.ui.theme, "workflows", {
|
|
985
|
+
running,
|
|
986
|
+
done: completedRuns,
|
|
987
|
+
failed: failedRuns,
|
|
988
|
+
}),
|
|
989
|
+
);
|
|
883
990
|
updateWorkflowWidget();
|
|
884
991
|
} catch {
|
|
885
992
|
// UI may be unavailable.
|
|
@@ -893,7 +1000,7 @@ export default function workflows(pi: ExtensionAPI) {
|
|
|
893
1000
|
};
|
|
894
1001
|
|
|
895
1002
|
const recordSettledRun = (details: WorkflowDetails) => {
|
|
896
|
-
settledRuns.set(details
|
|
1003
|
+
settledRuns.set(details);
|
|
897
1004
|
if (details.status === "completed") completedRuns += 1;
|
|
898
1005
|
else failedRuns += 1;
|
|
899
1006
|
};
|
|
@@ -916,10 +1023,11 @@ export default function workflows(pi: ExtensionAPI) {
|
|
|
916
1023
|
try {
|
|
917
1024
|
await showWorkflowDashboard(
|
|
918
1025
|
ctx,
|
|
919
|
-
|
|
1026
|
+
dashboardDetails,
|
|
920
1027
|
initialRunId,
|
|
921
1028
|
startedSince,
|
|
922
1029
|
stopRun,
|
|
1030
|
+
dashboardRetainedDetails,
|
|
923
1031
|
);
|
|
924
1032
|
acknowledgeSettledRuns();
|
|
925
1033
|
} finally {
|
|
@@ -953,6 +1061,20 @@ export default function workflows(pi: ExtensionAPI) {
|
|
|
953
1061
|
};
|
|
954
1062
|
|
|
955
1063
|
pi.on("session_start", (_event, ctx) => {
|
|
1064
|
+
unregisterWebCapability?.();
|
|
1065
|
+
const scope = ctx.sessionManager;
|
|
1066
|
+
webCapabilityScope = scope;
|
|
1067
|
+
unregisterWebCapability = registerWebCapability(scope, {
|
|
1068
|
+
kind: "workflows",
|
|
1069
|
+
snapshot: () =>
|
|
1070
|
+
projectWorkflowCapability([
|
|
1071
|
+
...activeDetails().values(),
|
|
1072
|
+
...settledRuns
|
|
1073
|
+
.entriesArray()
|
|
1074
|
+
.filter(([runId]) => !activeRuns.has(runId))
|
|
1075
|
+
.map(([, details]) => details),
|
|
1076
|
+
]),
|
|
1077
|
+
});
|
|
956
1078
|
registerStableToolFamily();
|
|
957
1079
|
if (ctx.hasUI) lastContext = ctx;
|
|
958
1080
|
agentTypes = loadAgentTypes({
|
|
@@ -963,7 +1085,7 @@ export default function workflows(pi: ExtensionAPI) {
|
|
|
963
1085
|
turnStartedAt = 0;
|
|
964
1086
|
completedRuns = 0;
|
|
965
1087
|
failedRuns = 0;
|
|
966
|
-
settledRuns.
|
|
1088
|
+
settledRuns.resetSession();
|
|
967
1089
|
installWorkflowNavigation(ctx);
|
|
968
1090
|
updateIndicator();
|
|
969
1091
|
|
|
@@ -1010,17 +1132,25 @@ export default function workflows(pi: ExtensionAPI) {
|
|
|
1010
1132
|
navigationLayerRegistered = false;
|
|
1011
1133
|
}
|
|
1012
1134
|
await shutdownActiveWorkflowRuns([...activeRuns.values()]);
|
|
1135
|
+
// Give deferred completions one final delivery attempt. Failed sends stay
|
|
1136
|
+
// durably pending; clearing first would discard an envelope whose initial
|
|
1137
|
+
// persistence may have failed.
|
|
1138
|
+
await resultDelivery.parentSettled();
|
|
1013
1139
|
try {
|
|
1014
1140
|
lastContext?.ui.setStatus("workflows", undefined);
|
|
1015
1141
|
lastContext?.ui.setWidget(widgetKey, undefined);
|
|
1016
1142
|
} catch {
|
|
1017
1143
|
// UI may already be disposed.
|
|
1018
1144
|
}
|
|
1145
|
+
statusWriter.reset();
|
|
1146
|
+
unregisterWebCapability?.();
|
|
1147
|
+
unregisterWebCapability = undefined;
|
|
1148
|
+
webCapabilityScope = undefined;
|
|
1019
1149
|
lastContext = undefined;
|
|
1020
1150
|
widgetVisible = false;
|
|
1151
|
+
widgetEntryKey = undefined;
|
|
1021
1152
|
requestWidgetRender = undefined;
|
|
1022
1153
|
stripState.focused = false;
|
|
1023
|
-
resultDelivery.clear();
|
|
1024
1154
|
});
|
|
1025
1155
|
|
|
1026
1156
|
pi.registerCommand("workflows", {
|
|
@@ -1127,11 +1257,11 @@ export default function workflows(pi: ExtensionAPI) {
|
|
|
1127
1257
|
const runId = `wf_${randomBytes(6).toString("hex")}`;
|
|
1128
1258
|
const runDir = path.join(getAgentDir(), "workflows", runId);
|
|
1129
1259
|
const canDeliverLater = ctx.hasUI && ctx.mode === "tui";
|
|
1130
|
-
const
|
|
1260
|
+
const launchMode = resolveWorkflowLaunchMode(
|
|
1131
1261
|
{ wait: params.wait, background: params.background },
|
|
1132
1262
|
canDeliverLater,
|
|
1133
1263
|
);
|
|
1134
|
-
const background =
|
|
1264
|
+
const background = launchMode === "detached";
|
|
1135
1265
|
const now = Date.now();
|
|
1136
1266
|
|
|
1137
1267
|
const details: WorkflowDetails = {
|
|
@@ -1146,7 +1276,7 @@ export default function workflows(pi: ExtensionAPI) {
|
|
|
1146
1276
|
agents: [],
|
|
1147
1277
|
delivery: {
|
|
1148
1278
|
id: `workflow:${runId}:terminal`,
|
|
1149
|
-
state:
|
|
1279
|
+
state: launchMode === "inline" ? "held-for-inline" : "none",
|
|
1150
1280
|
attempts: 0,
|
|
1151
1281
|
updatedAt: now,
|
|
1152
1282
|
},
|
|
@@ -1155,7 +1285,7 @@ export default function workflows(pi: ExtensionAPI) {
|
|
|
1155
1285
|
// Resume: replay cached results for calls whose content is unchanged.
|
|
1156
1286
|
// A missing or unreadable source degrades to a normal full run — resume
|
|
1157
1287
|
// is an optimization and must not become a new way to fail.
|
|
1158
|
-
const
|
|
1288
|
+
const journal = createJournalAccumulator();
|
|
1159
1289
|
let replay: ReplayCache | undefined;
|
|
1160
1290
|
if (params.resume_from_run_id) {
|
|
1161
1291
|
const source = resolveRunDir(params.resume_from_run_id);
|
|
@@ -1175,7 +1305,10 @@ export default function workflows(pi: ExtensionAPI) {
|
|
|
1175
1305
|
writeRunFile(runDir, "args.json", params.args);
|
|
1176
1306
|
persistWorkflowJson(runDir, details);
|
|
1177
1307
|
const persistence = createWorkflowPersistence(runDir, details, {
|
|
1178
|
-
journal: () =>
|
|
1308
|
+
journal: () => journal,
|
|
1309
|
+
...(workflowLifecycleTestHooks?.persistWorkflow
|
|
1310
|
+
? { persist: workflowLifecycleTestHooks.persistWorkflow }
|
|
1311
|
+
: {}),
|
|
1179
1312
|
});
|
|
1180
1313
|
|
|
1181
1314
|
// A caller wait never owns the run. All runs survive an interrupted
|
|
@@ -1219,7 +1352,7 @@ export default function workflows(pi: ExtensionAPI) {
|
|
|
1219
1352
|
if (background) return;
|
|
1220
1353
|
onUpdate?.({
|
|
1221
1354
|
content: [{ type: "text", text: summaryLine(details) }],
|
|
1222
|
-
details:
|
|
1355
|
+
details: compactWorkflowToolDetails(details),
|
|
1223
1356
|
});
|
|
1224
1357
|
};
|
|
1225
1358
|
const emit = (checkpoint = true) => {
|
|
@@ -1237,6 +1370,15 @@ export default function workflows(pi: ExtensionAPI) {
|
|
|
1237
1370
|
flush(terminal);
|
|
1238
1371
|
};
|
|
1239
1372
|
|
|
1373
|
+
const persistTerminalRecovery = () => {
|
|
1374
|
+
try {
|
|
1375
|
+
persistWorkflowTerminalState(runDir, details);
|
|
1376
|
+
} catch {
|
|
1377
|
+
// The original persistence error remains authoritative; restart
|
|
1378
|
+
// reconciliation handles the remaining uncertainty.
|
|
1379
|
+
}
|
|
1380
|
+
};
|
|
1381
|
+
|
|
1240
1382
|
const terminalize = (
|
|
1241
1383
|
status: WorkflowDetails["status"],
|
|
1242
1384
|
error?: string,
|
|
@@ -1289,7 +1431,8 @@ export default function workflows(pi: ExtensionAPI) {
|
|
|
1289
1431
|
try {
|
|
1290
1432
|
persistence.flush();
|
|
1291
1433
|
} catch (persistenceError) {
|
|
1292
|
-
details
|
|
1434
|
+
appendArtifactPersistenceFailure(details, persistenceError);
|
|
1435
|
+
persistTerminalRecovery();
|
|
1293
1436
|
}
|
|
1294
1437
|
flushNow(true);
|
|
1295
1438
|
};
|
|
@@ -1306,9 +1449,9 @@ export default function workflows(pi: ExtensionAPI) {
|
|
|
1306
1449
|
|
|
1307
1450
|
// The script's narrator. Unlike phase(), this is append-only progress
|
|
1308
1451
|
// text, so it never mutates the phase list a run is judged against.
|
|
1309
|
-
const logFn = (text: string) => {
|
|
1452
|
+
const logFn = (text: string, kind?: "pipeline-drop") => {
|
|
1310
1453
|
if (runSettled) return;
|
|
1311
|
-
appendLog(details, text, Date.now());
|
|
1454
|
+
appendLog(details, text, Date.now(), kind);
|
|
1312
1455
|
emit();
|
|
1313
1456
|
};
|
|
1314
1457
|
|
|
@@ -1629,35 +1772,90 @@ export default function workflows(pi: ExtensionAPI) {
|
|
|
1629
1772
|
});
|
|
1630
1773
|
const callKey = replayIdentity ? replayKey(replayIdentity) : undefined;
|
|
1631
1774
|
let replayBoundaryViolated = false;
|
|
1775
|
+
const persistAgentResult = (result: {
|
|
1776
|
+
output: string;
|
|
1777
|
+
structured?: unknown;
|
|
1778
|
+
}) => {
|
|
1779
|
+
try {
|
|
1780
|
+
return {
|
|
1781
|
+
ok: true as const,
|
|
1782
|
+
artifact: persistWorkflowAgentResult(runDir, index, result),
|
|
1783
|
+
};
|
|
1784
|
+
} catch (error) {
|
|
1785
|
+
return { ok: false as const, error: errorText(error) };
|
|
1786
|
+
}
|
|
1787
|
+
};
|
|
1632
1788
|
// Checked before controller.schedule on purpose: schedule() charges the
|
|
1633
1789
|
// run's agent-call budget on entry, and a replayed call runs no agent.
|
|
1634
1790
|
const cached =
|
|
1635
1791
|
callKey && replayLease.canReplay ? replay?.take(callKey) : undefined;
|
|
1636
1792
|
if (cached) {
|
|
1637
1793
|
const finishedAt = Date.now();
|
|
1638
|
-
record.invocation = transitionInvocation(record.invocation!, {
|
|
1639
|
-
status: "replayed",
|
|
1640
|
-
at: finishedAt,
|
|
1641
|
-
});
|
|
1642
|
-
record.state = "done";
|
|
1643
|
-
record.replayed = true;
|
|
1644
1794
|
record.finishedAt = finishedAt;
|
|
1645
1795
|
record.preview = sanitizeWorkflowDisplayText(
|
|
1646
1796
|
cached.output,
|
|
1647
1797
|
PREVIEW_LENGTH,
|
|
1648
1798
|
);
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1799
|
+
const judged = applyAcceptance({
|
|
1800
|
+
contract: acceptanceContract,
|
|
1801
|
+
structured: cached.structured,
|
|
1802
|
+
agentOk: true,
|
|
1803
|
+
});
|
|
1804
|
+
if (judged.ledger) record.acceptance = judged.ledger;
|
|
1805
|
+
if (!judged.ok) {
|
|
1806
|
+
const error = sanitizeWorkflowDisplayLine(
|
|
1807
|
+
judged.error ?? "Agent failed",
|
|
1653
1808
|
);
|
|
1809
|
+
record.invocation = transitionInvocation(record.invocation!, {
|
|
1810
|
+
status: "rejected",
|
|
1811
|
+
at: finishedAt,
|
|
1812
|
+
});
|
|
1813
|
+
record.state = "error";
|
|
1814
|
+
record.error = error;
|
|
1815
|
+
emit();
|
|
1816
|
+
replayLease.end();
|
|
1817
|
+
return {
|
|
1818
|
+
ok: false,
|
|
1819
|
+
output: cached.output,
|
|
1820
|
+
...(cached.structured !== undefined
|
|
1821
|
+
? { structured: cached.structured }
|
|
1822
|
+
: {}),
|
|
1823
|
+
...(record.acceptance ? { acceptance: record.acceptance } : {}),
|
|
1824
|
+
error,
|
|
1825
|
+
};
|
|
1654
1826
|
}
|
|
1655
|
-
|
|
1827
|
+
const persisted = persistAgentResult({
|
|
1656
1828
|
output: cached.output,
|
|
1657
1829
|
...(cached.structured !== undefined
|
|
1658
1830
|
? { structured: cached.structured }
|
|
1659
1831
|
: {}),
|
|
1660
1832
|
});
|
|
1833
|
+
if (!persisted.ok) {
|
|
1834
|
+
record.invocation = transitionInvocation(record.invocation!, {
|
|
1835
|
+
status: "rejected",
|
|
1836
|
+
at: finishedAt,
|
|
1837
|
+
});
|
|
1838
|
+
record.state = "error";
|
|
1839
|
+
record.error = sanitizeWorkflowDisplayLine(persisted.error);
|
|
1840
|
+
emit();
|
|
1841
|
+
replayLease.end();
|
|
1842
|
+
return {
|
|
1843
|
+
ok: false,
|
|
1844
|
+
output: cached.output,
|
|
1845
|
+
...(cached.structured !== undefined
|
|
1846
|
+
? { structured: cached.structured }
|
|
1847
|
+
: {}),
|
|
1848
|
+
...(record.acceptance ? { acceptance: record.acceptance } : {}),
|
|
1849
|
+
error: persisted.error,
|
|
1850
|
+
};
|
|
1851
|
+
}
|
|
1852
|
+
record.invocation = transitionInvocation(record.invocation!, {
|
|
1853
|
+
status: "replayed",
|
|
1854
|
+
at: finishedAt,
|
|
1855
|
+
});
|
|
1856
|
+
record.state = "done";
|
|
1857
|
+
record.replayed = true;
|
|
1858
|
+
record.resultArtifact = persisted.artifact;
|
|
1661
1859
|
const ref = handoffs.register({
|
|
1662
1860
|
callId,
|
|
1663
1861
|
settled: true,
|
|
@@ -1672,7 +1870,7 @@ export default function workflows(pi: ExtensionAPI) {
|
|
|
1672
1870
|
emit();
|
|
1673
1871
|
// Re-journal so a chain of resumes keeps working: run C resuming from
|
|
1674
1872
|
// B still finds what B replayed from A.
|
|
1675
|
-
|
|
1873
|
+
journal.append(cached);
|
|
1676
1874
|
replayLease.end();
|
|
1677
1875
|
return {
|
|
1678
1876
|
ok: true,
|
|
@@ -1852,7 +2050,18 @@ export default function workflows(pi: ExtensionAPI) {
|
|
|
1852
2050
|
});
|
|
1853
2051
|
const acceptance = judged.ledger;
|
|
1854
2052
|
if (acceptance) record.acceptance = acceptance;
|
|
1855
|
-
|
|
2053
|
+
let artifactError: string | undefined;
|
|
2054
|
+
if (judged.ok) {
|
|
2055
|
+
const persisted = persistAgentResult({
|
|
2056
|
+
output: outcome.output,
|
|
2057
|
+
...(outcome.structured !== undefined
|
|
2058
|
+
? { structured: outcome.structured }
|
|
2059
|
+
: {}),
|
|
2060
|
+
});
|
|
2061
|
+
if (persisted.ok) record.resultArtifact = persisted.artifact;
|
|
2062
|
+
else artifactError = persisted.error;
|
|
2063
|
+
}
|
|
2064
|
+
const outcomeOk = judged.ok && artifactError === undefined;
|
|
1856
2065
|
record.invocation = transitionInvocation(record.invocation!, {
|
|
1857
2066
|
status: "settled",
|
|
1858
2067
|
outcome: outcomeOk ? "success" : "error",
|
|
@@ -1860,21 +2069,11 @@ export default function workflows(pi: ExtensionAPI) {
|
|
|
1860
2069
|
});
|
|
1861
2070
|
record.state = outcomeOk ? "done" : "error";
|
|
1862
2071
|
if (outcomeOk) delete record.error;
|
|
1863
|
-
else
|
|
1864
|
-
|
|
1865
|
-
|
|
2072
|
+
else {
|
|
2073
|
+
const failureError = artifactError ?? judged.error;
|
|
2074
|
+
record.error = failureError
|
|
2075
|
+
? sanitizeWorkflowDisplayLine(failureError)
|
|
1866
2076
|
: undefined;
|
|
1867
|
-
if (outcomeOk) {
|
|
1868
|
-
record.resultArtifact = persistWorkflowAgentResult(
|
|
1869
|
-
runDir,
|
|
1870
|
-
index,
|
|
1871
|
-
{
|
|
1872
|
-
output: outcome.output,
|
|
1873
|
-
...(outcome.structured !== undefined
|
|
1874
|
-
? { structured: outcome.structured }
|
|
1875
|
-
: {}),
|
|
1876
|
-
},
|
|
1877
|
-
);
|
|
1878
2077
|
}
|
|
1879
2078
|
const ref = handoffs.register({
|
|
1880
2079
|
callId,
|
|
@@ -1915,7 +2114,7 @@ export default function workflows(pi: ExtensionAPI) {
|
|
|
1915
2114
|
!replayBoundaryViolated &&
|
|
1916
2115
|
replayLease.canJournal()
|
|
1917
2116
|
) {
|
|
1918
|
-
|
|
2117
|
+
journal.append({
|
|
1919
2118
|
key: completedKey,
|
|
1920
2119
|
output: outcome.output,
|
|
1921
2120
|
...(outcome.structured !== undefined
|
|
@@ -1959,7 +2158,10 @@ export default function workflows(pi: ExtensionAPI) {
|
|
|
1959
2158
|
detached: false,
|
|
1960
2159
|
};
|
|
1961
2160
|
} else {
|
|
1962
|
-
|
|
2161
|
+
const reclaimer =
|
|
2162
|
+
workflowLifecycleTestHooks?.reclaimWorktree ??
|
|
2163
|
+
reclaimWorktree;
|
|
2164
|
+
cleanup = await reclaimer(ctx.cwd, worktree).catch(
|
|
1963
2165
|
(error): WorktreeCleanup => ({
|
|
1964
2166
|
removed: false,
|
|
1965
2167
|
branchDeleted: false,
|
|
@@ -1981,13 +2183,21 @@ export default function workflows(pi: ExtensionAPI) {
|
|
|
1981
2183
|
};
|
|
1982
2184
|
}
|
|
1983
2185
|
}
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
2186
|
+
record.worktreeCleanup = cleanup;
|
|
2187
|
+
if (cleanup.branchDeleted) delete record.worktreeBranch;
|
|
2188
|
+
else record.worktreeBranch = cleanup.branch;
|
|
2189
|
+
if (!cleanup.removed) record.worktreePath = worktree.path;
|
|
2190
|
+
// Forced settlement fixes the execution verdict, but cleanup
|
|
2191
|
+
// provenance discovered afterward still belongs in the run.
|
|
2192
|
+
// No later final flush remains, so failures must be observable.
|
|
2193
|
+
if (runSettled) {
|
|
2194
|
+
try {
|
|
2195
|
+
persistence.flush();
|
|
2196
|
+
} catch (error) {
|
|
2197
|
+
appendArtifactPersistenceFailure(details, error);
|
|
2198
|
+
persistTerminalRecovery();
|
|
2199
|
+
}
|
|
2200
|
+
} else emit();
|
|
1991
2201
|
}
|
|
1992
2202
|
}
|
|
1993
2203
|
}, invocationSignal)
|
|
@@ -2041,8 +2251,8 @@ export default function workflows(pi: ExtensionAPI) {
|
|
|
2041
2251
|
try {
|
|
2042
2252
|
persistence.flush();
|
|
2043
2253
|
} catch (error) {
|
|
2044
|
-
details
|
|
2045
|
-
|
|
2254
|
+
appendArtifactPersistenceFailure(details, error);
|
|
2255
|
+
persistTerminalRecovery();
|
|
2046
2256
|
throw new Error(details.error);
|
|
2047
2257
|
} finally {
|
|
2048
2258
|
flushNow(true);
|
|
@@ -2059,6 +2269,7 @@ export default function workflows(pi: ExtensionAPI) {
|
|
|
2059
2269
|
activeRuns.set(runId, activeRun);
|
|
2060
2270
|
const completion = runScript();
|
|
2061
2271
|
activeRun.completion = completion;
|
|
2272
|
+
workflowLifecycleTestHooks?.onRunStarted?.(activeRun);
|
|
2062
2273
|
if (ctx.hasUI) lastContext = ctx;
|
|
2063
2274
|
updateIndicator();
|
|
2064
2275
|
|
|
@@ -2072,8 +2283,8 @@ export default function workflows(pi: ExtensionAPI) {
|
|
|
2072
2283
|
try {
|
|
2073
2284
|
await completion;
|
|
2074
2285
|
} catch (error) {
|
|
2075
|
-
details.status = "failed";
|
|
2076
|
-
details.finishedAt
|
|
2286
|
+
if (details.status === "running") details.status = "failed";
|
|
2287
|
+
details.finishedAt ??= Date.now();
|
|
2077
2288
|
details.error = details.error ?? errorText(error);
|
|
2078
2289
|
} finally {
|
|
2079
2290
|
recordTerminalRun();
|
|
@@ -2096,7 +2307,7 @@ export default function workflows(pi: ExtensionAPI) {
|
|
|
2096
2307
|
}),
|
|
2097
2308
|
},
|
|
2098
2309
|
],
|
|
2099
|
-
details:
|
|
2310
|
+
details: compactWorkflowToolDetails(details),
|
|
2100
2311
|
};
|
|
2101
2312
|
}
|
|
2102
2313
|
|
|
@@ -2125,7 +2336,7 @@ export default function workflows(pi: ExtensionAPI) {
|
|
|
2125
2336
|
),
|
|
2126
2337
|
},
|
|
2127
2338
|
],
|
|
2128
|
-
details:
|
|
2339
|
+
details: compactWorkflowToolDetails(details),
|
|
2129
2340
|
};
|
|
2130
2341
|
},
|
|
2131
2342
|
|
|
@@ -2137,7 +2348,11 @@ export default function workflows(pi: ExtensionAPI) {
|
|
|
2137
2348
|
let text =
|
|
2138
2349
|
theme.fg("toolTitle", theme.bold("workflow ")) +
|
|
2139
2350
|
theme.fg("accent", (meta as WorkflowMeta).name ?? "(script)");
|
|
2140
|
-
if (args.background
|
|
2351
|
+
if (args.background !== undefined) {
|
|
2352
|
+
text += theme.fg("dim", ` (deprecated: use wait: ${!args.background})`);
|
|
2353
|
+
} else if (args.wait === true) {
|
|
2354
|
+
text += theme.fg("dim", " (wait)");
|
|
2355
|
+
}
|
|
2141
2356
|
const description = (meta as WorkflowMeta).description;
|
|
2142
2357
|
if (description) text += `\n ${theme.fg("dim", description)}`;
|
|
2143
2358
|
for (const phase of meta.phases.slice(0, 8)) {
|
|
@@ -2149,8 +2364,8 @@ export default function workflows(pi: ExtensionAPI) {
|
|
|
2149
2364
|
},
|
|
2150
2365
|
|
|
2151
2366
|
renderResult(result, { expanded, isPartial }, theme, context) {
|
|
2152
|
-
const details = result.details
|
|
2153
|
-
if (!details) {
|
|
2367
|
+
const details = result.details;
|
|
2368
|
+
if (!isWorkflowRenderDetails(details)) {
|
|
2154
2369
|
const first = result.content[0];
|
|
2155
2370
|
return new Text(
|
|
2156
2371
|
first?.type === "text" ? first.text : "(no output)",
|
|
@@ -2158,15 +2373,18 @@ export default function workflows(pi: ExtensionAPI) {
|
|
|
2158
2373
|
0,
|
|
2159
2374
|
);
|
|
2160
2375
|
}
|
|
2376
|
+
// A settled Pi tool result is committed transcript history. Keep its
|
|
2377
|
+
// launch snapshot stable; live run state belongs to the strip/dashboard.
|
|
2378
|
+
const settledAt = Date.now();
|
|
2161
2379
|
const currentDetails = () =>
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
2380
|
+
isPartial
|
|
2381
|
+
? (activeRuns.get(details.runId)?.details ??
|
|
2382
|
+
settledRuns.get(details.runId) ??
|
|
2383
|
+
details)
|
|
2384
|
+
: details;
|
|
2165
2385
|
syncWorkflowSpinner(
|
|
2166
2386
|
context.state as WorkflowRenderState,
|
|
2167
|
-
() =>
|
|
2168
|
-
currentDetails().status === "running" &&
|
|
2169
|
-
(isPartial || activeRuns.has(details.runId)),
|
|
2387
|
+
() => isPartial && currentDetails().status === "running",
|
|
2170
2388
|
context.invalidate,
|
|
2171
2389
|
);
|
|
2172
2390
|
|
|
@@ -2174,21 +2392,13 @@ export default function workflows(pi: ExtensionAPI) {
|
|
|
2174
2392
|
render(width: number) {
|
|
2175
2393
|
const current = currentDetails();
|
|
2176
2394
|
const totals = formatUsage(aggregateUsage(current.agents));
|
|
2395
|
+
const now = isPartial ? Date.now() : settledAt;
|
|
2177
2396
|
if (!expanded) {
|
|
2178
|
-
return buildCollapsedRows(
|
|
2179
|
-
current,
|
|
2180
|
-
theme,
|
|
2181
|
-
width,
|
|
2182
|
-
Date.now(),
|
|
2183
|
-
totals,
|
|
2184
|
-
);
|
|
2397
|
+
return buildCollapsedRows(current, theme, width, now, totals);
|
|
2185
2398
|
}
|
|
2186
|
-
return buildExpandedWorkflow(
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
Date.now(),
|
|
2190
|
-
totals,
|
|
2191
|
-
).render(width);
|
|
2399
|
+
return buildExpandedWorkflow(current, theme, now, totals).render(
|
|
2400
|
+
width,
|
|
2401
|
+
);
|
|
2192
2402
|
},
|
|
2193
2403
|
invalidate() {},
|
|
2194
2404
|
};
|
|
@@ -2206,24 +2416,25 @@ export default function workflows(pi: ExtensionAPI) {
|
|
|
2206
2416
|
|
|
2207
2417
|
const active = activeRuns.get(resolution.runId);
|
|
2208
2418
|
if (active) return { ok: true, details: active.details } as const;
|
|
2209
|
-
const settled = settledRuns.get(resolution.runId);
|
|
2210
|
-
if (settled) return { ok: true, details: settled } as const;
|
|
2211
|
-
|
|
2212
2419
|
const details = readPersistedWorkflowDetails(resolution.runId, {
|
|
2213
2420
|
hydrateArtifacts: true,
|
|
2214
2421
|
});
|
|
2215
|
-
if (
|
|
2422
|
+
if (details) {
|
|
2423
|
+
// A run absent from activeRuns cannot still be running this session; a
|
|
2424
|
+
// persisted "running" is a run that was hard-killed or missed the
|
|
2425
|
+
// shutdown settle deadline.
|
|
2216
2426
|
return {
|
|
2217
|
-
ok:
|
|
2218
|
-
|
|
2427
|
+
ok: true,
|
|
2428
|
+
details: recoverStaleWorkflowDetails(details),
|
|
2219
2429
|
} as const;
|
|
2220
2430
|
}
|
|
2221
|
-
//
|
|
2222
|
-
//
|
|
2223
|
-
|
|
2431
|
+
// Keep the bounded projection as a diagnostic fallback when an artifact is
|
|
2432
|
+
// temporarily unreadable. An explicit id still resolves to a known run.
|
|
2433
|
+
const settled = settledRuns.get(resolution.runId);
|
|
2434
|
+
if (settled) return { ok: true, details: settled } as const;
|
|
2224
2435
|
return {
|
|
2225
|
-
ok:
|
|
2226
|
-
|
|
2436
|
+
ok: false,
|
|
2437
|
+
error: `Workflow run ${resolution.runId} could not be read.`,
|
|
2227
2438
|
} as const;
|
|
2228
2439
|
};
|
|
2229
2440
|
|
|
@@ -2296,32 +2507,57 @@ export default function workflows(pi: ExtensionAPI) {
|
|
|
2296
2507
|
if (!resolution.ok) throw new Error(resolution.error);
|
|
2297
2508
|
const details = resolution.details;
|
|
2298
2509
|
const runDir = path.join(getAgentDir(), "workflows", details.runId);
|
|
2510
|
+
const retention = settledRuns.stats;
|
|
2299
2511
|
return Promise.resolve({
|
|
2300
2512
|
content: [
|
|
2301
2513
|
{ type: "text", text: buildWorkflowStatusSummary(details, runDir) },
|
|
2302
2514
|
],
|
|
2303
|
-
details: {
|
|
2515
|
+
details: {
|
|
2516
|
+
runs: [summarize(details)],
|
|
2517
|
+
retention,
|
|
2518
|
+
settledRunsEvicted: retention.settledRunsEvicted,
|
|
2519
|
+
},
|
|
2304
2520
|
});
|
|
2305
2521
|
}
|
|
2306
2522
|
const runs = [
|
|
2307
2523
|
...[...activeRuns.values()].map((run) => run.details),
|
|
2308
2524
|
...settledRuns.values(),
|
|
2309
2525
|
];
|
|
2526
|
+
const retention = settledRuns.stats;
|
|
2310
2527
|
if (runs.length === 0) {
|
|
2311
2528
|
return Promise.resolve({
|
|
2312
2529
|
content: [
|
|
2313
|
-
{
|
|
2530
|
+
{
|
|
2531
|
+
type: "text",
|
|
2532
|
+
text:
|
|
2533
|
+
retention.evictedRuns > 0
|
|
2534
|
+
? `No active or retained workflows. ${retention.evictedRuns} settled run(s) omitted from memory in the current session; canonical artifacts remain available on disk.`
|
|
2535
|
+
: "No active or recently finished workflows.",
|
|
2536
|
+
},
|
|
2314
2537
|
],
|
|
2315
|
-
details: {
|
|
2538
|
+
details: {
|
|
2539
|
+
runs: [],
|
|
2540
|
+
retention,
|
|
2541
|
+
settledRunsEvicted: retention.settledRunsEvicted,
|
|
2542
|
+
},
|
|
2316
2543
|
});
|
|
2317
2544
|
}
|
|
2318
2545
|
const lines = runs.map((d) => {
|
|
2319
2546
|
const { done, failed, uncertain } = countStates(d);
|
|
2320
2547
|
return `${d.runId}${d.name ? ` "${d.name}"` : ""} — ${statusWord(d.status)} · ${done + failed}/${d.agents.length} agents${failed ? `, ${failed} failed` : ""}${uncertain ? `, ${uncertain} uncertain` : ""}`;
|
|
2321
2548
|
});
|
|
2549
|
+
if (retention.evictedRuns > 0) {
|
|
2550
|
+
lines.push(
|
|
2551
|
+
`Retention (current session): ${retention.retainedRuns} settled projection(s) retained; ${retention.evictedRuns} evicted/omitted (${retention.evictedBytes} UTF-8 bytes). Canonical artifacts remain available on disk.`,
|
|
2552
|
+
);
|
|
2553
|
+
}
|
|
2322
2554
|
return Promise.resolve({
|
|
2323
2555
|
content: [{ type: "text", text: lines.join("\n") }],
|
|
2324
|
-
details: {
|
|
2556
|
+
details: {
|
|
2557
|
+
runs: runs.map(summarize),
|
|
2558
|
+
retention,
|
|
2559
|
+
settledRunsEvicted: retention.settledRunsEvicted,
|
|
2560
|
+
},
|
|
2325
2561
|
});
|
|
2326
2562
|
},
|
|
2327
2563
|
});
|
|
@@ -2329,7 +2565,6 @@ export default function workflows(pi: ExtensionAPI) {
|
|
|
2329
2565
|
pi.registerMessageRenderer(
|
|
2330
2566
|
"workflow-result",
|
|
2331
2567
|
(message, { expanded }, theme) => {
|
|
2332
|
-
const details = message.details as WorkflowDetails | undefined;
|
|
2333
2568
|
const body =
|
|
2334
2569
|
typeof message.content === "string"
|
|
2335
2570
|
? message.content
|
|
@@ -2337,18 +2572,73 @@ export default function workflows(pi: ExtensionAPI) {
|
|
|
2337
2572
|
?.map((part) => (part.type === "text" ? part.text : ""))
|
|
2338
2573
|
.join("") ?? "");
|
|
2339
2574
|
const safeBody = sanitizeWorkflowDisplayText(body);
|
|
2340
|
-
|
|
2341
|
-
|
|
2342
|
-
|
|
2343
|
-
|
|
2344
|
-
|
|
2345
|
-
|
|
2346
|
-
|
|
2347
|
-
|
|
2348
|
-
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
|
|
2575
|
+
const display = isWorkflowCompletionDisplay(message.details)
|
|
2576
|
+
? message.details
|
|
2577
|
+
: undefined;
|
|
2578
|
+
const legacyDetails = isWorkflowRenderDetails(message.details)
|
|
2579
|
+
? message.details
|
|
2580
|
+
: undefined;
|
|
2581
|
+
if (!display && !legacyDetails) {
|
|
2582
|
+
return new Text(safeBody, 0, 0);
|
|
2583
|
+
}
|
|
2584
|
+
if (legacyDetails) {
|
|
2585
|
+
const headerParts = runHeader(legacyDetails, theme, Date.now());
|
|
2586
|
+
const header = headerParts.right
|
|
2587
|
+
? `${headerParts.left} ${headerParts.right}`
|
|
2588
|
+
: headerParts.left;
|
|
2589
|
+
if (expanded) return new Text(`${header}\n\n${safeBody}`, 0, 0);
|
|
2590
|
+
const preview = safeBody.split("\n").slice(0, 8).join("\n");
|
|
2591
|
+
return new Text(
|
|
2592
|
+
`${header}\n${preview}\n${theme.fg("muted", `(${keyHint("app.tools.expand", "to expand")})`)}`,
|
|
2593
|
+
0,
|
|
2594
|
+
0,
|
|
2595
|
+
);
|
|
2596
|
+
}
|
|
2597
|
+
if (!display) return new Text(safeBody, 0, 0);
|
|
2598
|
+
if (expanded) {
|
|
2599
|
+
return new Text(buildExpandedWorkflowCompletion(display), 0, 0);
|
|
2600
|
+
}
|
|
2601
|
+
return {
|
|
2602
|
+
render(width: number) {
|
|
2603
|
+
const rows: string[] = [];
|
|
2604
|
+
for (const entry of display.entries) {
|
|
2605
|
+
rows.push(
|
|
2606
|
+
truncateToWidth(
|
|
2607
|
+
`${statusGlyph(entry.status, theme, Date.now())} ${workflowCompletionSummary(entry)}`,
|
|
2608
|
+
width,
|
|
2609
|
+
"…",
|
|
2610
|
+
),
|
|
2611
|
+
);
|
|
2612
|
+
for (const alert of workflowCompletionAlerts(entry)) {
|
|
2613
|
+
rows.push(
|
|
2614
|
+
truncateToWidth(` ${theme.fg("error", alert)}`, width, "…"),
|
|
2615
|
+
);
|
|
2616
|
+
}
|
|
2617
|
+
const result = workflowCompletionResultPreview(entry);
|
|
2618
|
+
if (result) {
|
|
2619
|
+
rows.push(
|
|
2620
|
+
truncateToWidth(
|
|
2621
|
+
` ${theme.fg("accent", "Result:")} ${result}`,
|
|
2622
|
+
width,
|
|
2623
|
+
"…",
|
|
2624
|
+
),
|
|
2625
|
+
);
|
|
2626
|
+
}
|
|
2627
|
+
}
|
|
2628
|
+
rows.push(
|
|
2629
|
+
truncateToWidth(
|
|
2630
|
+
theme.fg(
|
|
2631
|
+
"muted",
|
|
2632
|
+
`(${keyHint("app.tools.expand", "to expand")})`,
|
|
2633
|
+
),
|
|
2634
|
+
width,
|
|
2635
|
+
"…",
|
|
2636
|
+
),
|
|
2637
|
+
);
|
|
2638
|
+
return rows;
|
|
2639
|
+
},
|
|
2640
|
+
invalidate() {},
|
|
2641
|
+
};
|
|
2352
2642
|
},
|
|
2353
2643
|
);
|
|
2354
2644
|
}
|