Three small wins from boocode_code_review_v2 §1 #11/#7/#8. #11 sampling knobs: top_n_sigma + dry_* family as first-class Agent fields, threaded into the request body via providerOptions.openaiCompatible. Fixes a latent bug — top_k (rejected by the AI-SDK provider) and min_p (never passed to streamText) were dead on the wire; both now route through the same channel. --reasoning-budget documented in data/AGENTS.md. #7 live PTY stream-json: new stream-json-parser.ts line-buffers qwen/claude NDJSON and emits text/reasoning/tool frames live + persists, with a fallback to the old opaque slice. claude gets --output-format stream-json --verbose. #8 token UI: agent_sessions input/output_tokens/cost now flow through the route + type and render beside the AgentComposerBar session chip. Built by 3 parallel agents. Server 523 + coder 245 tests passing; builds + web tsc clean. Builds on v2.7.2. openspec sampling-streamjson-tokens. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
60 lines
2.4 KiB
TypeScript
60 lines
2.4 KiB
TypeScript
import type { FastifyInstance } from 'fastify';
|
|
import type { Sql } from '../db.js';
|
|
|
|
// v2.6 Phase 1-UX (design §9b): chat-scoped "resumed vs new session" indicator.
|
|
// `agent_sessions` is keyed (chat_id, agent) — the tab/chat is the agent-context
|
|
// unit (P1.5-b). The route param is a SESSION id, so we resolve every chat in the
|
|
// session and return the union of their agent_sessions rows. A session with two
|
|
// opencode tabs yields two rows (one per chat); the frontend keys the chip per
|
|
// chat, but the wire shape is a flat per-(chat,agent) list.
|
|
//
|
|
// has_session = agent_session_id IS NOT NULL — i.e. a native backend session id
|
|
// (opencode/ACP) was created and stored, so switching back resumes rather than
|
|
// starts fresh.
|
|
export interface AgentSessionRow {
|
|
agent: string;
|
|
status: string;
|
|
has_session: boolean;
|
|
last_active_at: string | null;
|
|
// v2.6.8 per-(chat,agent) running token/cost totals (sampling-streamjson-tokens
|
|
// #8). BIGINT columns arrive as strings over the wire; the frontend coerces.
|
|
input_tokens: number;
|
|
output_tokens: number;
|
|
cost: number;
|
|
}
|
|
|
|
export function registerAgentSessionRoutes(app: FastifyInstance, sql: Sql): void {
|
|
// GET /api/sessions/:sessionId/agent-sessions — list the agent-session rows for
|
|
// every chat in the session (drives the AgentComposerBar resumed/new chip).
|
|
app.get<{ Params: { sessionId: string } }>(
|
|
'/api/sessions/:sessionId/agent-sessions',
|
|
async (req, reply) => {
|
|
const sessionId = req.params.sessionId;
|
|
|
|
const session = await sql<{ id: string }[]>`SELECT id FROM sessions WHERE id = ${sessionId}`;
|
|
if (session.length === 0) {
|
|
reply.code(404);
|
|
return { error: 'session not found' };
|
|
}
|
|
|
|
// Join through chats so the session-scoped param resolves to its (chat,agent)
|
|
// rows. last_active_at first → the frontend reads the freshest activity.
|
|
const rows = await sql<AgentSessionRow[]>`
|
|
SELECT
|
|
a.agent AS agent,
|
|
a.status AS status,
|
|
(a.agent_session_id IS NOT NULL) AS has_session,
|
|
a.last_active_at AS last_active_at,
|
|
a.input_tokens AS input_tokens,
|
|
a.output_tokens AS output_tokens,
|
|
a.cost AS cost
|
|
FROM agent_sessions a
|
|
JOIN chats c ON c.id = a.chat_id
|
|
WHERE c.session_id = ${sessionId}
|
|
ORDER BY a.last_active_at DESC NULLS LAST, a.agent ASC
|
|
`;
|
|
return rows;
|
|
},
|
|
);
|
|
}
|