Scope
Extend cmd/proxyd to support the /conversations endpoints and to link response turns to conversations when a request carries conversation_id instead of previous_response_id.
Key design decisions
Mutual exclusion: conversation_id vs. previous_response_id
A POST /responses request may carry either conversation_id or previous_response_id, but not both. The proxy validates this at request parse time and returns 400 Bad Request with an error code invalid_request if both are present.
Conversation ID is a stable identifier
Unlike previous_response_id (which changes every turn), conversation_id is stable for the lifetime of the conversation. Each response turn that belongs to a conversation appends its input/output messages to the conversation via PUT /conversations/{id}/messages after inference.
Prior context resolution
When conversation_id is present:
- Proxy calls
GET /conversations/{id}/messages to retrieve the full message history (flat context).
- Proxy appends the new
input[] to the flat context and calls the inference backend.
- After inference, proxy calls
PUT /conversations/{id}/messages to append the new input + output to the conversation.
- If
store: true, proxy also calls POST /responses on Charon (responsed) with no previous_response_id — the response turn is stored as an independent node (not chained). The returned response_id is included in the POST /responses response to the client.
When previous_response_id is present: existing behaviour (unchanged).
/conversations passthrough
For POST /conversations, GET /conversations/{id}, DELETE /conversations/{id}, PUT /conversations/{id}/messages, and GET /conversations/{id}/messages: the proxy forwards these directly to conversationd with no transformation. The proxy acts as a transparent router for these routes.
Changes to cmd/proxyd
New dependency
cmd/proxyd (and cmd/conversationd) get a pkg/conversation client package (mirrors pkg/charon) with:
type Backend interface {
Create(ctx context.Context, meta ConversationMeta) (ConversationMeta, error)
Get(ctx context.Context, id string) (ConversationMeta, error)
Delete(ctx context.Context, id string) error
AppendMessages(ctx context.Context, id string, messages []Message) (uint64, error)
ListMessages(ctx context.Context, id string, afterSeq uint64, limit int) ([]Message, error)
}
Handler changes (cmd/proxy/handler.go)
CreateRequest gains ConversationID *string json:"conversation_id,omitempty"``
HandleCreate dispatches on ConversationID != nil vs. PreviousResponseID != nil
- Add
HandleConversationCreate, HandleConversationGet, HandleConversationDelete, HandleConversationAppendMessages, HandleConversationListMessages — all forwarding to the conversation.Backend
- Register routes:
POST /conversations, GET /conversations/{id}, DELETE /conversations/{id}, PUT /conversations/{id}/messages, GET /conversations/{id}/messages
Config
ProxyOptions gains ConversationdURL string (see #78). Default derived from conversationd default listen address.
Acceptance criteria
Blocked by: #81, #78
Scope
Extend
cmd/proxydto support the/conversationsendpoints and to link response turns to conversations when a request carriesconversation_idinstead ofprevious_response_id.Key design decisions
Mutual exclusion: conversation_id vs. previous_response_id
A
POST /responsesrequest may carry eitherconversation_idorprevious_response_id, but not both. The proxy validates this at request parse time and returns400 Bad Requestwith an error codeinvalid_requestif both are present.Conversation ID is a stable identifier
Unlike
previous_response_id(which changes every turn),conversation_idis stable for the lifetime of the conversation. Each response turn that belongs to a conversation appends its input/output messages to the conversation viaPUT /conversations/{id}/messagesafter inference.Prior context resolution
When
conversation_idis present:GET /conversations/{id}/messagesto retrieve the full message history (flat context).input[]to the flat context and calls the inference backend.PUT /conversations/{id}/messagesto append the new input + output to the conversation.store: true, proxy also callsPOST /responseson Charon (responsed) with noprevious_response_id— the response turn is stored as an independent node (not chained). The returnedresponse_idis included in thePOST /responsesresponse to the client.When
previous_response_idis present: existing behaviour (unchanged)./conversations passthrough
For
POST /conversations,GET /conversations/{id},DELETE /conversations/{id},PUT /conversations/{id}/messages, andGET /conversations/{id}/messages: the proxy forwards these directly toconversationdwith no transformation. The proxy acts as a transparent router for these routes.Changes to cmd/proxyd
New dependency
cmd/proxyd(andcmd/conversationd) get apkg/conversationclient package (mirrorspkg/charon) with:Handler changes (cmd/proxy/handler.go)
CreateRequestgainsConversationID *stringjson:"conversation_id,omitempty"``HandleCreatedispatches onConversationID != nilvs.PreviousResponseID != nilHandleConversationCreate,HandleConversationGet,HandleConversationDelete,HandleConversationAppendMessages,HandleConversationListMessages— all forwarding to theconversation.BackendPOST /conversations,GET /conversations/{id},DELETE /conversations/{id},PUT /conversations/{id}/messages,GET /conversations/{id}/messagesConfig
ProxyOptionsgainsConversationdURL string(see #78). Default derived fromconversationddefault listen address.Acceptance criteria
POST /responseswith bothconversation_idandprevious_response_idreturns400POST /responseswithconversation_idresolves context from conversation store, not chainstoreconversation_id, new messages are appended to the conversationstore: true+conversation_id: response is also stored in responsed as a standalone nodestore: false+conversation_id: response is appended to conversation but not stored in responsed/conversationspassthrough routes reachcmd/conversationdmake presubmitpassesconversation_id/previous_response_iddispatch pathsBlocked by: #81, #78