feat(api): agent loop + conversations/messages schema (non-streaming) #338
Labels
No labels
area:auth
area:ci
area:db
area:infra
area:native
area:pwa
area:service
epic
feature
foundation
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
james/carol#338
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
The server-side agent loop that ties together the tool registry (#51), the provider adapters (#337), and the propose-then-confirm commit path (#51) — plus the normalized
conversations/messagespersistence (ADR-0029 §4). Part of epic #47.Non-streaming for this ticket (a JSON request/response turn). SSE streaming needs an adapter
stream()method that #337 deferred, so streaming + the SSE chat endpoint are the next ticket; the chat UI is the one after. Scoping the loop here keeps the hard state-machine + write-pause/resume correct first.Scope
025_conversations_messages(ADR-0029 §4, both engines):conversations— id, user_id FK (cascade), title (nullable), provider, model, status (active|awaiting_confirmation), created_at, updated_at.messages— id, conversation_id FK (cascade), user_id FK, role (user|assistant|tool), seq (per-conversation order), content (text, nullable), tool_calls (JSON text, nullable), tool_call_id (nullable), created_at.conversations: create/get/list/setStatus;messages: append/listByConversation ordered by seq).apps/api/lib/agent/loop.ts) —runTurn(ctx, conversationId, userText):LlmGenerateRequest(system prompt + the conversation's normalized history +llmToolDefsFromRegistry()from #51) and call the user'sLlmClient(#337getLlmClientForUser, injectable for tests).tool_calls: read tools → execute via the #51 registry, append atoolmessage with the result, loop again. Write tools → persist aProposedChange/pending_action(the #51 path), append an assistant message carrying the proposal, set the conversationawaiting_confirmation, and stop the turn (the user confirms via the existing #51 commit/reject endpoints).end_turn) → append the assistant text message → done. Enforce a max-tool-rounds guard.POST /api/conversations/{id}/resume: after the user commits/rejects the pending proposal (existing #51POST /api/agent/proposals/{id}/{commit,reject}), feed the correspondingtoolresult message back and continue the loop. Leaves #51's commit endpoint unchanged.POST /api/conversations(create),POST /api/conversations/{id}/messages(send a user message → run the turn → return the new messages + conversation status),GET /api/conversations(list, cursor),GET /api/conversations/{id}(with messages),POST /api/conversations/{id}/resume. Session or PAT; per-user scoped (cross-user conversation id → 404).awaiting_confirmation+ a pending proposal, DB unmutated) then resumes after commit; max-rounds guard; cross-user 404; history replay. Inject a fakeLlmClient— no real provider calls. OpenAPI for the new routes; api-client regen.Acceptance criteria
Out of scope
stream()method + the SSE chat endpoint (next ticket). The chat UI (next-next). Voice. A shared instance LLM key.Depends on #51 (registry + commit), #337 (adapters), #334 (config). Implements ADR-0029 §2/§4/§6.