Skip to content

Commit ae486de

Browse files
authored
feat(core,server,cli): engine, event log, and non-TUI tests
Squash-merge PR #160: EventLog, unified SSE (Last-Event-ID), frame reducer, wire models, migrations, and tests.
1 parent 5135c1c commit ae486de

479 files changed

Lines changed: 38957 additions & 22455 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

packages/shared/api-client/src/client.ts

Lines changed: 68 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import type {
1515
ClientPresence,
1616
CombinedStats,
1717
CreateChatSessionInput,
18+
CreateSessionRequest,
1819
CreateProjectInput,
1920
CreateTaskInput,
2021
DiffFile,
@@ -34,7 +35,10 @@ import type {
3435
ReviewStatusResponse,
3536
RunTaskInput,
3637
SearchMentionsInput,
38+
SessionItemResponse,
39+
SessionReplayPage,
3740
SessionTimelineEntry,
41+
SessionsResponse,
3842
SettingsResponse,
3943
TaskCommitsResponse,
4044
TaskCountsResponse,
@@ -176,7 +180,7 @@ export class KaganApiClient {
176180

177181
// -- Core HTTP Methods ----------------------------------------------------
178182

179-
protected getFullUrl(path: string): string {
183+
getFullUrl(path: string): string {
180184
return `${this._protocol}://${this._baseUrl}${path}`;
181185
}
182186

@@ -244,6 +248,20 @@ export class KaganApiClient {
244248
return this.request<T>("DELETE", path);
245249
}
246250

251+
/**
252+
* Raw fetch with auth headers appended — for streaming paths (SSE) that must
253+
* bypass envelope unwrapping. Returns the raw Response.
254+
*
255+
* @param url Full URL (protocol + host + path). Use getFullUrl() to build it.
256+
* @param init Optional RequestInit. Auth headers are merged in automatically.
257+
*/
258+
public streamRequest(url: string, init?: RequestInit): Promise<Response> {
259+
return this._fetchImpl(url, {
260+
...init,
261+
headers: { ...init?.headers, ...this.getAuthHeaders() },
262+
});
263+
}
264+
247265
// -- Tasks ----------------------------------------------------------------
248266

249267
/** GET /api/tasks */
@@ -823,6 +841,55 @@ export class KaganApiClient {
823841
async verifyApi(): Promise<void> {
824842
await this.getSettings();
825843
}
844+
845+
// -- Unified sessions -----------------------------------------------------
846+
847+
/** GET /api/v1/sessions */
848+
getSessions(): Promise<SessionsResponse> {
849+
return this.get<SessionsResponse>("/api/v1/sessions");
850+
}
851+
852+
/** POST /api/v1/sessions */
853+
createSession(input: CreateSessionRequest): Promise<SessionItemResponse> {
854+
return this.post<SessionItemResponse>("/api/v1/sessions", input);
855+
}
856+
857+
/** POST /api/v1/sessions/:sessionId/message */
858+
sendSessionMessage(
859+
sessionId: string,
860+
text: string,
861+
options?: { agent_backend?: string; attachments?: unknown[] },
862+
): Promise<unknown> {
863+
return this.post<unknown>(`/api/v1/sessions/${encodeURIComponent(sessionId)}/message`, {
864+
text,
865+
...options,
866+
});
867+
}
868+
869+
/** POST /api/v1/sessions/:sessionId/stop */
870+
stopSession(sessionId: string): Promise<unknown> {
871+
return this.post<unknown>(`/api/v1/sessions/${encodeURIComponent(sessionId)}/stop`, {});
872+
}
873+
874+
/** POST /api/v1/sessions/:sessionId/close */
875+
closeSession(sessionId: string): Promise<unknown> {
876+
return this.post<unknown>(`/api/v1/sessions/${encodeURIComponent(sessionId)}/close`, {});
877+
}
878+
879+
/**
880+
* GET /api/v1/sessions/:sessionId/replay
881+
* Cursor-based pagination for agent session events.
882+
*/
883+
getSessionReplay(
884+
sessionId: string,
885+
opts: { cursor?: string; limit?: number; direction?: "forward" | "backward" } = {},
886+
): Promise<SessionReplayPage> {
887+
const params = new URLSearchParams();
888+
if (opts.cursor) params.set("cursor", opts.cursor);
889+
if (opts.limit !== undefined) params.set("limit", String(opts.limit));
890+
if (opts.direction) params.set("direction", opts.direction);
891+
return this.get<SessionReplayPage>(`/api/v1/sessions/${sessionId}/replay${withQuery(params)}`);
892+
}
826893
}
827894

828895
// ----------------------------------------------------------------------------

packages/shared/api-client/src/event-rendering.ts

Lines changed: 21 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,11 @@
44
* Transforms raw `(event_type, payload)` pairs into a client-agnostic
55
* `RenderableEvent` that carries kind, title, body, and severity.
66
* Mirrors the Python implementation in `kagan.core._event_rendering`.
7+
*
8+
* Note: task session events stored in the DB still use the legacy
9+
* agent_events kind strings (e.g. ``"output_chunk"``, ``"tool_call_start"``).
10+
* We compare against those string literals directly rather than via a const
11+
* so this module remains independent of the chat stream EVENT_TYPE constant.
712
*/
813

914
// ── Enums ────────────────────────────────────────────────────────────────────
@@ -134,8 +139,12 @@ export function renderEvent(
134139
sessionId: string = "",
135140
): RenderableEvent | null {
136141
const ids = { event_id: eventId, session_id: sessionId };
142+
const normalizedEventType = eventType.toLowerCase();
143+
144+
// Task session events stored in the DB use legacy agent_events kind strings.
145+
// We compare lowercase to handle both old UPPER_SNAKE and new snake_case storage.
137146

138-
if (eventType === "OUTPUT_CHUNK") {
147+
if (normalizedEventType === "output_chunk") {
139148
const text = String(payload.text ?? "");
140149
if (!text) return null;
141150
const thought = Boolean(payload.thought);
@@ -145,11 +154,11 @@ export function renderEvent(
145154
});
146155
}
147156

148-
if (eventType === "TOOL_CALL_START") {
157+
if (normalizedEventType === "tool_call_start") {
149158
return make("tool_start", extractToolTitle(payload), ids);
150159
}
151160

152-
if (eventType === "TOOL_CALL_UPDATE") {
161+
if (normalizedEventType === "tool_call_update") {
153162
const status = extractToolStatus(payload, "done");
154163
if (status === "completed" || status === "done") return null;
155164
return make("tool_update", extractToolTitle(payload), {
@@ -158,12 +167,12 @@ export function renderEvent(
158167
});
159168
}
160169

161-
if (eventType === "AGENT_STATUS") {
170+
if (normalizedEventType === "agent_status") {
162171
// Skipped — internal heartbeat; not meaningful to display in any client.
163172
return null;
164173
}
165174

166-
if (eventType === "TASK_STATUS_CHANGED") {
175+
if (normalizedEventType === "task_status_changed") {
167176
const from = String(payload.from ?? "?");
168177
const to = String(payload.to ?? "?");
169178
return make("status_change", `${from} -> ${to}`, {
@@ -172,7 +181,7 @@ export function renderEvent(
172181
});
173182
}
174183

175-
if (eventType === "CRITERION_VERDICT") {
184+
if (normalizedEventType === "criterion_verdict") {
176185
const verdict = String(payload.verdict ?? "");
177186
const reason = String(payload.reason ?? "");
178187
const verdictLabel = verdict === "PASS" ? "PASS" : verdict === "SKIP" ? "SKIP" : "FAIL";
@@ -186,14 +195,14 @@ export function renderEvent(
186195
});
187196
}
188197

189-
if (eventType === "AGENT_COMPLETED") {
198+
if (normalizedEventType === "agent_completed") {
190199
return make("note", "Agent completed", {
191200
severity: "success",
192201
...ids,
193202
});
194203
}
195204

196-
if (eventType === "AGENT_FAILED") {
205+
if (normalizedEventType === "agent_failed") {
197206
const error = String(payload.error ?? payload.details ?? "Agent failed");
198207
return make("error", "Agent failed", {
199208
body: error,
@@ -202,14 +211,14 @@ export function renderEvent(
202211
});
203212
}
204213

205-
if (eventType === "MERGE_COMPLETED") {
214+
if (normalizedEventType === "merge_completed") {
206215
return make("merge", "Merge completed", {
207216
severity: "success",
208217
...ids,
209218
});
210219
}
211220

212-
if (eventType === "MERGE_FAILED") {
221+
if (normalizedEventType === "merge_failed") {
213222
const error = String(payload.error ?? "unknown");
214223
return make("merge", "Merge failed", {
215224
body: error,
@@ -218,11 +227,11 @@ export function renderEvent(
218227
});
219228
}
220229

221-
if (eventType === "PLAN_UPDATE") {
230+
if (normalizedEventType === "plan_update") {
222231
return make("plan", "Plan updated", ids);
223232
}
224233

225-
if (eventType === "AUTO_REVIEW_STARTED") {
234+
if (normalizedEventType === "auto_review_started") {
226235
return make("note", "Auto-review started", ids);
227236
}
228237

0 commit comments

Comments
 (0)