@kata-sh/pi-symphony-extension 0.1.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 +117 -0
- package/package.json +48 -0
- package/scripts/wave3-mock-server.mjs +249 -0
- package/src/attach-url-policy.test.ts +20 -0
- package/src/attach-url-policy.ts +24 -0
- package/src/binary-resolver.test.ts +114 -0
- package/src/binary-resolver.ts +115 -0
- package/src/command-args.test.ts +33 -0
- package/src/command-args.ts +58 -0
- package/src/commands.test.ts +679 -0
- package/src/commands.ts +218 -0
- package/src/console-model.test.ts +252 -0
- package/src/console-model.ts +234 -0
- package/src/console.test.ts +873 -0
- package/src/console.ts +552 -0
- package/src/errors.ts +33 -0
- package/src/event-stream.test.ts +145 -0
- package/src/event-stream.ts +107 -0
- package/src/http-client.test.ts +580 -0
- package/src/http-client.ts +856 -0
- package/src/index.test.ts +80 -0
- package/src/index.ts +27 -0
- package/src/package-smoke.test.ts +24 -0
- package/src/process-manager.test.ts +232 -0
- package/src/process-manager.ts +290 -0
- package/src/progress.test.ts +175 -0
- package/src/progress.ts +75 -0
- package/src/runtime-helpers.ts +11 -0
- package/src/runtime.test.ts +237 -0
- package/src/runtime.ts +119 -0
- package/src/state.test.ts +173 -0
- package/src/state.ts +146 -0
- package/src/tools.test.ts +359 -0
- package/src/tools.ts +201 -0
- package/src/wave3-mock-server.test.ts +83 -0
- package/src/workflow-resolver.ts +40 -0
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
BlockedIssueResponse,
|
|
3
|
+
CompletedIssueResponse,
|
|
4
|
+
PendingEscalationResponse,
|
|
5
|
+
RetryQueueEntryResponse,
|
|
6
|
+
RunAttemptResponse,
|
|
7
|
+
SymphonyEventEnvelope,
|
|
8
|
+
SymphonyStateResponse,
|
|
9
|
+
} from "./http-client.ts";
|
|
10
|
+
|
|
11
|
+
export interface WorkerRow {
|
|
12
|
+
issueId: string;
|
|
13
|
+
issueIdentifier: string;
|
|
14
|
+
title: string;
|
|
15
|
+
trackerState: string;
|
|
16
|
+
attempt: string;
|
|
17
|
+
turnCount: string;
|
|
18
|
+
maxTurns: string;
|
|
19
|
+
lastActivity: string;
|
|
20
|
+
workerHost: string;
|
|
21
|
+
workspacePath: string;
|
|
22
|
+
status: string;
|
|
23
|
+
errorPreview: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export type IssueRowKind = "running" | "retry" | "blocked" | "completed";
|
|
27
|
+
|
|
28
|
+
export interface IssueRow {
|
|
29
|
+
kind: IssueRowKind;
|
|
30
|
+
issueId: string;
|
|
31
|
+
issueIdentifier: string;
|
|
32
|
+
title: string;
|
|
33
|
+
status: string;
|
|
34
|
+
trackerState: string;
|
|
35
|
+
attempt: string;
|
|
36
|
+
turnCount: string;
|
|
37
|
+
maxTurns: string;
|
|
38
|
+
lastActivity: string;
|
|
39
|
+
workerHost: string;
|
|
40
|
+
workspacePath: string;
|
|
41
|
+
errorPreview: string;
|
|
42
|
+
blockers: string;
|
|
43
|
+
completedAt: string;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface EscalationRow {
|
|
47
|
+
requestId: string;
|
|
48
|
+
issueId: string;
|
|
49
|
+
issueIdentifier: string;
|
|
50
|
+
method: string;
|
|
51
|
+
preview: string;
|
|
52
|
+
createdAt: string;
|
|
53
|
+
timeout: string;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function buildWorkerRows(state: SymphonyStateResponse | undefined): WorkerRow[] {
|
|
57
|
+
return Object.entries(state?.running ?? {})
|
|
58
|
+
.map(([issueId, attempt]) => buildWorkerRow(issueId, attempt, state))
|
|
59
|
+
.sort((left, right) => left.issueIdentifier.localeCompare(right.issueIdentifier));
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function buildIssueRows(state: SymphonyStateResponse | undefined): IssueRow[] {
|
|
63
|
+
if (!state) return [];
|
|
64
|
+
|
|
65
|
+
return [
|
|
66
|
+
...Object.entries(state.running ?? {})
|
|
67
|
+
.map(([issueId, attempt]) => workerToIssueRow(issueId, attempt, state))
|
|
68
|
+
.sort(compareIssueRows),
|
|
69
|
+
...(state.retry_queue ?? []).map(retryToIssueRow).sort(compareIssueRows),
|
|
70
|
+
...(state.blocked ?? []).map(blockedToIssueRow).sort(compareIssueRows),
|
|
71
|
+
...(state.completed ?? []).map(completedToIssueRow).sort(compareIssueRows),
|
|
72
|
+
];
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function buildEscalationRows(state: SymphonyStateResponse | undefined): EscalationRow[] {
|
|
76
|
+
return (state?.pending_escalations ?? [])
|
|
77
|
+
.slice()
|
|
78
|
+
.sort(compareEscalations)
|
|
79
|
+
.map((entry) => ({
|
|
80
|
+
requestId: entry.request_id,
|
|
81
|
+
issueId: entry.issue_id,
|
|
82
|
+
issueIdentifier: entry.issue_identifier,
|
|
83
|
+
method: entry.method,
|
|
84
|
+
preview: truncateText(entry.preview, 120),
|
|
85
|
+
createdAt: entry.created_at,
|
|
86
|
+
timeout: formatDuration(entry.timeout_ms),
|
|
87
|
+
}));
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function formatEventRows(events: SymphonyEventEnvelope[], limit = 8): string[] {
|
|
91
|
+
return events
|
|
92
|
+
.filter((event) => event.kind === "worker" || event.kind === "runtime" || event.kind.startsWith("escalation_"))
|
|
93
|
+
.slice(-limit)
|
|
94
|
+
.reverse()
|
|
95
|
+
.map((event) => [event.timestamp, event.severity, event.kind, event.issue ?? "-", event.event, eventSummary(event)].filter(Boolean).join(" "));
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function workerToIssueRow(issueId: string, attempt: RunAttemptResponse, state: SymphonyStateResponse | undefined): IssueRow {
|
|
99
|
+
return {
|
|
100
|
+
kind: "running",
|
|
101
|
+
...buildWorkerRow(issueId, attempt, state),
|
|
102
|
+
blockers: "-",
|
|
103
|
+
completedAt: "-",
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function retryToIssueRow(entry: RetryQueueEntryResponse): IssueRow {
|
|
108
|
+
const errorPreview = entry.error ? truncateText(entry.error, 120) : "-";
|
|
109
|
+
return {
|
|
110
|
+
kind: "retry",
|
|
111
|
+
issueId: entry.issue_id,
|
|
112
|
+
issueIdentifier: entry.identifier,
|
|
113
|
+
title: entry.error ?? "pending retry",
|
|
114
|
+
status: `retry in ${formatDuration(entry.due_in_ms)}`,
|
|
115
|
+
trackerState: "retry",
|
|
116
|
+
attempt: String(entry.attempt),
|
|
117
|
+
turnCount: "-",
|
|
118
|
+
maxTurns: "-",
|
|
119
|
+
lastActivity: "-",
|
|
120
|
+
workerHost: entry.worker_host ?? "local",
|
|
121
|
+
workspacePath: entry.workspace_path ?? "-",
|
|
122
|
+
errorPreview,
|
|
123
|
+
blockers: "-",
|
|
124
|
+
completedAt: "-",
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function blockedToIssueRow(entry: BlockedIssueResponse): IssueRow {
|
|
129
|
+
return {
|
|
130
|
+
kind: "blocked",
|
|
131
|
+
issueId: entry.issue_id,
|
|
132
|
+
issueIdentifier: entry.identifier,
|
|
133
|
+
title: entry.title,
|
|
134
|
+
status: entry.state,
|
|
135
|
+
trackerState: entry.state,
|
|
136
|
+
attempt: "-",
|
|
137
|
+
turnCount: "-",
|
|
138
|
+
maxTurns: "-",
|
|
139
|
+
lastActivity: "-",
|
|
140
|
+
workerHost: "-",
|
|
141
|
+
workspacePath: "-",
|
|
142
|
+
errorPreview: "-",
|
|
143
|
+
blockers: entry.blocker_identifiers.length > 0 ? entry.blocker_identifiers.join(", ") : "-",
|
|
144
|
+
completedAt: "-",
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function completedToIssueRow(entry: CompletedIssueResponse): IssueRow {
|
|
149
|
+
return {
|
|
150
|
+
kind: "completed",
|
|
151
|
+
issueId: entry.issue_id,
|
|
152
|
+
issueIdentifier: entry.identifier,
|
|
153
|
+
title: entry.title,
|
|
154
|
+
status: "completed",
|
|
155
|
+
trackerState: "completed",
|
|
156
|
+
attempt: "-",
|
|
157
|
+
turnCount: "-",
|
|
158
|
+
maxTurns: "-",
|
|
159
|
+
lastActivity: "-",
|
|
160
|
+
workerHost: "-",
|
|
161
|
+
workspacePath: "-",
|
|
162
|
+
errorPreview: "-",
|
|
163
|
+
blockers: "-",
|
|
164
|
+
completedAt: entry.completed_at ?? "-",
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function buildWorkerRow(issueId: string, attempt: RunAttemptResponse, state: SymphonyStateResponse | undefined): WorkerRow {
|
|
169
|
+
const session = state?.running_sessions?.[issueId];
|
|
170
|
+
const info = state?.running_session_info?.[issueId];
|
|
171
|
+
const turnCount = info?.turn_count ?? session?.turn_count;
|
|
172
|
+
const lastError = attempt.error ?? info?.last_error ?? session?.last_error;
|
|
173
|
+
|
|
174
|
+
return {
|
|
175
|
+
issueId,
|
|
176
|
+
issueIdentifier: attempt.issue_identifier,
|
|
177
|
+
title: attempt.issue_title ?? "-",
|
|
178
|
+
trackerState: attempt.tracker_state ?? "-",
|
|
179
|
+
attempt: String(attempt.attempt ?? 1),
|
|
180
|
+
turnCount: turnCount === undefined ? "-" : String(turnCount),
|
|
181
|
+
maxTurns: info?.max_turns === undefined ? "-" : String(info.max_turns),
|
|
182
|
+
lastActivity: formatLastActivity(info?.last_activity_ms, session?.last_activity_at),
|
|
183
|
+
workerHost: attempt.worker_host ?? "local",
|
|
184
|
+
workspacePath: attempt.workspace_path,
|
|
185
|
+
status: attempt.status,
|
|
186
|
+
errorPreview: lastError ? truncateText(lastError, 120) : "-",
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function formatLastActivity(lastActivityMs: number | null | undefined, lastActivityAt: string | null | undefined): string {
|
|
191
|
+
if (typeof lastActivityMs === "number" && Number.isFinite(lastActivityMs)) {
|
|
192
|
+
return new Date(lastActivityMs).toISOString();
|
|
193
|
+
}
|
|
194
|
+
return lastActivityAt ?? "-";
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function formatDuration(durationMs: number): string {
|
|
198
|
+
if (!Number.isFinite(durationMs) || durationMs <= 0) return "0s";
|
|
199
|
+
const totalSeconds = Math.ceil(durationMs / 1000);
|
|
200
|
+
if (totalSeconds < 60) return `${totalSeconds}s`;
|
|
201
|
+
const minutes = Math.floor(totalSeconds / 60);
|
|
202
|
+
const seconds = totalSeconds % 60;
|
|
203
|
+
return `${minutes}m ${seconds}s`;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function compareIssueRows(left: IssueRow, right: IssueRow): number {
|
|
207
|
+
return left.issueIdentifier.localeCompare(right.issueIdentifier);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function compareEscalations(left: PendingEscalationResponse, right: PendingEscalationResponse): number {
|
|
211
|
+
return timestampMs(left.created_at) - timestampMs(right.created_at) || left.request_id.localeCompare(right.request_id);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function timestampMs(value: string): number {
|
|
215
|
+
const parsed = Date.parse(value);
|
|
216
|
+
return Number.isFinite(parsed) ? parsed : 0;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function eventSummary(event: SymphonyEventEnvelope): string {
|
|
220
|
+
if (!isRecord(event.payload)) return "";
|
|
221
|
+
const preferred = event.payload.error_preview ?? event.payload.summary ?? event.payload.message ?? event.payload.reason ?? event.payload.error ?? event.payload.instruction_preview;
|
|
222
|
+
if (typeof preferred !== "string") return "";
|
|
223
|
+
const singleLine = preferred.replace(/\s+/g, " ").trim();
|
|
224
|
+
return singleLine ? truncateText(singleLine, 120) : "";
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function truncateText(value: string, maxLength: number): string {
|
|
228
|
+
if (value.length <= maxLength) return value;
|
|
229
|
+
return `${value.slice(0, maxLength - 1)}…`;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
233
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
234
|
+
}
|