@abelxiaoxing/cadence 1.8.4 → 2.0.1
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 +324 -117
- package/THIRD_PARTY_NOTICES.md +15 -0
- package/agents/design-explorer.md +14 -18
- package/agents/diagnosis-worker.md +14 -14
- package/agents/implementation-worker.md +16 -17
- package/config/workflow-task.example.json +7 -0
- package/docs/remote-access.md +60 -0
- package/docs/reuse-and-distribution.md +44 -0
- package/docs/service-management.md +108 -0
- package/licenses/bundled-web.txt +6245 -0
- package/licenses/pi-web-ui-MIT.txt +21 -0
- package/licenses/pi-web-ui-reuse.md +22 -0
- package/package.json +92 -20
- package/src/index.ts +21 -1802
- package/src/native-build.ts +6 -0
- package/src/native-client.ts +103 -0
- package/src/native-export.ts +58 -0
- package/src/native-history.ts +359 -0
- package/src/operator-cli.mjs +453 -1937
- package/src/plan-proposal.ts +87 -0
- package/src/sdk-worker.mjs +549 -0
- package/src/service/contracts/active.ts +82 -0
- package/src/service/contracts/budget.ts +148 -0
- package/src/service/contracts/build.ts +67 -0
- package/src/service/contracts/capture.ts +48 -0
- package/src/service/contracts/core.ts +33 -0
- package/src/service/contracts/current.ts +261 -0
- package/src/service/contracts/domain.ts +68 -0
- package/src/service/contracts/history.ts +2245 -0
- package/src/service/contracts/models.ts +139 -0
- package/src/service/coordination.ts +862 -0
- package/src/service/private-files.ts +204 -0
- package/src/service-cli.mjs +5592 -0
- package/src/service-client.ts +747 -0
- package/src/service-contract.ts +245 -0
- package/src/service-host.mjs +22382 -0
- package/src/service-http-client.ts +425 -0
- package/src/service-wire.ts +372 -0
- package/src/task-shell.ts +1045 -0
- package/src/task-store.ts +479 -0
- package/src/task-types.ts +184 -0
- package/src/verification-diagnostics.ts +29 -72
- package/src/worker-capture.mjs +1197 -0
- package/src/worker-public.mjs +1753 -0
- package/src/worker-redaction.mjs +608 -0
- package/src/worker-tools.mjs +202 -0
- package/web/dist/assets/KaTeX_AMS-Regular.ttf +0 -0
- package/web/dist/assets/KaTeX_AMS-Regular.woff +0 -0
- package/web/dist/assets/KaTeX_AMS-Regular.woff2 +0 -0
- package/web/dist/assets/KaTeX_Caligraphic-Bold.ttf +0 -0
- package/web/dist/assets/KaTeX_Caligraphic-Bold.woff +0 -0
- package/web/dist/assets/KaTeX_Caligraphic-Bold.woff2 +0 -0
- package/web/dist/assets/KaTeX_Caligraphic-Regular.ttf +0 -0
- package/web/dist/assets/KaTeX_Caligraphic-Regular.woff +0 -0
- package/web/dist/assets/KaTeX_Caligraphic-Regular.woff2 +0 -0
- package/web/dist/assets/KaTeX_Fraktur-Bold.ttf +0 -0
- package/web/dist/assets/KaTeX_Fraktur-Bold.woff +0 -0
- package/web/dist/assets/KaTeX_Fraktur-Bold.woff2 +0 -0
- package/web/dist/assets/KaTeX_Fraktur-Regular.ttf +0 -0
- package/web/dist/assets/KaTeX_Fraktur-Regular.woff +0 -0
- package/web/dist/assets/KaTeX_Fraktur-Regular.woff2 +0 -0
- package/web/dist/assets/KaTeX_Main-Bold.ttf +0 -0
- package/web/dist/assets/KaTeX_Main-Bold.woff +0 -0
- package/web/dist/assets/KaTeX_Main-Bold.woff2 +0 -0
- package/web/dist/assets/KaTeX_Main-BoldItalic.ttf +0 -0
- package/web/dist/assets/KaTeX_Main-BoldItalic.woff +0 -0
- package/web/dist/assets/KaTeX_Main-BoldItalic.woff2 +0 -0
- package/web/dist/assets/KaTeX_Main-Italic.ttf +0 -0
- package/web/dist/assets/KaTeX_Main-Italic.woff +0 -0
- package/web/dist/assets/KaTeX_Main-Italic.woff2 +0 -0
- package/web/dist/assets/KaTeX_Main-Regular.ttf +0 -0
- package/web/dist/assets/KaTeX_Main-Regular.woff +0 -0
- package/web/dist/assets/KaTeX_Main-Regular.woff2 +0 -0
- package/web/dist/assets/KaTeX_Math-BoldItalic.ttf +0 -0
- package/web/dist/assets/KaTeX_Math-BoldItalic.woff +0 -0
- package/web/dist/assets/KaTeX_Math-BoldItalic.woff2 +0 -0
- package/web/dist/assets/KaTeX_Math-Italic.ttf +0 -0
- package/web/dist/assets/KaTeX_Math-Italic.woff +0 -0
- package/web/dist/assets/KaTeX_Math-Italic.woff2 +0 -0
- package/web/dist/assets/KaTeX_SansSerif-Bold.ttf +0 -0
- package/web/dist/assets/KaTeX_SansSerif-Bold.woff +0 -0
- package/web/dist/assets/KaTeX_SansSerif-Bold.woff2 +0 -0
- package/web/dist/assets/KaTeX_SansSerif-Italic.ttf +0 -0
- package/web/dist/assets/KaTeX_SansSerif-Italic.woff +0 -0
- package/web/dist/assets/KaTeX_SansSerif-Italic.woff2 +0 -0
- package/web/dist/assets/KaTeX_SansSerif-Regular.ttf +0 -0
- package/web/dist/assets/KaTeX_SansSerif-Regular.woff +0 -0
- package/web/dist/assets/KaTeX_SansSerif-Regular.woff2 +0 -0
- package/web/dist/assets/KaTeX_Script-Regular.ttf +0 -0
- package/web/dist/assets/KaTeX_Script-Regular.woff +0 -0
- package/web/dist/assets/KaTeX_Script-Regular.woff2 +0 -0
- package/web/dist/assets/KaTeX_Size1-Regular.ttf +0 -0
- package/web/dist/assets/KaTeX_Size1-Regular.woff +0 -0
- package/web/dist/assets/KaTeX_Size1-Regular.woff2 +0 -0
- package/web/dist/assets/KaTeX_Size2-Regular.ttf +0 -0
- package/web/dist/assets/KaTeX_Size2-Regular.woff +0 -0
- package/web/dist/assets/KaTeX_Size2-Regular.woff2 +0 -0
- package/web/dist/assets/KaTeX_Size3-Regular.ttf +0 -0
- package/web/dist/assets/KaTeX_Size3-Regular.woff +0 -0
- package/web/dist/assets/KaTeX_Size3-Regular.woff2 +0 -0
- package/web/dist/assets/KaTeX_Size4-Regular.ttf +0 -0
- package/web/dist/assets/KaTeX_Size4-Regular.woff +0 -0
- package/web/dist/assets/KaTeX_Size4-Regular.woff2 +0 -0
- package/web/dist/assets/KaTeX_Typewriter-Regular.ttf +0 -0
- package/web/dist/assets/KaTeX_Typewriter-Regular.woff +0 -0
- package/web/dist/assets/KaTeX_Typewriter-Regular.woff2 +0 -0
- package/web/dist/assets/cadence.css +2 -0
- package/web/dist/assets/cadence.js +7 -0
- package/web/dist/assets/markdown.js +296 -0
- package/web/dist/assets/react.js +9 -0
- package/web/dist/assets/rolldown-runtime.js +1 -0
- package/web/dist/assets/timeline-client.js +41 -0
- package/web/dist/assets/vis-timeline.css +1274 -0
- package/web/dist/assets/vis-timeline.js +32 -0
- package/web/dist/index.html +18 -0
- package/web/dist/manifest.json +505 -0
- package/config/plan-draft.example.json +0 -125
- package/config/plan-draft.multiple-tasks.example.json +0 -375
- package/config/plan-draft.quick.example.json +0 -78
- package/prompts/abel-design.md +0 -382
- package/prompts/abel-diagnose.md +0 -71
- package/prompts/abel-implement.md +0 -284
- package/prompts/abel-init.md +0 -52
- package/src/activation.ts +0 -44
- package/src/activity-contracts.ts +0 -82
- package/src/agent-registry.ts +0 -53
- package/src/apply-transaction.ts +0 -1437
- package/src/artifact-store.ts +0 -213
- package/src/build-windows-job.mjs +0 -95
- package/src/candidate-artifact.ts +0 -143
- package/src/candidate-context.ts +0 -289
- package/src/candidate-patch.ts +0 -271
- package/src/candidate-workspace.ts +0 -80
- package/src/canonical.ts +0 -16
- package/src/change-contract-schema.ts +0 -191
- package/src/change-contract.ts +0 -324
- package/src/change-verification.ts +0 -1617
- package/src/contracts.ts +0 -2466
- package/src/control-contracts.ts +0 -382
- package/src/delivery-compiler.ts +0 -1805
- package/src/delivery-revision.ts +0 -42
- package/src/design-control.ts +0 -1299
- package/src/design-diagnostics.ts +0 -181
- package/src/design-journal.ts +0 -1773
- package/src/directory-sync.ts +0 -36
- package/src/durable-contracts.ts +0 -227
- package/src/durable-workflow.ts +0 -822
- package/src/evidence-draft.ts +0 -122
- package/src/execution-profile.ts +0 -64
- package/src/execution-retention.ts +0 -77
- package/src/file-snapshot.ts +0 -163
- package/src/implement-continuation.ts +0 -559
- package/src/implement-graph.ts +0 -690
- package/src/implement-plan.ts +0 -72
- package/src/install-config.d.mts +0 -6
- package/src/isolation-backend.ts +0 -353
- package/src/model-source.ts +0 -22
- package/src/openspec-cli.ts +0 -494
- package/src/operator-cli-entry.mjs +0 -24
- package/src/operator-tools.ts +0 -309
- package/src/package-candidate-process.ts +0 -206
- package/src/package-candidate.ts +0 -24
- package/src/package-delivery.ts +0 -486
- package/src/package-state.ts +0 -320
- package/src/package-verification.ts +0 -635
- package/src/package-workflow.ts +0 -305
- package/src/packet-runtime.ts +0 -459
- package/src/phase-execution.ts +0 -2163
- package/src/pi-adapter.ts +0 -19
- package/src/plan-draft-summary.ts +0 -527
- package/src/plan-draft.ts +0 -502
- package/src/remove-path.ts +0 -32
- package/src/run-state.ts +0 -420
- package/src/run-store.ts +0 -1179
- package/src/safe-path.ts +0 -77
- package/src/scoped-grep-worker.mjs +0 -23
- package/src/scoped-tools.ts +0 -775
- package/src/single-task-draft.ts +0 -142
- package/src/sqlite-schema.ts +0 -171
- package/src/state-root.ts +0 -215
- package/src/storage-schema.ts +0 -360
- package/src/subagent-activity.ts +0 -1417
- package/src/subagent-process.ts +0 -476
- package/src/task-ledger.ts +0 -1338
- package/src/transport-budget.ts +0 -50
- package/src/verification-capability.ts +0 -1191
- package/src/verification-environment-io.ts +0 -389
- package/src/verification-environment.ts +0 -162
- package/src/verification-identity.ts +0 -77
- package/src/verification-scan.ts +0 -64
- package/src/windows-job-backend.ts +0 -419
- package/src/windows-job.c +0 -149
- package/src/workflow-engine.ts +0 -28
- package/src/workflow-policy.ts +0 -1670
- package/src/workflow-recovery-policy.ts +0 -97
- package/src/workflow-scheduling.ts +0 -105
- package/src/workflow-state-machine.ts +0 -4341
- package/src/workflow-status.ts +0 -576
- package/src/workspace-io-entry.mjs +0 -76
- package/src/workspace-io-worker.mjs +0 -1312
- package/src/workspace-io.ts +0 -89
- package/src/workspace-store.ts +0 -634
package/src/subagent-activity.ts
DELETED
|
@@ -1,1417 +0,0 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
ExtensionUIContext,
|
|
3
|
-
ToolRenderResultOptions,
|
|
4
|
-
} from "@earendil-works/pi-coding-agent";
|
|
5
|
-
import type { Component } from "@earendil-works/pi-tui";
|
|
6
|
-
import {
|
|
7
|
-
stripTerminalSequences,
|
|
8
|
-
Text,
|
|
9
|
-
truncateToWidth,
|
|
10
|
-
visibleWidth,
|
|
11
|
-
} from "@earendil-works/pi-tui";
|
|
12
|
-
import type {
|
|
13
|
-
PacketActivityEvent,
|
|
14
|
-
PacketActivityObserver,
|
|
15
|
-
} from "./activity-contracts.ts";
|
|
16
|
-
import {
|
|
17
|
-
projectDesignDiagnostic,
|
|
18
|
-
type SafeDesignDiagnostic,
|
|
19
|
-
} from "./design-diagnostics.ts";
|
|
20
|
-
|
|
21
|
-
export const ACTIVITY_DETAILS_KEY = "activityDisplay" as const;
|
|
22
|
-
export const ACTIVITY_WIDGET_KEY = "abel-subagents" as const;
|
|
23
|
-
export const ACTIVITY_STATUS_KEY = "abel-subagents" as const;
|
|
24
|
-
export const ACTIVITY_REFRESH_MS = 100;
|
|
25
|
-
export const ACTIVITY_WIDGET_MAX_LINES = 12;
|
|
26
|
-
export const ACTIVITY_WIDGET_MIN_WIDTH = 3;
|
|
27
|
-
|
|
28
|
-
export type {
|
|
29
|
-
ActivityState,
|
|
30
|
-
TerminalActivityState,
|
|
31
|
-
WorkflowActivityState,
|
|
32
|
-
WorkflowActivityUpdate,
|
|
33
|
-
} from "./activity-contracts.ts";
|
|
34
|
-
export { WORKFLOW_ACTIVITY_STATES } from "./activity-contracts.ts";
|
|
35
|
-
|
|
36
|
-
import type {
|
|
37
|
-
ActivityState,
|
|
38
|
-
TerminalActivityState,
|
|
39
|
-
WorkflowActivityState,
|
|
40
|
-
WorkflowActivityUpdate,
|
|
41
|
-
} from "./activity-contracts.ts";
|
|
42
|
-
|
|
43
|
-
export interface EvidenceActivitySummary {
|
|
44
|
-
kind: "evidence";
|
|
45
|
-
conclusions: number;
|
|
46
|
-
citations: number;
|
|
47
|
-
risks: number;
|
|
48
|
-
blockingQuestions: number;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
export interface DiffActivitySummary {
|
|
52
|
-
kind: "diff";
|
|
53
|
-
summary: string;
|
|
54
|
-
riskCount: number;
|
|
55
|
-
retained: boolean;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
export type ActivitySummary = EvidenceActivitySummary | DiffActivitySummary;
|
|
59
|
-
export type ActivityTone = "accent" | "success" | "warning" | "muted" | "error";
|
|
60
|
-
|
|
61
|
-
export interface ActivityDisplay {
|
|
62
|
-
kind: "activityDisplay";
|
|
63
|
-
requestId: string;
|
|
64
|
-
role: string;
|
|
65
|
-
phase: string;
|
|
66
|
-
objective: string;
|
|
67
|
-
state: TerminalActivityState;
|
|
68
|
-
tone: ActivityTone;
|
|
69
|
-
elapsedMs: number;
|
|
70
|
-
summary?: ActivitySummary;
|
|
71
|
-
reason?: string;
|
|
72
|
-
runId?: string;
|
|
73
|
-
taskId?: string;
|
|
74
|
-
code?: string;
|
|
75
|
-
nextAction?: string;
|
|
76
|
-
attempt?: number;
|
|
77
|
-
maxAttempts?: number;
|
|
78
|
-
wait?: string;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
export interface ActivitySnapshot {
|
|
82
|
-
toolCallId: string;
|
|
83
|
-
requestId: string;
|
|
84
|
-
role: string;
|
|
85
|
-
phase: string;
|
|
86
|
-
objective: string;
|
|
87
|
-
state: ActivityState;
|
|
88
|
-
sequence: number;
|
|
89
|
-
startedAt: number;
|
|
90
|
-
elapsedMs: number;
|
|
91
|
-
spinnerFrame?: number;
|
|
92
|
-
tone?: ActivityTone;
|
|
93
|
-
runId?: string;
|
|
94
|
-
taskId?: string;
|
|
95
|
-
code?: string;
|
|
96
|
-
nextAction?: string;
|
|
97
|
-
attempt?: number;
|
|
98
|
-
maxAttempts?: number;
|
|
99
|
-
wait?: string;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
interface ActivityEntry extends ActivitySnapshot {
|
|
103
|
-
onUpdate?: (result: unknown) => void;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
type ActivityColor =
|
|
107
|
-
| "accent"
|
|
108
|
-
| "success"
|
|
109
|
-
| "error"
|
|
110
|
-
| "warning"
|
|
111
|
-
| "muted"
|
|
112
|
-
| "text";
|
|
113
|
-
type ActivityTheme = {
|
|
114
|
-
fg?: (color: ActivityColor, text: string) => string;
|
|
115
|
-
};
|
|
116
|
-
type ActivityUi = Pick<ExtensionUIContext, "setWidget" | "setStatus">;
|
|
117
|
-
type ActivityRenderContext = {
|
|
118
|
-
args?: unknown;
|
|
119
|
-
executionStarted?: boolean;
|
|
120
|
-
isError?: boolean;
|
|
121
|
-
};
|
|
122
|
-
type TimerHandle = ReturnType<typeof setInterval>;
|
|
123
|
-
|
|
124
|
-
export interface ActivityControllerOptions {
|
|
125
|
-
now?: () => number;
|
|
126
|
-
setInterval?: (callback: () => void, delay: number) => TimerHandle | unknown;
|
|
127
|
-
clearInterval?: (handle: TimerHandle | unknown) => void;
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
function boundedWidth(width: number): number {
|
|
131
|
-
return Math.max(0, Number.isFinite(width) ? Math.floor(width) : 0);
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
function normalizeWhitespace(value: string): string {
|
|
135
|
-
return stripTerminalSequences(value).replace(/\s+/g, " ").trim();
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
function redactPaths(value: string): string {
|
|
139
|
-
return value
|
|
140
|
-
.replace(
|
|
141
|
-
/(?:[A-Za-z]:[\\/]|\.{1,2}[\\/]|\/)(?:[^\s,;:()[\]{}]+[\\/])*[^\s,;:()[\]{}]+/g,
|
|
142
|
-
"[path]",
|
|
143
|
-
)
|
|
144
|
-
.replace(/(?:\b[\w.-]+[\\/])+[\w.-]+/g, "[path]")
|
|
145
|
-
.replace(/\b[\w.-]+\.[A-Za-z0-9]{1,12}\b/g, "[path]");
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
function redactProviderAndModel(value: string): string {
|
|
149
|
-
return value.replace(
|
|
150
|
-
/\b(?:provider|model)(?:\s+identity)?\s*(?:[:=]|\/)?\s*[^\s,;()[\]]+/gi,
|
|
151
|
-
"[redacted]",
|
|
152
|
-
);
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
export function sanitizeDisplayText(value: unknown, maxLength = 240): string {
|
|
156
|
-
const text = typeof value === "string" ? value : String(value ?? "");
|
|
157
|
-
const safe = redactProviderAndModel(redactPaths(normalizeWhitespace(text)));
|
|
158
|
-
return safe.slice(0, Math.max(0, maxLength));
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
const SAFE_WORKFLOW_CODE = /^[a-z0-9](?:[a-z0-9._:-]{0,126}[a-z0-9])?$/iu;
|
|
162
|
-
const SAFE_DESIGN_DETAIL_VALUE = /^[a-z0-9](?:[a-z0-9._:-]{0,126}[a-z0-9])?$/iu;
|
|
163
|
-
const SAFE_CONTROL_ACTIONS = new Set([
|
|
164
|
-
"start",
|
|
165
|
-
"status",
|
|
166
|
-
"resume",
|
|
167
|
-
"rebind",
|
|
168
|
-
"cancel",
|
|
169
|
-
"discard",
|
|
170
|
-
]);
|
|
171
|
-
|
|
172
|
-
export function sanitizeWorkflowCode(value: unknown): string {
|
|
173
|
-
const code = typeof value === "string" ? value : "";
|
|
174
|
-
return SAFE_WORKFLOW_CODE.test(code) ? code : "workflow-state-unavailable";
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
function sanitizeControlAction(value: unknown): string {
|
|
178
|
-
const action = typeof value === "string" ? value : "";
|
|
179
|
-
return SAFE_CONTROL_ACTIONS.has(action) ? action : "status";
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
const SAFE_FAILURE_REASONS = new Set([
|
|
183
|
-
"subagent failed",
|
|
184
|
-
"subagent cancelled",
|
|
185
|
-
"phase timed out",
|
|
186
|
-
]);
|
|
187
|
-
|
|
188
|
-
export function sanitizeFailureReason(value: unknown, maxLength = 240): string {
|
|
189
|
-
const normalized = normalizeWhitespace(String(value ?? ""));
|
|
190
|
-
const safe = SAFE_FAILURE_REASONS.has(normalized)
|
|
191
|
-
? normalized
|
|
192
|
-
: "subagent failed";
|
|
193
|
-
return safe.slice(0, Math.max(0, maxLength));
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
function safeDesignFailure(value: unknown): {
|
|
197
|
-
operation: string;
|
|
198
|
-
code: string;
|
|
199
|
-
diagnostics: SafeDesignDiagnostic[];
|
|
200
|
-
} | null {
|
|
201
|
-
const failure = asRecord(value);
|
|
202
|
-
if (
|
|
203
|
-
failure?.kind !== "design-control-failure" ||
|
|
204
|
-
typeof failure.operation !== "string" ||
|
|
205
|
-
!SAFE_DESIGN_DETAIL_VALUE.test(failure.operation) ||
|
|
206
|
-
typeof failure.code !== "string" ||
|
|
207
|
-
!SAFE_DESIGN_DETAIL_VALUE.test(failure.code) ||
|
|
208
|
-
!Array.isArray(failure.diagnostics)
|
|
209
|
-
) {
|
|
210
|
-
return null;
|
|
211
|
-
}
|
|
212
|
-
const diagnostics = failure.diagnostics.slice(0, 64).flatMap((candidate) => {
|
|
213
|
-
const diagnostic = projectDesignDiagnostic(candidate);
|
|
214
|
-
return diagnostic ? [diagnostic] : [];
|
|
215
|
-
});
|
|
216
|
-
return {
|
|
217
|
-
operation: failure.operation,
|
|
218
|
-
code: failure.code,
|
|
219
|
-
diagnostics,
|
|
220
|
-
};
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
function renderDesignFailure(
|
|
224
|
-
failure: ReturnType<typeof safeDesignFailure> & object,
|
|
225
|
-
expanded: boolean,
|
|
226
|
-
theme?: ActivityTheme,
|
|
227
|
-
): Component {
|
|
228
|
-
const title = styled(
|
|
229
|
-
theme,
|
|
230
|
-
"error",
|
|
231
|
-
`${dispatchLabel("design")} ${failure.operation} failed: ${failure.code}`,
|
|
232
|
-
);
|
|
233
|
-
if (!expanded || failure.diagnostics.length === 0) {
|
|
234
|
-
const suffix =
|
|
235
|
-
failure.diagnostics.length > 0
|
|
236
|
-
? styled(
|
|
237
|
-
theme,
|
|
238
|
-
"muted",
|
|
239
|
-
` (${failure.diagnostics.length} diagnostic${failure.diagnostics.length === 1 ? "" : "s"})`,
|
|
240
|
-
)
|
|
241
|
-
: "";
|
|
242
|
-
return new Text(`${title}${suffix}`, 0, 0);
|
|
243
|
-
}
|
|
244
|
-
const lines = failure.diagnostics.map((diagnostic) => {
|
|
245
|
-
const details = Object.entries(diagnostic)
|
|
246
|
-
.filter(([key]) => key !== "code")
|
|
247
|
-
.map(
|
|
248
|
-
([key, value]) =>
|
|
249
|
-
`${key}=${Array.isArray(value) ? JSON.stringify(value) : value}`,
|
|
250
|
-
)
|
|
251
|
-
.join(" · ");
|
|
252
|
-
return ` - ${diagnostic.code}${details ? ` · ${details}` : ""}`;
|
|
253
|
-
});
|
|
254
|
-
return new Text(
|
|
255
|
-
[title, ...lines.map((entry) => styled(theme, "muted", entry))].join("\n"),
|
|
256
|
-
0,
|
|
257
|
-
0,
|
|
258
|
-
);
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
export function formatElapsed(milliseconds: number): string {
|
|
262
|
-
const value = Math.max(
|
|
263
|
-
0,
|
|
264
|
-
Math.floor(Number.isFinite(milliseconds) ? milliseconds : 0),
|
|
265
|
-
);
|
|
266
|
-
if (value < 1_000) return `${value}ms`;
|
|
267
|
-
const seconds = Math.floor(value / 1_000);
|
|
268
|
-
if (seconds < 60) return `${seconds}s`;
|
|
269
|
-
const minutes = Math.floor(seconds / 60);
|
|
270
|
-
return `${minutes}m${String(seconds % 60).padStart(2, "0")}s`;
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
const SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
274
|
-
|
|
275
|
-
function stateGlyph(state: ActivityState, spinnerFrame = 0): string {
|
|
276
|
-
switch (state) {
|
|
277
|
-
case "queued":
|
|
278
|
-
return "…";
|
|
279
|
-
case "preparing":
|
|
280
|
-
case "connecting":
|
|
281
|
-
case "waiting-first-response":
|
|
282
|
-
case "running":
|
|
283
|
-
case "validating":
|
|
284
|
-
case "retrying":
|
|
285
|
-
case "verifying":
|
|
286
|
-
case "applying":
|
|
287
|
-
case "recovering":
|
|
288
|
-
return SPINNER_FRAMES[spinnerFrame % SPINNER_FRAMES.length] ?? "⠋";
|
|
289
|
-
case "completed":
|
|
290
|
-
return "✓";
|
|
291
|
-
case "failed":
|
|
292
|
-
case "rejected":
|
|
293
|
-
return "✗";
|
|
294
|
-
case "cancelled":
|
|
295
|
-
case "operation-cancelled":
|
|
296
|
-
return "⊘";
|
|
297
|
-
case "discarded":
|
|
298
|
-
return "◇";
|
|
299
|
-
case "paused":
|
|
300
|
-
case "approval-needed":
|
|
301
|
-
return "!";
|
|
302
|
-
case "timed-out":
|
|
303
|
-
return "⌛";
|
|
304
|
-
}
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
function line(value: string, width: number): string {
|
|
308
|
-
const bounded = truncateToWidth(value, boundedWidth(width), "");
|
|
309
|
-
return visibleWidth(bounded) <= boundedWidth(width)
|
|
310
|
-
? bounded
|
|
311
|
-
: truncateToWidth(bounded, boundedWidth(width), "");
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
function stateText(state: ActivityState): string {
|
|
315
|
-
return state;
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
function activityMetadata(
|
|
319
|
-
snapshot: ActivitySnapshot | ActivityDisplay,
|
|
320
|
-
): string[] {
|
|
321
|
-
const metadata: string[] = [];
|
|
322
|
-
if (snapshot.taskId) {
|
|
323
|
-
metadata.push(`task ${sanitizeDisplayText(snapshot.taskId, 80)}`);
|
|
324
|
-
}
|
|
325
|
-
if (snapshot.code) {
|
|
326
|
-
metadata.push(`code ${sanitizeWorkflowCode(snapshot.code)}`);
|
|
327
|
-
}
|
|
328
|
-
if (
|
|
329
|
-
Number.isSafeInteger(snapshot.attempt) &&
|
|
330
|
-
(snapshot.attempt as number) > 0
|
|
331
|
-
) {
|
|
332
|
-
metadata.push(
|
|
333
|
-
Number.isSafeInteger(snapshot.maxAttempts) &&
|
|
334
|
-
(snapshot.maxAttempts as number) >= (snapshot.attempt as number)
|
|
335
|
-
? `attempt ${snapshot.attempt}/${snapshot.maxAttempts}`
|
|
336
|
-
: `attempt ${snapshot.attempt}`,
|
|
337
|
-
);
|
|
338
|
-
}
|
|
339
|
-
if (snapshot.wait) {
|
|
340
|
-
metadata.push(`wait ${sanitizeDisplayText(snapshot.wait, 80)}`);
|
|
341
|
-
}
|
|
342
|
-
if (snapshot.nextAction) {
|
|
343
|
-
metadata.push(`next ${sanitizeControlAction(snapshot.nextAction)}`);
|
|
344
|
-
}
|
|
345
|
-
return metadata;
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
function styled(
|
|
349
|
-
theme: ActivityTheme | undefined,
|
|
350
|
-
color: ActivityColor,
|
|
351
|
-
value: string,
|
|
352
|
-
): string {
|
|
353
|
-
try {
|
|
354
|
-
return theme?.fg?.(color, value) ?? value;
|
|
355
|
-
} catch {
|
|
356
|
-
return value;
|
|
357
|
-
}
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
function renderActivityLines(
|
|
361
|
-
snapshot: ActivitySnapshot | ActivityDisplay,
|
|
362
|
-
width: number,
|
|
363
|
-
theme?: ActivityTheme,
|
|
364
|
-
now = Date.now(),
|
|
365
|
-
): string[] {
|
|
366
|
-
const elapsed =
|
|
367
|
-
"kind" in snapshot
|
|
368
|
-
? snapshot.elapsedMs
|
|
369
|
-
: Math.max(0, now - snapshot.startedAt);
|
|
370
|
-
const first = [
|
|
371
|
-
stateGlyph(
|
|
372
|
-
snapshot.state,
|
|
373
|
-
"spinnerFrame" in snapshot ? snapshot.spinnerFrame : undefined,
|
|
374
|
-
),
|
|
375
|
-
"Subagent",
|
|
376
|
-
sanitizeDisplayText(snapshot.role, 80),
|
|
377
|
-
`#${sanitizeDisplayText(snapshot.requestId, 128)}`,
|
|
378
|
-
sanitizeDisplayText(snapshot.phase, 40),
|
|
379
|
-
stateText(snapshot.state),
|
|
380
|
-
formatElapsed(elapsed),
|
|
381
|
-
].join(" · ");
|
|
382
|
-
const metadata = activityMetadata(snapshot);
|
|
383
|
-
const objective = sanitizeDisplayText(snapshot.objective, 240);
|
|
384
|
-
const second = ` ${[objective, ...metadata].filter(Boolean).join(" · ")}`;
|
|
385
|
-
const color = "tone" in snapshot && snapshot.tone ? snapshot.tone : "accent";
|
|
386
|
-
return [
|
|
387
|
-
line(styled(theme, color, first), width),
|
|
388
|
-
line(styled(theme, "muted", second), width),
|
|
389
|
-
];
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
export function renderActivityWidgetLines(
|
|
393
|
-
entries: readonly ActivitySnapshot[],
|
|
394
|
-
width: number,
|
|
395
|
-
_now = Date.now(),
|
|
396
|
-
): string[] {
|
|
397
|
-
const ordered = [...entries].sort(
|
|
398
|
-
(left, right) =>
|
|
399
|
-
left.sequence - right.sequence ||
|
|
400
|
-
left.toolCallId.localeCompare(right.toolCallId),
|
|
401
|
-
);
|
|
402
|
-
const visible = ordered.slice(0, 5);
|
|
403
|
-
const hidden = ordered.slice(visible.length);
|
|
404
|
-
const lines = [line(`Agents · ${ordered.length} active`, width)];
|
|
405
|
-
for (const entry of visible) {
|
|
406
|
-
lines.push(...renderActivityLines(entry, width, undefined, _now));
|
|
407
|
-
}
|
|
408
|
-
if (hidden.length > 0) {
|
|
409
|
-
const running = hidden.filter((entry) => entry.state !== "queued").length;
|
|
410
|
-
const queued = hidden.filter((entry) => entry.state === "queued").length;
|
|
411
|
-
const overflow = [
|
|
412
|
-
`+${hidden.length} more (${running} running, ${queued} queued)`,
|
|
413
|
-
`+${hidden.length} (${running}r, ${queued}q)`,
|
|
414
|
-
`+${hidden.length} ${running}r ${queued}q`,
|
|
415
|
-
`+${hidden.length} ${running}/${queued}`,
|
|
416
|
-
`${hidden.length}:${running}/${queued}`,
|
|
417
|
-
`${running}/${queued}`,
|
|
418
|
-
].find((candidate) => visibleWidth(candidate) <= boundedWidth(width));
|
|
419
|
-
lines.push(line(overflow ?? `${running}/${queued}`, width));
|
|
420
|
-
}
|
|
421
|
-
return lines
|
|
422
|
-
.slice(0, ACTIVITY_WIDGET_MAX_LINES)
|
|
423
|
-
.map((entry) => line(entry, width));
|
|
424
|
-
}
|
|
425
|
-
|
|
426
|
-
export class ActivityWidget implements Component {
|
|
427
|
-
constructor(
|
|
428
|
-
private readonly entries: () => readonly ActivitySnapshot[],
|
|
429
|
-
private readonly now: () => number = Date.now,
|
|
430
|
-
private readonly theme?: ActivityTheme,
|
|
431
|
-
) {}
|
|
432
|
-
|
|
433
|
-
render(width: number): string[] {
|
|
434
|
-
return renderActivityWidgetLines(this.entries(), width, this.now()).map(
|
|
435
|
-
(entry, index) =>
|
|
436
|
-
line(
|
|
437
|
-
index === 0
|
|
438
|
-
? styled(this.theme, "accent", entry)
|
|
439
|
-
: styled(this.theme, "text", entry),
|
|
440
|
-
width,
|
|
441
|
-
),
|
|
442
|
-
);
|
|
443
|
-
}
|
|
444
|
-
|
|
445
|
-
invalidate(): void {}
|
|
446
|
-
}
|
|
447
|
-
|
|
448
|
-
export class ActivityInlineComponent implements Component {
|
|
449
|
-
constructor(
|
|
450
|
-
private readonly display: ActivityDisplay | ActivitySnapshot,
|
|
451
|
-
private readonly theme?: ActivityTheme,
|
|
452
|
-
private readonly expanded = false,
|
|
453
|
-
private readonly now: () => number = Date.now,
|
|
454
|
-
) {}
|
|
455
|
-
|
|
456
|
-
render(width: number): string[] {
|
|
457
|
-
const lines = renderActivityLines(
|
|
458
|
-
this.display,
|
|
459
|
-
width,
|
|
460
|
-
this.theme,
|
|
461
|
-
this.now(),
|
|
462
|
-
);
|
|
463
|
-
if (
|
|
464
|
-
!this.expanded ||
|
|
465
|
-
!("summary" in this.display) ||
|
|
466
|
-
!this.display.summary
|
|
467
|
-
) {
|
|
468
|
-
if ("reason" in this.display && this.display.reason) {
|
|
469
|
-
lines[1] = line(
|
|
470
|
-
styled(
|
|
471
|
-
this.theme,
|
|
472
|
-
this.display.tone,
|
|
473
|
-
` reason: ${sanitizeFailureReason(this.display.reason)}`,
|
|
474
|
-
),
|
|
475
|
-
width,
|
|
476
|
-
);
|
|
477
|
-
}
|
|
478
|
-
return lines;
|
|
479
|
-
}
|
|
480
|
-
const summary = this.display.summary;
|
|
481
|
-
const detail =
|
|
482
|
-
summary.kind === "evidence"
|
|
483
|
-
? ` evidence: ${summary.conclusions} conclusions · ${summary.citations} citations · ${summary.risks} risks · ${summary.blockingQuestions} blocking`
|
|
484
|
-
: ` diff: ${sanitizeDisplayText(summary.summary)} · ${summary.riskCount} risks · retained: ${summary.retained ? "yes" : "no"}`;
|
|
485
|
-
return [lines[0] ?? "", line(styled(this.theme, "muted", detail), width)];
|
|
486
|
-
}
|
|
487
|
-
|
|
488
|
-
invalidate(): void {}
|
|
489
|
-
}
|
|
490
|
-
|
|
491
|
-
function asRecord(value: unknown): Record<string, unknown> | undefined {
|
|
492
|
-
return typeof value === "object" && value !== null
|
|
493
|
-
? (value as Record<string, unknown>)
|
|
494
|
-
: undefined;
|
|
495
|
-
}
|
|
496
|
-
|
|
497
|
-
function hasAutomaticContinuation(result: Record<string, unknown>): boolean {
|
|
498
|
-
const continuation = asRecord(result.continuation);
|
|
499
|
-
return (
|
|
500
|
-
["paused", "approval-needed"].includes(String(result.state)) &&
|
|
501
|
-
asRecord(result.pause)?.code !== "operation-cancelled" &&
|
|
502
|
-
continuation?.owner === "parent" &&
|
|
503
|
-
continuation.automatic === true &&
|
|
504
|
-
(continuation.action === "amend" ||
|
|
505
|
-
continuation.command === "resume" ||
|
|
506
|
-
continuation.kind === "inspect-recovery")
|
|
507
|
-
);
|
|
508
|
-
}
|
|
509
|
-
|
|
510
|
-
function workflowStateOf(
|
|
511
|
-
result: Record<string, unknown>,
|
|
512
|
-
): WorkflowActivityState {
|
|
513
|
-
const pause = asRecord(result.pause);
|
|
514
|
-
const operation = asRecord(result.operation);
|
|
515
|
-
if (
|
|
516
|
-
pause?.code === "operation-cancelled" ||
|
|
517
|
-
operation?.kind === "operation-cancelled" ||
|
|
518
|
-
result.state === "operation-cancelled"
|
|
519
|
-
) {
|
|
520
|
-
return "operation-cancelled";
|
|
521
|
-
}
|
|
522
|
-
if (hasAutomaticContinuation(result)) return "recovering";
|
|
523
|
-
const state = result.state;
|
|
524
|
-
switch (state) {
|
|
525
|
-
case "not-started":
|
|
526
|
-
case "created":
|
|
527
|
-
case "ready":
|
|
528
|
-
case "queued":
|
|
529
|
-
return "queued";
|
|
530
|
-
case "preparing":
|
|
531
|
-
return "preparing";
|
|
532
|
-
case "connecting":
|
|
533
|
-
return "connecting";
|
|
534
|
-
case "waiting-first-response":
|
|
535
|
-
return "waiting-first-response";
|
|
536
|
-
case "running":
|
|
537
|
-
return "running";
|
|
538
|
-
case "validating-delivery":
|
|
539
|
-
case "validating":
|
|
540
|
-
return "validating";
|
|
541
|
-
case "retryable":
|
|
542
|
-
case "retrying":
|
|
543
|
-
return "retrying";
|
|
544
|
-
case "verifying":
|
|
545
|
-
case "change-verifying":
|
|
546
|
-
case "ready-to-apply":
|
|
547
|
-
return "verifying";
|
|
548
|
-
case "paused":
|
|
549
|
-
return "paused";
|
|
550
|
-
case "approval-needed":
|
|
551
|
-
return "approval-needed";
|
|
552
|
-
case "applying":
|
|
553
|
-
return "applying";
|
|
554
|
-
case "recovering":
|
|
555
|
-
return "recovering";
|
|
556
|
-
case "discarded":
|
|
557
|
-
return "discarded";
|
|
558
|
-
case "rejected":
|
|
559
|
-
return "rejected";
|
|
560
|
-
case "completed":
|
|
561
|
-
return result.completed === true ? "completed" : "rejected";
|
|
562
|
-
default:
|
|
563
|
-
return "running";
|
|
564
|
-
}
|
|
565
|
-
}
|
|
566
|
-
|
|
567
|
-
function workflowTone(state: WorkflowActivityState): ActivityTone {
|
|
568
|
-
switch (state) {
|
|
569
|
-
case "completed":
|
|
570
|
-
return "success";
|
|
571
|
-
case "rejected":
|
|
572
|
-
return "error";
|
|
573
|
-
case "retrying":
|
|
574
|
-
case "paused":
|
|
575
|
-
case "approval-needed":
|
|
576
|
-
return "warning";
|
|
577
|
-
case "operation-cancelled":
|
|
578
|
-
case "discarded":
|
|
579
|
-
return "muted";
|
|
580
|
-
default:
|
|
581
|
-
return "accent";
|
|
582
|
-
}
|
|
583
|
-
}
|
|
584
|
-
|
|
585
|
-
function legalWorkflowCommands(result: Record<string, unknown>): string[] {
|
|
586
|
-
return Array.isArray(result.legalCommands)
|
|
587
|
-
? result.legalCommands.filter(
|
|
588
|
-
(command): command is string =>
|
|
589
|
-
typeof command === "string" && SAFE_CONTROL_ACTIONS.has(command),
|
|
590
|
-
)
|
|
591
|
-
: [];
|
|
592
|
-
}
|
|
593
|
-
|
|
594
|
-
function nextWorkflowAction(
|
|
595
|
-
state: WorkflowActivityState,
|
|
596
|
-
commands: readonly string[],
|
|
597
|
-
): string | undefined {
|
|
598
|
-
const preferred =
|
|
599
|
-
state === "paused" ||
|
|
600
|
-
state === "approval-needed" ||
|
|
601
|
-
state === "retrying" ||
|
|
602
|
-
state === "operation-cancelled"
|
|
603
|
-
? ["resume", "status", "discard"]
|
|
604
|
-
: state === "completed" || state === "discarded" || state === "rejected"
|
|
605
|
-
? ["status"]
|
|
606
|
-
: ["status", "cancel", "discard"];
|
|
607
|
-
return preferred.find((command) => commands.includes(command)) ?? commands[0];
|
|
608
|
-
}
|
|
609
|
-
|
|
610
|
-
function firstWorkflowTask(result: Record<string, unknown>):
|
|
611
|
-
| {
|
|
612
|
-
taskId?: string;
|
|
613
|
-
phase?: string;
|
|
614
|
-
}
|
|
615
|
-
| undefined {
|
|
616
|
-
if (!Array.isArray(result.tasks)) return undefined;
|
|
617
|
-
const records = result.tasks
|
|
618
|
-
.map(asRecord)
|
|
619
|
-
.filter((task): task is Record<string, unknown> => task !== undefined);
|
|
620
|
-
const task =
|
|
621
|
-
records.find((candidate) => candidate.state !== "verified") ?? records[0];
|
|
622
|
-
return task
|
|
623
|
-
? {
|
|
624
|
-
...(typeof task.taskId === "string" ? { taskId: task.taskId } : {}),
|
|
625
|
-
...(typeof task.phase === "string" ? { phase: task.phase } : {}),
|
|
626
|
-
}
|
|
627
|
-
: undefined;
|
|
628
|
-
}
|
|
629
|
-
|
|
630
|
-
function positiveInteger(value: unknown): number | undefined {
|
|
631
|
-
return Number.isSafeInteger(value) && (value as number) > 0
|
|
632
|
-
? (value as number)
|
|
633
|
-
: undefined;
|
|
634
|
-
}
|
|
635
|
-
|
|
636
|
-
function workflowWait(
|
|
637
|
-
state: WorkflowActivityState,
|
|
638
|
-
result: Record<string, unknown>,
|
|
639
|
-
): string | undefined {
|
|
640
|
-
if (typeof result.wait === "string") {
|
|
641
|
-
return sanitizeDisplayText(result.wait, 80);
|
|
642
|
-
}
|
|
643
|
-
const approval = asRecord(result.approval);
|
|
644
|
-
if (
|
|
645
|
-
state === "approval-needed" &&
|
|
646
|
-
(approval?.gate === "gate-a" || approval?.gate === "gate-b")
|
|
647
|
-
) {
|
|
648
|
-
return approval.gate;
|
|
649
|
-
}
|
|
650
|
-
if (state === "preparing") return "worker-setup";
|
|
651
|
-
if (state === "connecting") return "connection";
|
|
652
|
-
if (state === "waiting-first-response") return "first-response";
|
|
653
|
-
if (state === "retrying") return "bounded-policy";
|
|
654
|
-
const queue = Array.isArray(result.queue)
|
|
655
|
-
? asRecord(result.queue[0])
|
|
656
|
-
: undefined;
|
|
657
|
-
const position = positiveInteger(queue?.position);
|
|
658
|
-
return state === "queued" && position ? `queue-${position}` : undefined;
|
|
659
|
-
}
|
|
660
|
-
|
|
661
|
-
function workflowCode(result: Record<string, unknown>): string | undefined {
|
|
662
|
-
const pause = asRecord(result.pause);
|
|
663
|
-
const raw =
|
|
664
|
-
typeof result.code === "string"
|
|
665
|
-
? result.code
|
|
666
|
-
: typeof pause?.code === "string"
|
|
667
|
-
? pause.code
|
|
668
|
-
: undefined;
|
|
669
|
-
return raw === undefined ? undefined : sanitizeWorkflowCode(raw);
|
|
670
|
-
}
|
|
671
|
-
|
|
672
|
-
export function projectWorkflowActivity(
|
|
673
|
-
args: unknown,
|
|
674
|
-
result: unknown,
|
|
675
|
-
elapsedMs: number,
|
|
676
|
-
): ActivityDisplay {
|
|
677
|
-
const command = asRecord(args) ?? {};
|
|
678
|
-
const payload = asRecord(result) ?? {};
|
|
679
|
-
const state = workflowStateOf(payload);
|
|
680
|
-
const task = firstWorkflowTask(payload);
|
|
681
|
-
const stage =
|
|
682
|
-
payload.stage === "abel-design" || payload.stage === "abel-implement"
|
|
683
|
-
? payload.stage
|
|
684
|
-
: command.stage === "abel-design" || command.stage === "abel-implement"
|
|
685
|
-
? command.stage
|
|
686
|
-
: "abel-control";
|
|
687
|
-
const change =
|
|
688
|
-
typeof payload.change === "string"
|
|
689
|
-
? payload.change
|
|
690
|
-
: typeof command.change === "string"
|
|
691
|
-
? command.change
|
|
692
|
-
: undefined;
|
|
693
|
-
const runId = typeof payload.runId === "string" ? payload.runId : undefined;
|
|
694
|
-
const taskId =
|
|
695
|
-
typeof payload.taskId === "string" ? payload.taskId : task?.taskId;
|
|
696
|
-
const phase =
|
|
697
|
-
typeof payload.phase === "string"
|
|
698
|
-
? payload.phase
|
|
699
|
-
: (task?.phase ??
|
|
700
|
-
(typeof command.command === "string" ? command.command : "control"));
|
|
701
|
-
const objective =
|
|
702
|
-
typeof payload.objective === "string"
|
|
703
|
-
? payload.objective
|
|
704
|
-
: change
|
|
705
|
-
? `change ${change}`
|
|
706
|
-
: `${String(command.command ?? "control")} workflow run`;
|
|
707
|
-
const legalCommands = legalWorkflowCommands(payload);
|
|
708
|
-
const policy = asRecord(payload.policy);
|
|
709
|
-
const attempt = positiveInteger(payload.attempt ?? policy?.attempt);
|
|
710
|
-
const maxAttempts = positiveInteger(
|
|
711
|
-
payload.maxAttempts ?? policy?.maxAttempts,
|
|
712
|
-
);
|
|
713
|
-
const code =
|
|
714
|
-
state === "rejected" && payload.state === "completed"
|
|
715
|
-
? "completion-state-inconsistent"
|
|
716
|
-
: workflowCode(payload);
|
|
717
|
-
return {
|
|
718
|
-
kind: "activityDisplay",
|
|
719
|
-
requestId: sanitizeDisplayText(
|
|
720
|
-
runId ?? taskId ?? change ?? command.operationId ?? "pending",
|
|
721
|
-
128,
|
|
722
|
-
),
|
|
723
|
-
role: sanitizeDisplayText(stage, 80),
|
|
724
|
-
phase: sanitizeDisplayText(phase, 40),
|
|
725
|
-
objective: sanitizeDisplayText(objective, 240),
|
|
726
|
-
state,
|
|
727
|
-
tone: workflowTone(state),
|
|
728
|
-
elapsedMs: Math.max(0, elapsedMs),
|
|
729
|
-
...(runId ? { runId: sanitizeDisplayText(runId, 128) } : {}),
|
|
730
|
-
...(taskId ? { taskId: sanitizeDisplayText(taskId, 128) } : {}),
|
|
731
|
-
...(code ? { code } : {}),
|
|
732
|
-
...(attempt ? { attempt } : {}),
|
|
733
|
-
...(maxAttempts ? { maxAttempts } : {}),
|
|
734
|
-
...(workflowWait(state, payload)
|
|
735
|
-
? { wait: workflowWait(state, payload) }
|
|
736
|
-
: {}),
|
|
737
|
-
...(!hasAutomaticContinuation(payload) &&
|
|
738
|
-
nextWorkflowAction(state, legalCommands)
|
|
739
|
-
? { nextAction: nextWorkflowAction(state, legalCommands) }
|
|
740
|
-
: {}),
|
|
741
|
-
};
|
|
742
|
-
}
|
|
743
|
-
|
|
744
|
-
function activityResultFromUpdate(update: WorkflowActivityUpdate): unknown {
|
|
745
|
-
return {
|
|
746
|
-
...update,
|
|
747
|
-
...(update.state === "completed" ? { completed: true } : {}),
|
|
748
|
-
...(update.code ? { pause: { code: update.code } } : {}),
|
|
749
|
-
};
|
|
750
|
-
}
|
|
751
|
-
|
|
752
|
-
export function summarizeDispatchResult(
|
|
753
|
-
result: unknown,
|
|
754
|
-
): ActivitySummary | undefined {
|
|
755
|
-
const outer = asRecord(result);
|
|
756
|
-
if (!outer || outer.ok === false) return undefined;
|
|
757
|
-
const outcome = outer.ok === true ? asRecord(outer.result) : outer;
|
|
758
|
-
if (!outcome) return undefined;
|
|
759
|
-
const value =
|
|
760
|
-
outcome.kind === "candidate" ? asRecord(outcome.result) : outcome;
|
|
761
|
-
if (!value) return undefined;
|
|
762
|
-
if (
|
|
763
|
-
value.kind === "evidence" &&
|
|
764
|
-
Array.isArray(value.conclusions) &&
|
|
765
|
-
Array.isArray(value.citations) &&
|
|
766
|
-
Array.isArray(value.risks) &&
|
|
767
|
-
Array.isArray(value.blockingQuestions)
|
|
768
|
-
) {
|
|
769
|
-
return {
|
|
770
|
-
kind: "evidence",
|
|
771
|
-
conclusions: value.conclusions.length,
|
|
772
|
-
citations: value.citations.length,
|
|
773
|
-
risks: value.risks.length,
|
|
774
|
-
blockingQuestions: value.blockingQuestions.length,
|
|
775
|
-
};
|
|
776
|
-
}
|
|
777
|
-
if (
|
|
778
|
-
value.kind === "evidence" &&
|
|
779
|
-
Array.isArray(value.evidence) &&
|
|
780
|
-
Array.isArray(value.existing_structures) &&
|
|
781
|
-
Array.isArray(value.risks) &&
|
|
782
|
-
Array.isArray(value.open_questions)
|
|
783
|
-
) {
|
|
784
|
-
return {
|
|
785
|
-
kind: "evidence",
|
|
786
|
-
conclusions: value.existing_structures.length,
|
|
787
|
-
citations: value.evidence.length,
|
|
788
|
-
risks: value.risks.length,
|
|
789
|
-
blockingQuestions: value.open_questions.length,
|
|
790
|
-
};
|
|
791
|
-
}
|
|
792
|
-
if (
|
|
793
|
-
value.kind === "diff" &&
|
|
794
|
-
typeof value.summary === "string" &&
|
|
795
|
-
Array.isArray(value.risks)
|
|
796
|
-
) {
|
|
797
|
-
return {
|
|
798
|
-
kind: "diff",
|
|
799
|
-
summary: sanitizeDisplayText(value.summary),
|
|
800
|
-
riskCount: value.risks.length,
|
|
801
|
-
retained:
|
|
802
|
-
typeof outer.resultId === "string" ||
|
|
803
|
-
typeof outcome.resultId === "string",
|
|
804
|
-
};
|
|
805
|
-
}
|
|
806
|
-
return undefined;
|
|
807
|
-
}
|
|
808
|
-
|
|
809
|
-
function toneForResult(result: unknown): ActivityTone | undefined {
|
|
810
|
-
const outer = asRecord(result);
|
|
811
|
-
if (!outer || outer.ok === false) return undefined;
|
|
812
|
-
const value = outer.ok === true ? asRecord(outer.result) : outer;
|
|
813
|
-
switch (value?.kind) {
|
|
814
|
-
case "candidate":
|
|
815
|
-
case "applied":
|
|
816
|
-
case "completed":
|
|
817
|
-
return "success";
|
|
818
|
-
case "deferred":
|
|
819
|
-
case "retry":
|
|
820
|
-
case "checkpoint-required":
|
|
821
|
-
return "warning";
|
|
822
|
-
case "blocked":
|
|
823
|
-
case "cancelled":
|
|
824
|
-
return "muted";
|
|
825
|
-
default:
|
|
826
|
-
return undefined;
|
|
827
|
-
}
|
|
828
|
-
}
|
|
829
|
-
|
|
830
|
-
function terminalState(
|
|
831
|
-
event: PacketActivityEvent,
|
|
832
|
-
): event is PacketActivityEvent & {
|
|
833
|
-
state: TerminalActivityState;
|
|
834
|
-
} {
|
|
835
|
-
return ![
|
|
836
|
-
"queued",
|
|
837
|
-
"preparing",
|
|
838
|
-
"connecting",
|
|
839
|
-
"waiting-first-response",
|
|
840
|
-
"running",
|
|
841
|
-
"retrying",
|
|
842
|
-
].includes(event.state);
|
|
843
|
-
}
|
|
844
|
-
|
|
845
|
-
function displayFromEvent(
|
|
846
|
-
entry: ActivityEntry,
|
|
847
|
-
event: PacketActivityEvent,
|
|
848
|
-
elapsedMs: number,
|
|
849
|
-
): ActivityDisplay {
|
|
850
|
-
const reason = event.failureReason;
|
|
851
|
-
return {
|
|
852
|
-
kind: "activityDisplay",
|
|
853
|
-
requestId: entry.requestId,
|
|
854
|
-
role: entry.role,
|
|
855
|
-
phase: entry.phase,
|
|
856
|
-
objective: sanitizeDisplayText(entry.objective),
|
|
857
|
-
state: event.state as TerminalActivityState,
|
|
858
|
-
...(entry.attempt ? { attempt: entry.attempt } : {}),
|
|
859
|
-
...(entry.maxAttempts ? { maxAttempts: entry.maxAttempts } : {}),
|
|
860
|
-
...(entry.code ? { code: entry.code } : {}),
|
|
861
|
-
...(entry.wait ? { wait: entry.wait } : {}),
|
|
862
|
-
tone:
|
|
863
|
-
event.state === "failed" || event.state === "timed-out"
|
|
864
|
-
? "error"
|
|
865
|
-
: event.state === "cancelled"
|
|
866
|
-
? "muted"
|
|
867
|
-
: "success",
|
|
868
|
-
elapsedMs,
|
|
869
|
-
...(reason === undefined ? {} : { reason: sanitizeFailureReason(reason) }),
|
|
870
|
-
};
|
|
871
|
-
}
|
|
872
|
-
|
|
873
|
-
export function createActivityDisplay(
|
|
874
|
-
event: PacketActivityEvent,
|
|
875
|
-
elapsedMs: number,
|
|
876
|
-
result?: unknown,
|
|
877
|
-
): ActivityDisplay {
|
|
878
|
-
const entry: ActivityEntry = {
|
|
879
|
-
toolCallId: "",
|
|
880
|
-
requestId: event.requestId,
|
|
881
|
-
role: event.role,
|
|
882
|
-
phase: event.phase,
|
|
883
|
-
objective: event.objective,
|
|
884
|
-
state: event.state,
|
|
885
|
-
sequence: event.sequence,
|
|
886
|
-
startedAt: 0,
|
|
887
|
-
elapsedMs,
|
|
888
|
-
...(event.attempt ? { attempt: event.attempt } : {}),
|
|
889
|
-
...(event.maxAttempts ? { maxAttempts: event.maxAttempts } : {}),
|
|
890
|
-
...(event.code ? { code: event.code } : {}),
|
|
891
|
-
...(event.wait ? { wait: event.wait } : {}),
|
|
892
|
-
};
|
|
893
|
-
const display = displayFromEvent(entry, event, elapsedMs);
|
|
894
|
-
const summary = result ? summarizeDispatchResult(result) : undefined;
|
|
895
|
-
const tone = result ? toneForResult(result) : undefined;
|
|
896
|
-
return {
|
|
897
|
-
...display,
|
|
898
|
-
...(tone === undefined ? {} : { tone }),
|
|
899
|
-
...(summary === undefined ? {} : { summary }),
|
|
900
|
-
};
|
|
901
|
-
}
|
|
902
|
-
|
|
903
|
-
function activityPartial(display: ActivityDisplay | ActivitySnapshot): unknown {
|
|
904
|
-
const safeDisplay =
|
|
905
|
-
"onUpdate" in display
|
|
906
|
-
? (({ onUpdate: _onUpdate, ...snapshot }) => snapshot)(display)
|
|
907
|
-
: display;
|
|
908
|
-
return {
|
|
909
|
-
content: [
|
|
910
|
-
{
|
|
911
|
-
type: "text",
|
|
912
|
-
text: renderActivityLines(safeDisplay, 120).join("\n"),
|
|
913
|
-
},
|
|
914
|
-
],
|
|
915
|
-
details: { [ACTIVITY_DETAILS_KEY]: safeDisplay },
|
|
916
|
-
};
|
|
917
|
-
}
|
|
918
|
-
|
|
919
|
-
export class ActivityController {
|
|
920
|
-
private ui?: ActivityUi;
|
|
921
|
-
private accepting = false;
|
|
922
|
-
private readonly entries = new Map<string, ActivityEntry>();
|
|
923
|
-
private readonly terminals = new Map<string, ActivityDisplay>();
|
|
924
|
-
private spinnerFrame = 0;
|
|
925
|
-
private timer?: TimerHandle | unknown;
|
|
926
|
-
private requestRender?: () => void;
|
|
927
|
-
private widgetInstalled = false;
|
|
928
|
-
private controlSequence = 0;
|
|
929
|
-
private readonly now: () => number;
|
|
930
|
-
private readonly setIntervalFn: (
|
|
931
|
-
callback: () => void,
|
|
932
|
-
delay: number,
|
|
933
|
-
) => TimerHandle | unknown;
|
|
934
|
-
private readonly clearIntervalFn: (handle: TimerHandle | unknown) => void;
|
|
935
|
-
|
|
936
|
-
constructor(options: ActivityControllerOptions = {}) {
|
|
937
|
-
this.now = options.now ?? Date.now;
|
|
938
|
-
this.setIntervalFn =
|
|
939
|
-
options.setInterval ??
|
|
940
|
-
((callback, delay) => setInterval(callback, delay));
|
|
941
|
-
this.clearIntervalFn =
|
|
942
|
-
options.clearInterval ??
|
|
943
|
-
((handle) => clearInterval(handle as TimerHandle));
|
|
944
|
-
}
|
|
945
|
-
|
|
946
|
-
attach(ui: ActivityUi): void {
|
|
947
|
-
this.detachUi();
|
|
948
|
-
this.ui = ui;
|
|
949
|
-
this.accepting = true;
|
|
950
|
-
}
|
|
951
|
-
|
|
952
|
-
detach(): void {
|
|
953
|
-
this.accepting = false;
|
|
954
|
-
this.entries.clear();
|
|
955
|
-
this.terminals.clear();
|
|
956
|
-
this.stopTimer();
|
|
957
|
-
this.detachUi();
|
|
958
|
-
this.ui = undefined;
|
|
959
|
-
}
|
|
960
|
-
|
|
961
|
-
clear(): void {
|
|
962
|
-
this.entries.clear();
|
|
963
|
-
this.terminals.clear();
|
|
964
|
-
this.stopTimer();
|
|
965
|
-
this.clearUi();
|
|
966
|
-
}
|
|
967
|
-
|
|
968
|
-
beginWorkflow(
|
|
969
|
-
toolCallId: string,
|
|
970
|
-
args: unknown,
|
|
971
|
-
onUpdate?: (result: unknown) => void,
|
|
972
|
-
): void {
|
|
973
|
-
if (!this.accepting || this.entries.has(toolCallId)) return;
|
|
974
|
-
const display = projectWorkflowActivity(args, { state: "queued" }, 0);
|
|
975
|
-
const entry: ActivityEntry = {
|
|
976
|
-
toolCallId,
|
|
977
|
-
requestId: display.requestId,
|
|
978
|
-
role: display.role,
|
|
979
|
-
phase: display.phase,
|
|
980
|
-
objective: display.objective,
|
|
981
|
-
state: display.state,
|
|
982
|
-
sequence: ++this.controlSequence,
|
|
983
|
-
startedAt: this.now(),
|
|
984
|
-
elapsedMs: 0,
|
|
985
|
-
spinnerFrame: this.spinnerFrame,
|
|
986
|
-
tone: display.tone,
|
|
987
|
-
onUpdate,
|
|
988
|
-
};
|
|
989
|
-
this.entries.set(toolCallId, entry);
|
|
990
|
-
this.emit(entry, entry);
|
|
991
|
-
this.syncUi();
|
|
992
|
-
}
|
|
993
|
-
|
|
994
|
-
updateWorkflow(
|
|
995
|
-
toolCallId: string,
|
|
996
|
-
args: unknown,
|
|
997
|
-
update: WorkflowActivityUpdate,
|
|
998
|
-
): void {
|
|
999
|
-
const entry = this.entries.get(toolCallId);
|
|
1000
|
-
if (!this.accepting || !entry) return;
|
|
1001
|
-
const display = projectWorkflowActivity(
|
|
1002
|
-
args,
|
|
1003
|
-
activityResultFromUpdate(update),
|
|
1004
|
-
Math.max(0, this.now() - entry.startedAt),
|
|
1005
|
-
);
|
|
1006
|
-
Object.assign(entry, {
|
|
1007
|
-
requestId: display.requestId,
|
|
1008
|
-
role: display.role,
|
|
1009
|
-
phase: display.phase,
|
|
1010
|
-
objective: display.objective,
|
|
1011
|
-
state: display.state,
|
|
1012
|
-
elapsedMs: display.elapsedMs,
|
|
1013
|
-
tone: display.tone,
|
|
1014
|
-
runId: display.runId,
|
|
1015
|
-
taskId: display.taskId,
|
|
1016
|
-
code: display.code,
|
|
1017
|
-
nextAction: display.nextAction,
|
|
1018
|
-
attempt: display.attempt,
|
|
1019
|
-
maxAttempts: display.maxAttempts,
|
|
1020
|
-
wait: display.wait,
|
|
1021
|
-
});
|
|
1022
|
-
this.emit(entry, entry);
|
|
1023
|
-
this.syncUi();
|
|
1024
|
-
}
|
|
1025
|
-
|
|
1026
|
-
finalizeWorkflow(
|
|
1027
|
-
toolCallId: string,
|
|
1028
|
-
args: unknown,
|
|
1029
|
-
result: unknown,
|
|
1030
|
-
): ActivityDisplay | undefined {
|
|
1031
|
-
const entry = this.entries.get(toolCallId);
|
|
1032
|
-
if (!entry) return undefined;
|
|
1033
|
-
const display = projectWorkflowActivity(
|
|
1034
|
-
args,
|
|
1035
|
-
result,
|
|
1036
|
-
Math.max(0, this.now() - entry.startedAt),
|
|
1037
|
-
);
|
|
1038
|
-
this.emit(entry, display);
|
|
1039
|
-
this.entries.delete(toolCallId);
|
|
1040
|
-
this.syncUi();
|
|
1041
|
-
return display;
|
|
1042
|
-
}
|
|
1043
|
-
|
|
1044
|
-
failWorkflow(toolCallId: string): void {
|
|
1045
|
-
const entry = this.entries.get(toolCallId);
|
|
1046
|
-
if (!entry) return;
|
|
1047
|
-
const display: ActivityDisplay = {
|
|
1048
|
-
kind: "activityDisplay",
|
|
1049
|
-
requestId: entry.requestId,
|
|
1050
|
-
role: entry.role,
|
|
1051
|
-
phase: entry.phase,
|
|
1052
|
-
objective: entry.objective,
|
|
1053
|
-
state: "failed",
|
|
1054
|
-
tone: "error",
|
|
1055
|
-
elapsedMs: Math.max(0, this.now() - entry.startedAt),
|
|
1056
|
-
reason: "subagent failed",
|
|
1057
|
-
};
|
|
1058
|
-
this.emit(entry, display);
|
|
1059
|
-
this.entries.delete(toolCallId);
|
|
1060
|
-
this.syncUi();
|
|
1061
|
-
}
|
|
1062
|
-
|
|
1063
|
-
observe(
|
|
1064
|
-
toolCallId: string,
|
|
1065
|
-
onUpdate?: (result: unknown) => void,
|
|
1066
|
-
): PacketActivityObserver {
|
|
1067
|
-
return (event) => this.accept(toolCallId, onUpdate, event);
|
|
1068
|
-
}
|
|
1069
|
-
|
|
1070
|
-
accept(
|
|
1071
|
-
toolCallId: string,
|
|
1072
|
-
onUpdate: ((result: unknown) => void) | undefined,
|
|
1073
|
-
event: PacketActivityEvent,
|
|
1074
|
-
): void {
|
|
1075
|
-
if (!this.accepting || this.terminals.has(toolCallId)) return;
|
|
1076
|
-
let entry = this.entries.get(toolCallId);
|
|
1077
|
-
if (!entry) {
|
|
1078
|
-
entry = {
|
|
1079
|
-
toolCallId,
|
|
1080
|
-
requestId: event.requestId,
|
|
1081
|
-
role: event.role,
|
|
1082
|
-
phase: event.phase,
|
|
1083
|
-
objective: sanitizeDisplayText(event.objective),
|
|
1084
|
-
state: event.state,
|
|
1085
|
-
sequence: event.sequence,
|
|
1086
|
-
startedAt: this.now(),
|
|
1087
|
-
elapsedMs: 0,
|
|
1088
|
-
spinnerFrame: this.spinnerFrame,
|
|
1089
|
-
...(event.attempt ? { attempt: event.attempt } : {}),
|
|
1090
|
-
...(event.maxAttempts ? { maxAttempts: event.maxAttempts } : {}),
|
|
1091
|
-
...(event.code ? { code: event.code } : {}),
|
|
1092
|
-
...(event.wait ? { wait: event.wait } : {}),
|
|
1093
|
-
onUpdate,
|
|
1094
|
-
};
|
|
1095
|
-
this.entries.set(toolCallId, entry);
|
|
1096
|
-
} else {
|
|
1097
|
-
entry.onUpdate = onUpdate ?? entry.onUpdate;
|
|
1098
|
-
entry.state = event.state;
|
|
1099
|
-
entry.attempt = event.attempt;
|
|
1100
|
-
entry.maxAttempts = event.maxAttempts;
|
|
1101
|
-
entry.code = event.code;
|
|
1102
|
-
entry.wait = event.wait;
|
|
1103
|
-
}
|
|
1104
|
-
if (terminalState(event)) {
|
|
1105
|
-
const display = displayFromEvent(
|
|
1106
|
-
entry,
|
|
1107
|
-
event,
|
|
1108
|
-
Math.max(0, this.now() - entry.startedAt),
|
|
1109
|
-
);
|
|
1110
|
-
this.terminals.set(toolCallId, display);
|
|
1111
|
-
this.emit(entry, display);
|
|
1112
|
-
this.entries.delete(toolCallId);
|
|
1113
|
-
this.syncUi();
|
|
1114
|
-
return;
|
|
1115
|
-
}
|
|
1116
|
-
this.emit(entry, {
|
|
1117
|
-
...entry,
|
|
1118
|
-
elapsedMs: Math.max(0, this.now() - entry.startedAt),
|
|
1119
|
-
});
|
|
1120
|
-
this.syncUi();
|
|
1121
|
-
}
|
|
1122
|
-
|
|
1123
|
-
finalize(toolCallId: string, result: unknown): ActivityDisplay | undefined {
|
|
1124
|
-
const display = this.terminals.get(toolCallId);
|
|
1125
|
-
if (!display) return undefined;
|
|
1126
|
-
this.terminals.delete(toolCallId);
|
|
1127
|
-
const summary = summarizeDispatchResult(result);
|
|
1128
|
-
const tone = toneForResult(result);
|
|
1129
|
-
return {
|
|
1130
|
-
...display,
|
|
1131
|
-
...(tone === undefined ? {} : { tone }),
|
|
1132
|
-
...(summary === undefined ? {} : { summary }),
|
|
1133
|
-
};
|
|
1134
|
-
}
|
|
1135
|
-
|
|
1136
|
-
getActiveEntries(): readonly ActivitySnapshot[] {
|
|
1137
|
-
return [...this.entries.values()].map(
|
|
1138
|
-
({ onUpdate: _onUpdate, ...entry }) => ({
|
|
1139
|
-
...entry,
|
|
1140
|
-
elapsedMs: Math.max(0, this.now() - entry.startedAt),
|
|
1141
|
-
spinnerFrame: this.spinnerFrame,
|
|
1142
|
-
}),
|
|
1143
|
-
);
|
|
1144
|
-
}
|
|
1145
|
-
|
|
1146
|
-
isTimerActive(): boolean {
|
|
1147
|
-
return this.timer !== undefined;
|
|
1148
|
-
}
|
|
1149
|
-
|
|
1150
|
-
private emit(
|
|
1151
|
-
entry: ActivityEntry,
|
|
1152
|
-
display: ActivityDisplay | ActivitySnapshot,
|
|
1153
|
-
): void {
|
|
1154
|
-
try {
|
|
1155
|
-
entry.onUpdate?.(activityPartial(display));
|
|
1156
|
-
} catch {
|
|
1157
|
-
// A renderer callback is not part of orchestration.
|
|
1158
|
-
}
|
|
1159
|
-
}
|
|
1160
|
-
|
|
1161
|
-
private ensureWidget(): void {
|
|
1162
|
-
if (!this.ui || this.widgetInstalled) return;
|
|
1163
|
-
try {
|
|
1164
|
-
this.ui.setWidget(
|
|
1165
|
-
ACTIVITY_WIDGET_KEY,
|
|
1166
|
-
(tui, theme) => {
|
|
1167
|
-
this.requestRender = () => tui.requestRender();
|
|
1168
|
-
return new ActivityWidget(
|
|
1169
|
-
() => this.getActiveEntries(),
|
|
1170
|
-
this.now,
|
|
1171
|
-
theme,
|
|
1172
|
-
);
|
|
1173
|
-
},
|
|
1174
|
-
{ placement: "aboveEditor" },
|
|
1175
|
-
);
|
|
1176
|
-
this.widgetInstalled = true;
|
|
1177
|
-
} catch {
|
|
1178
|
-
// TUI failures are deliberately isolated from the run.
|
|
1179
|
-
}
|
|
1180
|
-
}
|
|
1181
|
-
|
|
1182
|
-
private startTimer(): void {
|
|
1183
|
-
if (this.timer !== undefined) return;
|
|
1184
|
-
try {
|
|
1185
|
-
this.timer = this.setIntervalFn(
|
|
1186
|
-
() => this.tick(),
|
|
1187
|
-
ACTIVITY_REFRESH_MS,
|
|
1188
|
-
) as TimerHandle;
|
|
1189
|
-
} catch {
|
|
1190
|
-
this.timer = undefined;
|
|
1191
|
-
}
|
|
1192
|
-
}
|
|
1193
|
-
|
|
1194
|
-
private stopTimer(): void {
|
|
1195
|
-
if (this.timer === undefined) return;
|
|
1196
|
-
try {
|
|
1197
|
-
this.clearIntervalFn(this.timer);
|
|
1198
|
-
} catch {
|
|
1199
|
-
// The timer is best-effort presentation state.
|
|
1200
|
-
}
|
|
1201
|
-
this.timer = undefined;
|
|
1202
|
-
}
|
|
1203
|
-
|
|
1204
|
-
private tick(): void {
|
|
1205
|
-
if (this.entries.size === 0) {
|
|
1206
|
-
this.stopTimer();
|
|
1207
|
-
this.clearUi();
|
|
1208
|
-
return;
|
|
1209
|
-
}
|
|
1210
|
-
this.spinnerFrame++;
|
|
1211
|
-
for (const entry of this.entries.values()) {
|
|
1212
|
-
this.emit(entry, {
|
|
1213
|
-
...entry,
|
|
1214
|
-
elapsedMs: Math.max(0, this.now() - entry.startedAt),
|
|
1215
|
-
spinnerFrame: this.spinnerFrame,
|
|
1216
|
-
});
|
|
1217
|
-
}
|
|
1218
|
-
try {
|
|
1219
|
-
this.requestRender?.();
|
|
1220
|
-
} catch {
|
|
1221
|
-
// Widget refresh is best-effort.
|
|
1222
|
-
}
|
|
1223
|
-
this.syncUi();
|
|
1224
|
-
}
|
|
1225
|
-
|
|
1226
|
-
private syncUi(): void {
|
|
1227
|
-
if (!this.ui) return;
|
|
1228
|
-
if (this.entries.size === 0) {
|
|
1229
|
-
this.stopTimer();
|
|
1230
|
-
this.clearUi();
|
|
1231
|
-
return;
|
|
1232
|
-
}
|
|
1233
|
-
this.ensureWidget();
|
|
1234
|
-
this.startTimer();
|
|
1235
|
-
try {
|
|
1236
|
-
const running = [...this.entries.values()].filter(
|
|
1237
|
-
(entry) => entry.state !== "queued",
|
|
1238
|
-
).length;
|
|
1239
|
-
const queued = this.entries.size - running;
|
|
1240
|
-
this.ui.setStatus(
|
|
1241
|
-
ACTIVITY_STATUS_KEY,
|
|
1242
|
-
`Agents: ${running} running, ${queued} queued`,
|
|
1243
|
-
);
|
|
1244
|
-
} catch {
|
|
1245
|
-
// Status rendering is optional.
|
|
1246
|
-
}
|
|
1247
|
-
}
|
|
1248
|
-
|
|
1249
|
-
private clearUi(): void {
|
|
1250
|
-
if (!this.ui) return;
|
|
1251
|
-
try {
|
|
1252
|
-
if (this.widgetInstalled)
|
|
1253
|
-
this.ui.setWidget(ACTIVITY_WIDGET_KEY, undefined);
|
|
1254
|
-
} catch {
|
|
1255
|
-
// Best-effort cleanup.
|
|
1256
|
-
}
|
|
1257
|
-
try {
|
|
1258
|
-
this.ui.setStatus(ACTIVITY_STATUS_KEY, undefined);
|
|
1259
|
-
} catch {
|
|
1260
|
-
// Best-effort cleanup.
|
|
1261
|
-
}
|
|
1262
|
-
this.widgetInstalled = false;
|
|
1263
|
-
this.requestRender = undefined;
|
|
1264
|
-
}
|
|
1265
|
-
|
|
1266
|
-
private detachUi(): void {
|
|
1267
|
-
this.clearUi();
|
|
1268
|
-
}
|
|
1269
|
-
}
|
|
1270
|
-
|
|
1271
|
-
export function renderActivityResult(
|
|
1272
|
-
result: unknown,
|
|
1273
|
-
options: ToolRenderResultOptions,
|
|
1274
|
-
theme?: ActivityTheme,
|
|
1275
|
-
context?: ActivityRenderContext,
|
|
1276
|
-
): Component {
|
|
1277
|
-
const details = asRecord(asRecord(result)?.details);
|
|
1278
|
-
const activity = details?.[ACTIVITY_DETAILS_KEY];
|
|
1279
|
-
const failed = context?.isError === true;
|
|
1280
|
-
const args = asRecord(context?.args);
|
|
1281
|
-
const failedRun = failed && args?.action === "run";
|
|
1282
|
-
if (isActivityDisplay(activity) && !failed) {
|
|
1283
|
-
return new ActivityInlineComponent(activity, theme, options.expanded);
|
|
1284
|
-
}
|
|
1285
|
-
if (failedRun) {
|
|
1286
|
-
const snapshot = identityFromDispatchArgs(asRecord(context?.args)?.request);
|
|
1287
|
-
const display: ActivityDisplay = {
|
|
1288
|
-
kind: "activityDisplay",
|
|
1289
|
-
requestId: isActivityDisplay(activity)
|
|
1290
|
-
? activity.requestId
|
|
1291
|
-
: snapshot.requestId,
|
|
1292
|
-
role: isActivityDisplay(activity) ? activity.role : snapshot.role,
|
|
1293
|
-
phase: isActivityDisplay(activity) ? activity.phase : snapshot.phase,
|
|
1294
|
-
objective: isActivityDisplay(activity)
|
|
1295
|
-
? activity.objective
|
|
1296
|
-
: snapshot.objective,
|
|
1297
|
-
state: "failed",
|
|
1298
|
-
tone: "error",
|
|
1299
|
-
elapsedMs: isActivityDisplay(activity) ? activity.elapsedMs : 0,
|
|
1300
|
-
reason: sanitizeFailureReason(
|
|
1301
|
-
isActivityDisplay(activity) ? activity.reason : undefined,
|
|
1302
|
-
),
|
|
1303
|
-
};
|
|
1304
|
-
return new ActivityInlineComponent(display, theme);
|
|
1305
|
-
}
|
|
1306
|
-
if (failed) {
|
|
1307
|
-
const designFailure = safeDesignFailure(details?.designFailure);
|
|
1308
|
-
if (args?.action === "design" && designFailure) {
|
|
1309
|
-
return renderDesignFailure(designFailure, options.expanded, theme);
|
|
1310
|
-
}
|
|
1311
|
-
return new Text(
|
|
1312
|
-
`${dispatchLabel(args?.action ?? args?.command)} failed`,
|
|
1313
|
-
0,
|
|
1314
|
-
0,
|
|
1315
|
-
);
|
|
1316
|
-
}
|
|
1317
|
-
const content = asRecord(result)?.content;
|
|
1318
|
-
const text = Array.isArray(content)
|
|
1319
|
-
? content
|
|
1320
|
-
.map((item) => asRecord(item)?.text)
|
|
1321
|
-
.filter((item): item is string => typeof item === "string")
|
|
1322
|
-
.join("\n")
|
|
1323
|
-
: "";
|
|
1324
|
-
return new Text(text, 0, 0);
|
|
1325
|
-
}
|
|
1326
|
-
|
|
1327
|
-
function dispatchLabel(action: unknown): string {
|
|
1328
|
-
const value = sanitizeDisplayText(action ?? "");
|
|
1329
|
-
return value.length > 0 ? `Abel Dispatch ${value}` : "Abel Dispatch";
|
|
1330
|
-
}
|
|
1331
|
-
|
|
1332
|
-
function isActivityDisplay(value: unknown): value is ActivityDisplay {
|
|
1333
|
-
const record = asRecord(value);
|
|
1334
|
-
return (
|
|
1335
|
-
record?.kind === "activityDisplay" &&
|
|
1336
|
-
typeof record.requestId === "string" &&
|
|
1337
|
-
typeof record.role === "string" &&
|
|
1338
|
-
typeof record.phase === "string" &&
|
|
1339
|
-
typeof record.objective === "string" &&
|
|
1340
|
-
typeof record.state === "string" &&
|
|
1341
|
-
typeof record.elapsedMs === "number"
|
|
1342
|
-
);
|
|
1343
|
-
}
|
|
1344
|
-
|
|
1345
|
-
function identityFromDispatchArgs(request: unknown): {
|
|
1346
|
-
requestId: string;
|
|
1347
|
-
role: string;
|
|
1348
|
-
phase: string;
|
|
1349
|
-
objective: string;
|
|
1350
|
-
} {
|
|
1351
|
-
const value = asRecord(request);
|
|
1352
|
-
const attempt = asRecord(value?.attempt);
|
|
1353
|
-
const boundary = asRecord(value?.boundary);
|
|
1354
|
-
return {
|
|
1355
|
-
requestId: sanitizeDisplayText(
|
|
1356
|
-
attempt?.requestId ?? value?.id ?? "unknown",
|
|
1357
|
-
128,
|
|
1358
|
-
),
|
|
1359
|
-
role: sanitizeDisplayText(
|
|
1360
|
-
value?.role ??
|
|
1361
|
-
(value?.stage === "abel-implement"
|
|
1362
|
-
? "implementation-worker"
|
|
1363
|
-
: "unknown"),
|
|
1364
|
-
80,
|
|
1365
|
-
),
|
|
1366
|
-
phase: sanitizeDisplayText(attempt?.phase ?? value?.phase ?? "unknown", 40),
|
|
1367
|
-
objective: sanitizeDisplayText(
|
|
1368
|
-
value?.objective ?? boundary?.objective ?? "",
|
|
1369
|
-
240,
|
|
1370
|
-
),
|
|
1371
|
-
};
|
|
1372
|
-
}
|
|
1373
|
-
|
|
1374
|
-
export function renderActivityCall(
|
|
1375
|
-
args: unknown,
|
|
1376
|
-
theme?: ActivityTheme,
|
|
1377
|
-
context?: ActivityRenderContext,
|
|
1378
|
-
): Component {
|
|
1379
|
-
const value = asRecord(args);
|
|
1380
|
-
if (typeof value?.command === "string") {
|
|
1381
|
-
if (context?.executionStarted || context?.isError) return new Text("");
|
|
1382
|
-
const projected = projectWorkflowActivity(value, { state: "queued" }, 0);
|
|
1383
|
-
const display: ActivitySnapshot = {
|
|
1384
|
-
toolCallId: "call",
|
|
1385
|
-
requestId: projected.requestId,
|
|
1386
|
-
role: projected.role,
|
|
1387
|
-
phase: projected.phase,
|
|
1388
|
-
objective: projected.objective,
|
|
1389
|
-
state: projected.state,
|
|
1390
|
-
sequence: 0,
|
|
1391
|
-
startedAt: Date.now(),
|
|
1392
|
-
elapsedMs: 0,
|
|
1393
|
-
tone: projected.tone,
|
|
1394
|
-
};
|
|
1395
|
-
return new ActivityInlineComponent(display, theme);
|
|
1396
|
-
}
|
|
1397
|
-
if (value?.action !== "run") {
|
|
1398
|
-
return new Text(context?.isError ? "" : dispatchLabel(value?.action), 0, 0);
|
|
1399
|
-
}
|
|
1400
|
-
if (context?.executionStarted || context?.isError) {
|
|
1401
|
-
return new Text("");
|
|
1402
|
-
}
|
|
1403
|
-
if (!asRecord(value.request)) return new Text("Subagent", 0, 0);
|
|
1404
|
-
const identity = identityFromDispatchArgs(value.request);
|
|
1405
|
-
const display: ActivitySnapshot = {
|
|
1406
|
-
toolCallId: "call",
|
|
1407
|
-
requestId: identity.requestId,
|
|
1408
|
-
role: identity.role,
|
|
1409
|
-
phase: identity.phase,
|
|
1410
|
-
objective: identity.objective,
|
|
1411
|
-
state: "queued",
|
|
1412
|
-
sequence: 0,
|
|
1413
|
-
startedAt: Date.now(),
|
|
1414
|
-
elapsedMs: 0,
|
|
1415
|
-
};
|
|
1416
|
-
return new ActivityInlineComponent(display, theme);
|
|
1417
|
-
}
|