nomos+web: streaming, provider routing, event gap-fill, embedded UI; fix approval FK & session context
Agent (cmd/nomos): - Stream LLM tokens via NewStreaming; emit text_delta then final text. - OpenRouter provider routing: data_collection=deny (ZDR) + require_parameters; NOMOS_PROVIDER_SORT opt-in; Exacto via model suffix. - Multi-turn: reload session history into context; UI passes session id. - Fix agent_activity logging (agent_id/session_id) and mcpClient data race. Events (live control-room feed): - approval.created (mcp), approval.decided (api), execution.completed/failed (approved-action path), signal.raised/resolved + health.changed (scheduler, transition-gated). Fixes: - createApproval FK violation (reuse execution entity) — the agent's only write path; log the previously-swallowed errors. Web UI: - Embed web/dist via //go:embed (single binary); Dockerfile builds SPA into the Go stage; committed .gitkeep placeholder keeps backend-only builds green. - Caddy: Authentik-gated /agent/* -> nomos so the UI reaches the agent same-origin in production. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
92
web/src/lib/api.ts
Normal file
92
web/src/lib/api.ts
Normal file
@@ -0,0 +1,92 @@
|
||||
const BASE = '/agent'
|
||||
|
||||
export interface Session {
|
||||
id: string
|
||||
title: string
|
||||
actor: string
|
||||
created_at: string
|
||||
last_active_at: string
|
||||
}
|
||||
|
||||
export interface Message {
|
||||
id: string
|
||||
session_id: string
|
||||
role: string
|
||||
content: any
|
||||
created_at: string
|
||||
}
|
||||
|
||||
export async function fetchSessions(): Promise<Session[]> {
|
||||
const res = await fetch(`${BASE}/sessions`)
|
||||
if (!res.ok) return []
|
||||
const data = await res.json()
|
||||
return data.sessions ?? []
|
||||
}
|
||||
|
||||
export async function fetchMessages(sessionId: string): Promise<Message[]> {
|
||||
const res = await fetch(`${BASE}/sessions/${sessionId}`)
|
||||
if (!res.ok) return []
|
||||
const data = await res.json()
|
||||
return data.messages ?? []
|
||||
}
|
||||
|
||||
export interface ChatEvent {
|
||||
type: string
|
||||
data: any
|
||||
session_id?: string
|
||||
iteration?: number
|
||||
}
|
||||
|
||||
export function streamChat(
|
||||
message: string,
|
||||
sessionId: string | null,
|
||||
onEvent: (ev: ChatEvent) => void,
|
||||
onError: (err: string) => void,
|
||||
onDone: () => void
|
||||
): AbortController {
|
||||
const controller = new AbortController()
|
||||
|
||||
fetch(`${BASE}/chat`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ message, session_id: sessionId ?? undefined }),
|
||||
signal: controller.signal
|
||||
}).then(async (res) => {
|
||||
if (!res.ok) {
|
||||
onError(`HTTP ${res.status}`)
|
||||
return
|
||||
}
|
||||
const reader = res.body?.getReader()
|
||||
if (!reader) {
|
||||
onError('no response body')
|
||||
return
|
||||
}
|
||||
const decoder = new TextDecoder()
|
||||
let buffer = ''
|
||||
|
||||
while (true) {
|
||||
const { done, value } = await reader.read()
|
||||
if (done) break
|
||||
buffer += decoder.decode(value, { stream: true })
|
||||
const lines = buffer.split('\n')
|
||||
buffer = lines.pop() ?? ''
|
||||
|
||||
for (const line of lines) {
|
||||
if (line.startsWith('data: ')) {
|
||||
try {
|
||||
const ev: ChatEvent = JSON.parse(line.slice(6))
|
||||
onEvent(ev)
|
||||
} catch {
|
||||
// skip malformed
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}).catch((err) => {
|
||||
onError(err.message)
|
||||
}).finally(() => {
|
||||
onDone()
|
||||
})
|
||||
|
||||
return controller
|
||||
}
|
||||
Reference in New Issue
Block a user