@kata-sh/pi-symphony-extension 0.1.2 → 2.3.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 +46 -1
- package/package.json +15 -4
- package/scripts/wave3-mock-server.mjs +10 -0
- package/src/console-model.test.ts +45 -1
- package/src/console-model.ts +70 -2
- package/src/console.ts +42 -5
- package/src/http-client.ts +45 -0
package/README.md
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
# @kata-sh/pi-symphony-extension
|
|
2
2
|
|
|
3
|
-
Pi extension for initializing, launching, attaching to, and monitoring Kata Symphony.
|
|
3
|
+
Pi extension for initializing, launching, attaching to, and monitoring Kata Symphony from Pi Coding Agent. It adds `/symphony:*` commands plus a live console for active workers, retry queue entries, blocked issues, completed issues, and pending escalations.
|
|
4
|
+
|
|
5
|
+

|
|
4
6
|
|
|
5
7
|
## Requirements
|
|
6
8
|
|
|
@@ -40,6 +42,49 @@ pnpm --dir apps/symphony/pi-extension typecheck
|
|
|
40
42
|
|
|
41
43
|
Run it locally with `pi -e ./apps/symphony/pi-extension`.
|
|
42
44
|
|
|
45
|
+
## Screenshot demo with mock data
|
|
46
|
+
|
|
47
|
+
Use the mock server to show an active Symphony session without tracker credentials or real workers.
|
|
48
|
+
|
|
49
|
+
1. Start the seeded mock Symphony API:
|
|
50
|
+
|
|
51
|
+
```sh
|
|
52
|
+
pnpm --dir apps/symphony/pi-extension run mock:wave3
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
If port `8787` is already in use, choose another port:
|
|
56
|
+
|
|
57
|
+
```sh
|
|
58
|
+
pnpm --dir apps/symphony/pi-extension run mock:wave3 -- --port 8788
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Expected output includes:
|
|
62
|
+
|
|
63
|
+
```text
|
|
64
|
+
Mock Symphony server: http://127.0.0.1:8787
|
|
65
|
+
/symphony:attach http://127.0.0.1:8787
|
|
66
|
+
/symphony:console
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Use the printed port in the attach command when you override `--port`.
|
|
70
|
+
|
|
71
|
+
2. Start Pi with the local extension:
|
|
72
|
+
|
|
73
|
+
```sh
|
|
74
|
+
pi -e ./apps/symphony/pi-extension
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
3. In Pi, run:
|
|
78
|
+
|
|
79
|
+
```text
|
|
80
|
+
/symphony:attach http://127.0.0.1:8787
|
|
81
|
+
/symphony:console
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
4. Use `↑` / `↓` to select the running worker, retry queue, blocked issue, completed issue, or pending escalation. Capture the screenshot when the console shows the seeded `SIM-*` items.
|
|
85
|
+
|
|
86
|
+
Suggested screenshot path for this README: `apps/symphony/pi-extension/assets/symphony-console.png`.
|
|
87
|
+
|
|
43
88
|
## Commands through Wave 3
|
|
44
89
|
|
|
45
90
|
- `/symphony:help`
|
package/package.json
CHANGED
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kata-sh/pi-symphony-extension",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.3.1",
|
|
4
4
|
"description": "Pi extension for launching, attaching to, and monitoring Kata Symphony",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"private": false,
|
|
7
7
|
"type": "module",
|
|
8
|
-
"keywords": [
|
|
8
|
+
"keywords": [
|
|
9
|
+
"pi-package",
|
|
10
|
+
"symphony",
|
|
11
|
+
"kata"
|
|
12
|
+
],
|
|
9
13
|
"repository": {
|
|
10
14
|
"type": "git",
|
|
11
15
|
"url": "git+https://github.com/gannonh/kata.git",
|
|
@@ -15,7 +19,12 @@
|
|
|
15
19
|
".": "./src/index.ts",
|
|
16
20
|
"./package.json": "./package.json"
|
|
17
21
|
},
|
|
18
|
-
"files": [
|
|
22
|
+
"files": [
|
|
23
|
+
"src",
|
|
24
|
+
"scripts",
|
|
25
|
+
"README.md",
|
|
26
|
+
"package.json"
|
|
27
|
+
],
|
|
19
28
|
"scripts": {
|
|
20
29
|
"build": "tsc --noEmit",
|
|
21
30
|
"typecheck": "tsc --noEmit",
|
|
@@ -25,7 +34,9 @@
|
|
|
25
34
|
"lint": "eslint src/ --max-warnings=0"
|
|
26
35
|
},
|
|
27
36
|
"pi": {
|
|
28
|
-
"extensions": [
|
|
37
|
+
"extensions": [
|
|
38
|
+
"./src/index.ts"
|
|
39
|
+
]
|
|
29
40
|
},
|
|
30
41
|
"peerDependencies": {
|
|
31
42
|
"@earendil-works/pi-ai": "*",
|
|
@@ -20,6 +20,16 @@ const server = createServer((req, res) => {
|
|
|
20
20
|
});
|
|
21
21
|
});
|
|
22
22
|
|
|
23
|
+
server.on("error", (error) => {
|
|
24
|
+
if (error?.code === "EADDRINUSE") {
|
|
25
|
+
console.error(`Port ${options.port} is already in use on ${options.host}.`);
|
|
26
|
+
console.error("Try another port, for example:");
|
|
27
|
+
console.error("pnpm --dir apps/symphony/pi-extension run mock:wave3 -- --port 8788");
|
|
28
|
+
process.exit(1);
|
|
29
|
+
}
|
|
30
|
+
throw error;
|
|
31
|
+
});
|
|
32
|
+
|
|
23
33
|
server.listen(options.port, options.host, () => {
|
|
24
34
|
const address = server.address();
|
|
25
35
|
if (!address || typeof address === "string") {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, expect, it } from "vitest";
|
|
2
|
-
import { buildEscalationRows, buildIssueRows, buildWorkerRows, formatEventRows } from "./console-model.ts";
|
|
2
|
+
import { buildEscalationRows, buildIssueRows, buildTriageRows, buildWorkerRows, formatEventRows } from "./console-model.ts";
|
|
3
3
|
import type { SymphonyEventEnvelope, SymphonyStateResponse } from "./http-client.ts";
|
|
4
4
|
|
|
5
5
|
function stateFixture(): SymphonyStateResponse {
|
|
@@ -126,6 +126,50 @@ describe("console model", () => {
|
|
|
126
126
|
});
|
|
127
127
|
});
|
|
128
128
|
|
|
129
|
+
it("includes triage sessions between running and retry rows", () => {
|
|
130
|
+
const state = stateFixture();
|
|
131
|
+
state.triage_sessions = [
|
|
132
|
+
{
|
|
133
|
+
issue_identifier: "#1",
|
|
134
|
+
run_id: "run-1",
|
|
135
|
+
stage_run_id: "stage-1",
|
|
136
|
+
attempt: 2,
|
|
137
|
+
harness: "pi",
|
|
138
|
+
model: "openai-codex/gpt-5.6-luna",
|
|
139
|
+
started_at: "2026-05-14T12:00:00Z",
|
|
140
|
+
last_activity_at: "2026-05-14T12:00:05Z",
|
|
141
|
+
last_event: "tool_execution_start",
|
|
142
|
+
last_event_message: "running read",
|
|
143
|
+
current_tool_name: "read",
|
|
144
|
+
current_tool_args_preview: "README.md",
|
|
145
|
+
total_tokens: 12,
|
|
146
|
+
},
|
|
147
|
+
];
|
|
148
|
+
|
|
149
|
+
const issueRows = buildIssueRows(state);
|
|
150
|
+
const triageRows = buildTriageRows(state);
|
|
151
|
+
|
|
152
|
+
expect(issueRows.map((row) => `${row.kind}:${row.issueIdentifier}`)).toEqual([
|
|
153
|
+
"running:SIM-123",
|
|
154
|
+
"running:SIM-777",
|
|
155
|
+
"triage:#1",
|
|
156
|
+
]);
|
|
157
|
+
expect(triageRows).toEqual([
|
|
158
|
+
{
|
|
159
|
+
issueIdentifier: "#1",
|
|
160
|
+
stageRunId: "stage-1",
|
|
161
|
+
attempt: "2",
|
|
162
|
+
harness: "pi",
|
|
163
|
+
model: "openai-codex/gpt-5.6-luna",
|
|
164
|
+
status: "triage#2",
|
|
165
|
+
lastEvent: "tool_execution_start",
|
|
166
|
+
message: "read: README.md",
|
|
167
|
+
lastActivity: "2026-05-14T12:00:05Z",
|
|
168
|
+
tokens: "12",
|
|
169
|
+
},
|
|
170
|
+
]);
|
|
171
|
+
});
|
|
172
|
+
|
|
129
173
|
it("rounds retry wait times up to the next second", () => {
|
|
130
174
|
const state = stateFixture();
|
|
131
175
|
state.retry_queue = [
|
package/src/console-model.ts
CHANGED
|
@@ -6,6 +6,7 @@ import type {
|
|
|
6
6
|
RunAttemptResponse,
|
|
7
7
|
SymphonyEventEnvelope,
|
|
8
8
|
SymphonyStateResponse,
|
|
9
|
+
TriageSessionResponse,
|
|
9
10
|
} from "./http-client.ts";
|
|
10
11
|
|
|
11
12
|
export interface WorkerRow {
|
|
@@ -23,7 +24,7 @@ export interface WorkerRow {
|
|
|
23
24
|
errorPreview: string;
|
|
24
25
|
}
|
|
25
26
|
|
|
26
|
-
export type IssueRowKind = "running" | "retry" | "blocked" | "completed";
|
|
27
|
+
export type IssueRowKind = "running" | "triage" | "retry" | "blocked" | "completed";
|
|
27
28
|
|
|
28
29
|
export interface IssueRow {
|
|
29
30
|
kind: IssueRowKind;
|
|
@@ -43,6 +44,19 @@ export interface IssueRow {
|
|
|
43
44
|
completedAt: string;
|
|
44
45
|
}
|
|
45
46
|
|
|
47
|
+
export interface TriageRow {
|
|
48
|
+
issueIdentifier: string;
|
|
49
|
+
stageRunId: string;
|
|
50
|
+
attempt: string;
|
|
51
|
+
harness: string;
|
|
52
|
+
model: string;
|
|
53
|
+
status: string;
|
|
54
|
+
lastEvent: string;
|
|
55
|
+
message: string;
|
|
56
|
+
lastActivity: string;
|
|
57
|
+
tokens: string;
|
|
58
|
+
}
|
|
59
|
+
|
|
46
60
|
export interface EscalationRow {
|
|
47
61
|
requestId: string;
|
|
48
62
|
issueId: string;
|
|
@@ -66,12 +80,31 @@ export function buildIssueRows(state: SymphonyStateResponse | undefined): IssueR
|
|
|
66
80
|
...Object.entries(state.running ?? {})
|
|
67
81
|
.map(([issueId, attempt]) => workerToIssueRow(issueId, attempt, state))
|
|
68
82
|
.sort(compareIssueRows),
|
|
83
|
+
...(state.triage_sessions ?? []).map(triageToIssueRow).sort(compareIssueRows),
|
|
69
84
|
...(state.retry_queue ?? []).map(retryToIssueRow).sort(compareIssueRows),
|
|
70
85
|
...(state.blocked ?? []).map(blockedToIssueRow).sort(compareIssueRows),
|
|
71
86
|
...(state.completed ?? []).map(completedToIssueRow).sort(compareIssueRows),
|
|
72
87
|
];
|
|
73
88
|
}
|
|
74
89
|
|
|
90
|
+
export function buildTriageRows(state: SymphonyStateResponse | undefined): TriageRow[] {
|
|
91
|
+
return (state?.triage_sessions ?? [])
|
|
92
|
+
.slice()
|
|
93
|
+
.sort((left, right) => left.issue_identifier.localeCompare(right.issue_identifier))
|
|
94
|
+
.map((session) => ({
|
|
95
|
+
issueIdentifier: session.issue_identifier,
|
|
96
|
+
stageRunId: session.stage_run_id,
|
|
97
|
+
attempt: String(session.attempt),
|
|
98
|
+
harness: session.harness,
|
|
99
|
+
model: session.model?.trim() || "-",
|
|
100
|
+
status: `triage#${session.attempt}`,
|
|
101
|
+
lastEvent: session.last_event?.trim() || "-",
|
|
102
|
+
message: triageMessage(session),
|
|
103
|
+
lastActivity: session.last_activity_at ?? session.started_at,
|
|
104
|
+
tokens: session.total_tokens === undefined ? "-" : String(session.total_tokens),
|
|
105
|
+
}));
|
|
106
|
+
}
|
|
107
|
+
|
|
75
108
|
export function buildEscalationRows(state: SymphonyStateResponse | undefined): EscalationRow[] {
|
|
76
109
|
return (state?.pending_escalations ?? [])
|
|
77
110
|
.slice()
|
|
@@ -89,12 +122,47 @@ export function buildEscalationRows(state: SymphonyStateResponse | undefined): E
|
|
|
89
122
|
|
|
90
123
|
export function formatEventRows(events: SymphonyEventEnvelope[], limit = 8): string[] {
|
|
91
124
|
return events
|
|
92
|
-
.filter(
|
|
125
|
+
.filter(
|
|
126
|
+
(event) =>
|
|
127
|
+
event.kind === "worker" ||
|
|
128
|
+
event.kind === "runtime" ||
|
|
129
|
+
event.kind === "triage" ||
|
|
130
|
+
event.kind.startsWith("escalation_") ||
|
|
131
|
+
event.event.startsWith("triage_"),
|
|
132
|
+
)
|
|
93
133
|
.slice(-limit)
|
|
94
134
|
.reverse()
|
|
95
135
|
.map((event) => [event.timestamp, event.severity, event.kind, event.issue ?? "-", event.event, eventSummary(event)].filter(Boolean).join(" "));
|
|
96
136
|
}
|
|
97
137
|
|
|
138
|
+
function triageToIssueRow(session: TriageSessionResponse): IssueRow {
|
|
139
|
+
return {
|
|
140
|
+
kind: "triage",
|
|
141
|
+
issueId: session.stage_run_id,
|
|
142
|
+
issueIdentifier: session.issue_identifier,
|
|
143
|
+
title: triageMessage(session),
|
|
144
|
+
status: `triage#${session.attempt}`,
|
|
145
|
+
trackerState: "triage",
|
|
146
|
+
attempt: String(session.attempt),
|
|
147
|
+
turnCount: "1",
|
|
148
|
+
maxTurns: "1",
|
|
149
|
+
lastActivity: session.last_activity_at ?? session.started_at,
|
|
150
|
+
workerHost: session.harness,
|
|
151
|
+
workspacePath: session.session_id ?? "-",
|
|
152
|
+
errorPreview: "-",
|
|
153
|
+
blockers: "-",
|
|
154
|
+
completedAt: "-",
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function triageMessage(session: TriageSessionResponse): string {
|
|
159
|
+
if (session.current_tool_name) {
|
|
160
|
+
const args = session.current_tool_args_preview?.trim();
|
|
161
|
+
return args ? `${session.current_tool_name}: ${truncateText(args, 80)}` : session.current_tool_name;
|
|
162
|
+
}
|
|
163
|
+
return session.last_event_message?.trim() || session.last_event?.trim() || "running";
|
|
164
|
+
}
|
|
165
|
+
|
|
98
166
|
function workerToIssueRow(issueId: string, attempt: RunAttemptResponse, state: SymphonyStateResponse | undefined): IssueRow {
|
|
99
167
|
return {
|
|
100
168
|
kind: "running",
|
package/src/console.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import { matchesKey, truncateToWidth } from "@earendil-works/pi-tui";
|
|
3
|
-
import { buildEscalationRows, buildIssueRows, buildWorkerRows, formatEventRows, type EscalationRow, type IssueRow, type WorkerRow } from "./console-model.ts";
|
|
3
|
+
import { buildEscalationRows, buildIssueRows, buildTriageRows, buildWorkerRows, formatEventRows, type EscalationRow, type IssueRow, type TriageRow, type WorkerRow } from "./console-model.ts";
|
|
4
4
|
import { startSymphonyEventStream, type EventStreamHandle } from "./event-stream.ts";
|
|
5
5
|
import type { SymphonyEventEnvelope, SymphonyStateResponse } from "./http-client.ts";
|
|
6
6
|
import type { SymphonyRuntime } from "./runtime.ts";
|
|
@@ -124,6 +124,8 @@ export class SymphonyConsoleComponent {
|
|
|
124
124
|
const escalationRows: EscalationRow[] = buildEscalationRows(symphonyState);
|
|
125
125
|
this.clampSelection(issueRows.length + escalationRows.length);
|
|
126
126
|
const runningRows = issueRows.filter((row) => row.kind === "running");
|
|
127
|
+
const triageRows = issueRows.filter((row) => row.kind === "triage");
|
|
128
|
+
const triageDetailRows: TriageRow[] = buildTriageRows(symphonyState);
|
|
127
129
|
const retryRows = issueRows.filter((row) => row.kind === "retry");
|
|
128
130
|
const blockedRows = issueRows.filter((row) => row.kind === "blocked");
|
|
129
131
|
const completedRows = issueRows.filter((row) => row.kind === "completed");
|
|
@@ -132,6 +134,10 @@ export class SymphonyConsoleComponent {
|
|
|
132
134
|
const connection = state.attachedBaseUrl ? color(theme, "success", "attached") : color(theme, "error", "detached");
|
|
133
135
|
const polling = health?.pollingChecking ? color(theme, "warning", "checking") : color(theme, "success", "idle");
|
|
134
136
|
const consoleWidth = Math.max(44, width);
|
|
137
|
+
const triageOffset = runningRows.length;
|
|
138
|
+
const retryOffset = triageOffset + triageRows.length;
|
|
139
|
+
const blockedOffset = retryOffset + retryRows.length;
|
|
140
|
+
const completedOffset = blockedOffset + blockedRows.length;
|
|
135
141
|
const lines = [
|
|
136
142
|
color(theme, "accent", bold(theme, "Symphony Console")),
|
|
137
143
|
"",
|
|
@@ -145,13 +151,14 @@ export class SymphonyConsoleComponent {
|
|
|
145
151
|
], consoleWidth, theme),
|
|
146
152
|
"",
|
|
147
153
|
...boxLines("Worker Summary", [
|
|
148
|
-
`workers: ${color(theme, "success", `running: ${health?.runningCount ?? workers.length}`)} | ${color(theme, "warning", `retry: ${health?.retryCount ?? retryRows.length}`)} | ${color(theme, "error", `blocked: ${health?.blockedCount ?? blockedRows.length}`)} | ${color(theme, "accent", `completed: ${health?.completedCount ?? completedRows.length}`)}`,
|
|
154
|
+
`workers: ${color(theme, "success", `running: ${health?.runningCount ?? workers.length}`)} | ${color(theme, "accent", `triage: ${triageRows.length}`)} | ${color(theme, "warning", `retry: ${health?.retryCount ?? retryRows.length}`)} | ${color(theme, "error", `blocked: ${health?.blockedCount ?? blockedRows.length}`)} | ${color(theme, "accent", `completed: ${health?.completedCount ?? completedRows.length}`)}`,
|
|
149
155
|
], consoleWidth, theme),
|
|
150
156
|
"",
|
|
151
157
|
...boxLines("Running Workers", renderIssueTable(runningRows, this.selectedIndex, 0, theme), consoleWidth, theme),
|
|
152
|
-
...boxLines("
|
|
153
|
-
...boxLines("
|
|
154
|
-
...boxLines("
|
|
158
|
+
...boxLines("Triage", renderTriageTable(triageDetailRows, this.selectedIndex, triageOffset, theme), consoleWidth, theme),
|
|
159
|
+
...boxLines("Retry Queue", renderIssueTable(retryRows, this.selectedIndex, retryOffset, theme), consoleWidth, theme),
|
|
160
|
+
...boxLines("Blocked Issues", renderIssueTable(blockedRows, this.selectedIndex, blockedOffset, theme), consoleWidth, theme),
|
|
161
|
+
...boxLines("Completed Issues", renderIssueTable(completedRows, this.selectedIndex, completedOffset, theme), consoleWidth, theme),
|
|
155
162
|
...boxLines("Selected Issue", renderSelectedIssueDetails(selectedIssue, state.console.showDetails, theme), consoleWidth, theme),
|
|
156
163
|
...boxLines("Pending Escalations", renderEscalationTable(escalationRows, this.selectedIndex - issueRows.length, theme), consoleWidth, theme),
|
|
157
164
|
...boxLines("Selected Escalation", renderSelectedEscalationDetails(escalationRows[this.selectedIndex - issueRows.length], state.console.showDetails, theme), consoleWidth, theme),
|
|
@@ -295,6 +302,27 @@ function renderIssueTable(rows: IssueRow[], selectedIndex: number, selectedOffse
|
|
|
295
302
|
return lines;
|
|
296
303
|
}
|
|
297
304
|
|
|
305
|
+
function renderTriageTable(rows: TriageRow[], selectedIndex: number, selectedOffset: number, theme?: ConsoleTheme): string[] {
|
|
306
|
+
const lines = [color(theme, "dim", "sel issue attempt harness model event message")];
|
|
307
|
+
if (rows.length === 0) return [...lines, color(theme, "dim", "- none")];
|
|
308
|
+
|
|
309
|
+
for (const [index, row] of rows.entries()) {
|
|
310
|
+
const rowIndex = selectedOffset + index;
|
|
311
|
+
const selected = rowIndex === selectedIndex ? ">" : " ";
|
|
312
|
+
const line = [
|
|
313
|
+
selected,
|
|
314
|
+
pad(row.issueIdentifier, 8),
|
|
315
|
+
pad(row.attempt, 7),
|
|
316
|
+
pad(row.harness, 8),
|
|
317
|
+
pad(row.model, 21),
|
|
318
|
+
pad(row.lastEvent, 16),
|
|
319
|
+
row.message,
|
|
320
|
+
].join(" ");
|
|
321
|
+
lines.push(rowIndex === selectedIndex ? selectedLine(theme, line) : line);
|
|
322
|
+
}
|
|
323
|
+
return lines;
|
|
324
|
+
}
|
|
325
|
+
|
|
298
326
|
function renderSelectedIssueDetails(issue: IssueRow | undefined, showDetails: boolean, theme?: ConsoleTheme): string[] {
|
|
299
327
|
if (!showDetails) return [];
|
|
300
328
|
if (!issue) return [color(theme, "dim", "none")];
|
|
@@ -315,6 +343,15 @@ function renderSelectedIssueDetails(issue: IssueRow | undefined, showDetails: bo
|
|
|
315
343
|
`workspace: ${color(theme, "dim", issue.workspacePath)}`,
|
|
316
344
|
`error: ${issue.errorPreview === "-" ? color(theme, "dim", issue.errorPreview) : color(theme, "error", issue.errorPreview)}`,
|
|
317
345
|
);
|
|
346
|
+
} else if (issue.kind === "triage") {
|
|
347
|
+
lines.push(
|
|
348
|
+
`stage: triage`,
|
|
349
|
+
`attempt: ${issue.attempt}`,
|
|
350
|
+
`harness: ${issue.workerHost}`,
|
|
351
|
+
`last activity: ${color(theme, "dim", issue.lastActivity)}`,
|
|
352
|
+
`session: ${color(theme, "dim", issue.workspacePath)}`,
|
|
353
|
+
`detail: ${issue.title}`,
|
|
354
|
+
);
|
|
318
355
|
} else if (issue.kind === "retry") {
|
|
319
356
|
lines.push(
|
|
320
357
|
`attempt: ${issue.attempt}`,
|
package/src/http-client.ts
CHANGED
|
@@ -145,6 +145,23 @@ export interface CodexTotalsResponse {
|
|
|
145
145
|
seconds_running: number;
|
|
146
146
|
}
|
|
147
147
|
|
|
148
|
+
export interface TriageSessionResponse {
|
|
149
|
+
issue_identifier: string;
|
|
150
|
+
run_id: string;
|
|
151
|
+
stage_run_id: string;
|
|
152
|
+
attempt: number;
|
|
153
|
+
harness: string;
|
|
154
|
+
model?: string | null;
|
|
155
|
+
started_at: string;
|
|
156
|
+
last_activity_at?: string | null;
|
|
157
|
+
last_event?: string | null;
|
|
158
|
+
last_event_message?: string | null;
|
|
159
|
+
session_id?: string | null;
|
|
160
|
+
current_tool_name?: string | null;
|
|
161
|
+
current_tool_args_preview?: string | null;
|
|
162
|
+
total_tokens?: number;
|
|
163
|
+
}
|
|
164
|
+
|
|
148
165
|
export interface SymphonyStateResponse {
|
|
149
166
|
tracker_project_url?: string | null;
|
|
150
167
|
running?: Record<string, RunAttemptResponse>;
|
|
@@ -154,6 +171,8 @@ export interface SymphonyStateResponse {
|
|
|
154
171
|
blocked: BlockedIssueResponse[];
|
|
155
172
|
pending_escalations?: PendingEscalationResponse[];
|
|
156
173
|
completed: CompletedIssueResponse[];
|
|
174
|
+
/** In-flight A1 triage attempts (optional for older Symphony builds). */
|
|
175
|
+
triage_sessions?: TriageSessionResponse[];
|
|
157
176
|
polling?: {
|
|
158
177
|
checking?: boolean;
|
|
159
178
|
next_poll_in_ms?: number;
|
|
@@ -406,6 +425,7 @@ function validateSymphonyStateResponse(value: unknown, details: Record<string, u
|
|
|
406
425
|
validateOptionalRecord(value, "running_sessions", details);
|
|
407
426
|
validateOptionalRecord(value, "running_session_info", details);
|
|
408
427
|
validateOptionalArray(value, "claimed", details);
|
|
428
|
+
validateOptionalTriageSessions(value, details);
|
|
409
429
|
validatePendingEscalations(value, "pending_escalations", details);
|
|
410
430
|
validateSharedContextSummary(value.shared_context, details, "shared_context", throwNonSymphonyState);
|
|
411
431
|
validateSupervisorSnapshot(value.supervisor, details, "supervisor", throwNonSymphonyState);
|
|
@@ -748,6 +768,31 @@ function validateOptionalArray(value: Record<string, unknown>, field: string, de
|
|
|
748
768
|
}
|
|
749
769
|
}
|
|
750
770
|
|
|
771
|
+
function validateOptionalTriageSessions(value: Record<string, unknown>, details: Record<string, unknown>): void {
|
|
772
|
+
const fieldValue = value.triage_sessions;
|
|
773
|
+
if (fieldValue === undefined) return;
|
|
774
|
+
if (!Array.isArray(fieldValue)) {
|
|
775
|
+
throwNonSymphonyState(details, "state response field had an invalid shape", {
|
|
776
|
+
field: "triage_sessions",
|
|
777
|
+
expected: "array",
|
|
778
|
+
});
|
|
779
|
+
}
|
|
780
|
+
fieldValue.forEach((entry, index) => {
|
|
781
|
+
if (!isRecord(entry)) {
|
|
782
|
+
throwNonSymphonyState(details, "state response field had an invalid shape", {
|
|
783
|
+
field: `triage_sessions.${index}`,
|
|
784
|
+
expected: "object",
|
|
785
|
+
});
|
|
786
|
+
}
|
|
787
|
+
validateRequiredString(entry, "issue_identifier", details, `triage_sessions.${index}.issue_identifier`);
|
|
788
|
+
validateRequiredString(entry, "run_id", details, `triage_sessions.${index}.run_id`);
|
|
789
|
+
validateRequiredString(entry, "stage_run_id", details, `triage_sessions.${index}.stage_run_id`);
|
|
790
|
+
validateRequiredNumber(entry, "attempt", details, `triage_sessions.${index}.attempt`);
|
|
791
|
+
validateRequiredString(entry, "harness", details, `triage_sessions.${index}.harness`);
|
|
792
|
+
validateRequiredString(entry, "started_at", details, `triage_sessions.${index}.started_at`);
|
|
793
|
+
});
|
|
794
|
+
}
|
|
795
|
+
|
|
751
796
|
function validateOptionalRecord(value: Record<string, unknown>, field: string, details: Record<string, unknown>, detailField = field): void {
|
|
752
797
|
const fieldValue = value[field];
|
|
753
798
|
if (fieldValue === undefined) return;
|