2026-06-09 17:08:34 +02:00
|
|
|
import api, { API_URL } from "./client";
|
2026-06-24 01:15:57 +02:00
|
|
|
import type {
|
|
|
|
|
BatchConfirmPayload,
|
|
|
|
|
BatchConfirmResult,
|
|
|
|
|
BatchPreviewResult,
|
|
|
|
|
ConfirmPayload,
|
|
|
|
|
DraftProposal,
|
|
|
|
|
} from "./prompter";
|
2026-06-09 17:08:34 +02:00
|
|
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
// Live intake chat — the panel side of the spawned-agent bridge.
|
|
|
|
|
//
|
|
|
|
|
// Unlike the legacy Ollama session API (`prompter.ts`), the brain here is a
|
|
|
|
|
// real spawned Claude Code agent. The panel:
|
|
|
|
|
// 1. POSTs /prompter/live/start with the scope (project XOR product) + the
|
|
|
|
|
// opening message → gets a session id (the agent is spawned, the opening
|
|
|
|
|
// message is delivered server-side once its container is reachable).
|
|
|
|
|
// 2. opens an SSE stream and watches the agent work (token deltas, tool
|
|
|
|
|
// calls), and renders the draft card when the agent proposes one.
|
|
|
|
|
// 3. POSTs each subsequent message to /messages; replies arrive over SSE.
|
|
|
|
|
// 4. on confirm, /confirm turns the draft into a task and reaps the agent.
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
2026-06-24 01:15:57 +02:00
|
|
|
/** Open a live chat scoped to exactly one of project / product / project_ids
|
|
|
|
|
* (a MegaTask spanning several possibly-unrelated repos). */
|
2026-06-09 17:08:34 +02:00
|
|
|
export interface StartLivePayload {
|
|
|
|
|
project_id?: string;
|
|
|
|
|
product_id?: string;
|
2026-06-24 01:15:57 +02:00
|
|
|
project_ids?: string[];
|
2026-06-09 17:08:34 +02:00
|
|
|
initial_message?: string;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export interface StartLiveResponse {
|
|
|
|
|
session_id: string;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** Event kinds the container relays — mirrors the backend driver.StreamChunk. */
|
|
|
|
|
export type LiveEventKind =
|
|
|
|
|
| "text"
|
|
|
|
|
| "thinking"
|
|
|
|
|
| "tool_use"
|
|
|
|
|
| "tool_result"
|
|
|
|
|
| "turn_end"
|
|
|
|
|
| "system"
|
|
|
|
|
| "draft"
|
2026-06-24 01:15:57 +02:00
|
|
|
| "batch"
|
2026-06-09 17:08:34 +02:00
|
|
|
| "error";
|
|
|
|
|
|
|
|
|
|
/** One normalized event from the agent's live reply. */
|
|
|
|
|
export interface LiveEvent {
|
|
|
|
|
kind: LiveEventKind;
|
|
|
|
|
text?: string;
|
|
|
|
|
tool?: string;
|
|
|
|
|
data?: Record<string, unknown>;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** Every named SSE event we subscribe to; each carries the full LiveEvent as JSON. */
|
|
|
|
|
export const LIVE_EVENT_KINDS: LiveEventKind[] = [
|
|
|
|
|
"text",
|
|
|
|
|
"thinking",
|
|
|
|
|
"tool_use",
|
|
|
|
|
"tool_result",
|
|
|
|
|
"turn_end",
|
|
|
|
|
"system",
|
|
|
|
|
"draft",
|
2026-06-24 01:15:57 +02:00
|
|
|
"batch",
|
2026-06-09 17:08:34 +02:00
|
|
|
"error",
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
export const prompterLiveApi = {
|
|
|
|
|
/** Spawn the intake agent for a new chat. */
|
|
|
|
|
start: async (payload: StartLivePayload): Promise<StartLiveResponse> => {
|
|
|
|
|
const { data } = await api.post<StartLiveResponse>(
|
|
|
|
|
"/prompter/live/start",
|
2026-06-24 01:15:57 +02:00
|
|
|
payload,
|
2026-06-09 17:08:34 +02:00
|
|
|
);
|
|
|
|
|
return data;
|
|
|
|
|
},
|
|
|
|
|
|
2026-06-14 00:40:18 +02:00
|
|
|
/** Re-open intake to re-draft a board-reviewed task with the board's feedback.
|
|
|
|
|
* Spawns a fresh session seeded with the current draft + the board review. */
|
|
|
|
|
reInterview: async (taskId: string): Promise<StartLiveResponse> => {
|
|
|
|
|
const { data } = await api.post<StartLiveResponse>(
|
2026-06-24 01:15:57 +02:00
|
|
|
`/prompter/live/re-interview/${taskId}`,
|
2026-06-14 00:40:18 +02:00
|
|
|
);
|
|
|
|
|
return data;
|
|
|
|
|
},
|
|
|
|
|
|
2026-06-30 08:08:35 +02:00
|
|
|
/** SSE URL the panel opens to watch the agent. EventSource cannot send custom
|
|
|
|
|
* headers, so the live-intake stream carries no `X-Agent-*` auth — the route
|
|
|
|
|
* is authenticated solely by the opaque, unguessable session id on the
|
|
|
|
|
* trusted internal network. By design: any session-id leakage grants stream
|
|
|
|
|
* access, so session ids must be treated as bearer credentials (never logged
|
|
|
|
|
* client-side, never put in a shareable URL). */
|
2026-06-09 17:08:34 +02:00
|
|
|
streamUrl: (sessionId: string): string =>
|
|
|
|
|
`${API_URL}/prompter/live/${sessionId}/stream`,
|
|
|
|
|
|
2026-06-10 12:04:24 +02:00
|
|
|
/** Is this session still running? The panel calls this after a reload to
|
|
|
|
|
* decide whether to reconnect the chat or fall back to the scope form. */
|
|
|
|
|
status: async (sessionId: string): Promise<{ alive: boolean }> => {
|
|
|
|
|
const { data } = await api.get<{ alive: boolean }>(
|
2026-06-24 01:15:57 +02:00
|
|
|
`/prompter/live/${sessionId}/status`,
|
2026-06-10 12:04:24 +02:00
|
|
|
);
|
|
|
|
|
return data;
|
|
|
|
|
},
|
|
|
|
|
|
2026-06-09 17:08:34 +02:00
|
|
|
/** Deliver the human's message to the running agent; the reply streams back. */
|
|
|
|
|
sendMessage: async (sessionId: string, text: string): Promise<void> => {
|
|
|
|
|
await api.post(`/prompter/live/${sessionId}/messages`, { text });
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
/** Reap the session (draft confirmed, or the human left the page). */
|
|
|
|
|
stop: async (sessionId: string): Promise<void> => {
|
|
|
|
|
await api.post(`/prompter/live/${sessionId}/stop`);
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
/** Confirm the draft → create the task and reap the agent (Phase 4 backend). */
|
|
|
|
|
confirm: async (
|
|
|
|
|
sessionId: string,
|
2026-06-24 01:15:57 +02:00
|
|
|
payload: ConfirmPayload,
|
2026-06-09 17:08:34 +02:00
|
|
|
): Promise<{ task_id: string }> => {
|
|
|
|
|
const { data } = await api.post<{ task_id: string }>(
|
|
|
|
|
`/prompter/live/${sessionId}/confirm`,
|
2026-06-24 01:15:57 +02:00
|
|
|
payload,
|
|
|
|
|
);
|
|
|
|
|
return data;
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
/** Preview a MegaTask's waves without creating anything — for the human to
|
|
|
|
|
* review the sequencing before confirming. */
|
|
|
|
|
previewBatch: async (
|
|
|
|
|
sessionId: string,
|
|
|
|
|
drafts: DraftProposal[],
|
|
|
|
|
): Promise<BatchPreviewResult> => {
|
|
|
|
|
const { data } = await api.post<BatchPreviewResult>(
|
|
|
|
|
`/prompter/live/${sessionId}/preview-batch`,
|
|
|
|
|
{ drafts },
|
|
|
|
|
);
|
|
|
|
|
return data;
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
/** Confirm a MegaTask → create the umbrella + sequenced root-subtasks, reap.
|
|
|
|
|
* Returns the umbrella id, the root-subtask ids, and the computed waves. */
|
|
|
|
|
confirmBatch: async (
|
|
|
|
|
sessionId: string,
|
|
|
|
|
payload: BatchConfirmPayload,
|
|
|
|
|
): Promise<BatchConfirmResult> => {
|
|
|
|
|
const { data } = await api.post<BatchConfirmResult>(
|
|
|
|
|
`/prompter/live/${sessionId}/confirm-batch`,
|
|
|
|
|
payload,
|
2026-06-09 17:08:34 +02:00
|
|
|
);
|
|
|
|
|
return data;
|
|
|
|
|
},
|
|
|
|
|
};
|