Skip to main content

Quad API

Table of Contents

Top

upsquad/quad/v1/quad.proto

GetSuggestionsRequest

GetSuggestionsRequest asks for a page of chips.

Tenant + user identity flow from gateway-injected JWT headers, not from the request body — same convention as ListThreads / PinThread / SendMessage.

FieldTypeLabelDescription
thread_idstringthread_id is the Quad thread the chips are scoped to. May be empty — the V1 provider ignores it; the V2 LLM provider will use it to load recent thread events for personalisation. The empty value is a valid request (renders chips for the new- thread empty state).
max_suggestionsint32max_suggestions caps the chip count. Server clamps to [1, 8]; 0 (or unset) defaults to 4. The cap matches the upper bound on the design's chip strip — anything larger overflows the viewport on mobile.

GetSuggestionsResponse

GetSuggestionsResponse carries the chips in display order. An empty suggestions slice is treated by the handler as a provider error (CodeInternal) — "no chips" is a UI failure mode worth flagging loudly rather than silently rendering an empty strip — so callers can rely on len(suggestions) >= 1 on success.

FieldTypeLabelDescription
suggestionsSuggestionrepeatedsuggestions is the chip list in display order. Length in [1, max_suggestions]. The provider chooses ordering; clients MUST NOT re-sort.

ListThreadsRequest

ListThreadsRequest is the page-keyed list request. Tenant + user are sourced from JWT context; only paging knobs travel on the body.

FieldTypeLabelDescription
page_sizeint32page_size caps the number of threads returned. Server clamps to [1, 100]; 0 (or unset) defaults to 20. The cap matches the upper bound on the redesigned client portal sidebar — anything larger is virtualised on the client and isn't worth the server-side round-trip.
page_tokenstringpage_token is the opaque continuation token returned by the previous ListThreads call. Empty/unset starts at the first page. The server treats this as a black box; clients MUST NOT inspect or modify it. Token format may change without a proto bump.

ListThreadsResponse

ListThreadsResponse carries one page of threads + the continuation token. An empty next_page_token signals last page.

FieldTypeLabelDescription
threadsThreadrepeatedthreads is the page contents in sort order (pinned DESC, last_message_at DESC NULLS LAST, id DESC).
next_page_tokenstringnext_page_token is the opaque token to pass on the next call. Empty when no more pages exist.

PinThreadRequest

PinThreadRequest sets or clears the pinned bit on a thread.

FieldTypeLabelDescription
thread_idstringthread_id is the UUID of the thread to pin/unpin. Required.
pinnedboolpinned is the desired pinned state. The server is idempotent on both transitions — re-pinning an already-pinned thread keeps the existing pinned_at timestamp; un-pinning an already-unpinned thread is a no-op.

PinThreadResponse

PinThreadResponse returns the post-mutation thread row so the client can reconcile its in-memory state without a re-list.

FieldTypeLabelDescription
threadThreadthread is the post-mutation thread row.

SendMessageRequest

SendMessageRequest carries the user message for a chat thread. tenant_id / user_id are NOT carried on the request — they come from gateway-injected JWT headers, same convention as every other portal- facing Connect service in this repo (LifecycleService, ApprovalService).

FieldTypeLabelDescription
thread_idstringthread_id is the Quad thread UUID. For now this maps 1:1 onto the underlying agent session_id. GAP-10 introduces a thread→session mapping table; until then callers SHOULD pass an existing session UUID here, and an empty thread_id creates a new session (downstream LifecycleService semantics).
textstringtext is the user's input message. UTF-8, bounded at 32 KiB (enforced downstream by LifecycleService).
target_agent_idstringtarget_agent_id names the agent the message is directed at — the resolved target of a client-side @mention (M1, tracker #1536).

The redesigned /quad page resolves an @mention to a concrete agent_configurations UUID client-side (deterministic, NOT LLM free-text parsing) and carries it here. The Quad proxy (internal/quad/connect_adapter.go) consumes this to select the ensure-session-for-(thread, agent) target and to drive the deterministic delegate_to_agent invocation — wired by T3/T4, not by this field's addition.

OPTIONAL and additive: an empty target_agent_id preserves the pre-M1 behaviour (message goes to the thread's default session / the user's own Quad orchestrator). Tenant + user identity still flow from gateway-injected JWT headers — never from this field. |

StreamTaskStatusRequest

StreamTaskStatusRequest names the Task to stream. Tenant + caller identity flow from JWT context; the caller may only stream a Task they requested.

FieldTypeLabelDescription
task_idstringtask_id is the canonical Task UUID returned by SubmitTask (required).

SubmitTaskRequest

SubmitTaskRequest turns a Quad conversation into a Task. Tenant + caller identity flow from gateway-injected JWT context — never from this body — so there is deliberately no requester field: the Task is always created as the caller (see the SubmitTask RPC comment).

FieldTypeLabelDescription
thread_idstringthread_id is the Quad conversation the ask originated from. It is the payload_ref of the created Task and is mixed into the idempotency key so two submissions of the same intent from the same thread collapse. May be empty for an ad-hoc submission with no thread context.
titlestringtitle is the human-readable Task label (required). Typically the user's request summarised to <=120 chars by the client.
intentstringintent is an optional machine-readable category/verb for the request (e.g. "draft_email", "triage_incident"). It is recorded on the Task and mixed into the idempotency key.
intent_tagsstringrepeatedintent_tags are explicit deterministic routing hints for the MVP. They are forwarded to the Coordinator's TeamRouter as labels under two conventions: a "key:value" tag becomes the label {key: value} (so "team:<id>" / "domain:engineering" target the router directly), and bare tags — plus the intent, when set — are deduplicated, sorted, and comma-joined under the single "intent_tags" label for the capability matcher. They are also mixed into the idempotency key, order-insensitively (the key derivation sorts them so tag reordering does not defeat dedup). An LLM-assisted planner that infers these is a post-MVP follow-up.

SubmitTaskResponse

SubmitTaskResponse returns the created Task's id + initial status. The client uses task_id to open StreamTaskStatus and to deep-link the Task card (F8).

FieldTypeLabelDescription
task_idstringtask_id is the canonical Task UUID created on behalf of the caller.
statusupsquad.task.v1.TaskStatusstatus is the Task's status at creation time (always TASK_STATUS_PENDING for a fresh submission; the prior status for a deduplicated re-submission).
deduplicatedbooldeduplicated is true when this submission matched a prior submission's idempotency key and resolved to the existing Task instead of creating a new one (a double-tap). The client can use this to avoid showing a duplicate "task created" toast.

Suggestion

Suggestion is one chip in the GetSuggestions response.

V1 mirrors the chips in the design mock at docs/design/redesign-2026-04/project/quad.jsx SUGGESTIONS array (4 chips: failing_now, most_approvals_week, over_budget_agents, walk_through_approval). V2 (LLM provider) will personalise the label/icon/prompt_text per user + per thread without changing the wire shape.

FieldTypeLabelDescription
idstringid is the opaque identifier surfaced to the client for telemetry and dedup. Stable for the life of one response; not stable across calls (V2 will mint per-response ids).
labelstringlabel is the chip text shown in the UI (≤80 chars).
iconstringicon is a single-glyph hint (e.g. "⚠", "✓", "$", "→"). Empty when the provider has no icon preference.
prompt_textstringprompt_text is what the client should insert into the chat input on click. For V1 this equals label; the V2 LLM provider may diverge (e.g. label="Show failing agents" with prompt_text= "Show me a table of agents that have failed in the last 24h").

TaskStatusEvent

TaskStatusEvent is one Task status transition pushed to the caller. The first event on a stream reports the current status; subsequent events are emitted only when the status changes.

FieldTypeLabelDescription
task_idstringtask_id is the Task this event is about.
statusupsquad.task.v1.TaskStatusstatus is the Task's lifecycle state as of this event.
routed_team_idstringrouted_team_id is the team the Task was routed to, once known. Empty until the Coordinator routes the Task.
failure_reasonstringfailure_reason explains a FAILED or REJECTED terminal state. Empty in all non-failure states.
atgoogle.protobuf.Timestampat is when the Task last changed (the canonical Task's updated_at).

Thread

Thread is the projection returned by ListThreads + PinThread. Mirrors the quad_threads table (migration 087) minus internal columns (org_id, user_id — both implicit from the auth context).

session_id is exposed so the client can correlate a thread with the underlying agent session if it ever wants to (e.g. for jumping into the workflow detail view). It is NOT load-bearing for the chat surface — a follow-up that decouples threads from sessions can drop or repurpose this field without breaking SendMessage.

FieldTypeLabelDescription
idstringid is the thread UUID. Pass this as SendMessageRequest.thread_id and PinThreadRequest.thread_id.
session_idstringsession_id is the underlying agent session UUID this thread wraps. Stable for the life of the thread.
titlestringtitle is a human-readable thread label — typically the first user message excerpt (≤120 chars). May be empty for freshly-minted threads with no traffic yet.
pinnedboolpinned is true when the user has pinned this thread.
pinned_atgoogle.protobuf.Timestamppinned_at is when the pin was last set; unset when pinned=false.
last_message_atgoogle.protobuf.Timestamplast_message_at is the timestamp of the most recent message on this thread. Unset when the thread has no message traffic yet.
created_atgoogle.protobuf.Timestampcreated_at is when the thread row was minted.

QuadService

QuadService exposes the chat surface for the portal /quad page. Mounted on Connect-Web in cmd/context-engine/main.go next to LifecycleService. The implementation is a thin gRPC-client proxy into the authoritative LifecycleService running in agent-orchestrator.

Method NameRequest TypeResponse TypeDescription
SendMessageSendMessageRequest.upsquad.runtime.v1.AgentEvent streamSendMessage posts a user message to a chat thread and returns a stream of events (token, status, completion, error). Today this proxies into LifecycleService.SendMessage; later (GAP-12) the streamed event will become a Quad-native MessageEvent with a structured-answer variant.

The skeleton mapping in the current proxy adapter is:

SendMessageRequest.thread_id -> runtime.v1.SendMessageRequest.session_id SendMessageRequest.text -> runtime.v1.SendMessageRequest.message

Empty thread_id requests a new session (mirrors LifecycleService semantics). All identity (tenant_id, user_id, clearance) flows from gateway-injected JWT headers — same convention as ApprovalService and LifecycleService on the ICS Connect mux. | | ListThreads | ListThreadsRequest | ListThreadsResponse | ListThreads returns the calling user's chat threads under the resolved tenant, newest-active first with pinned threads pinned to the top. Tenant + user identity are sourced from gateway-injected JWT context — never from the request body.

Sort order (mirrors idx_quad_threads_list in migration 087):

ORDER BY pinned DESC, last_message_at DESC NULLS LAST, id DESC

pinned threads first (so the user's bookmarks stay at the top of the sidebar), then most-recently-active, with id as the keyset tiebreaker so pagination is stable when last_message_at ties.

Pagination is opaque keyset (NOT offset). Callers should treat next_page_token as a black box and pass it back unchanged. An empty next_page_token in the response means the last page has been served. | | PinThread | PinThreadRequest | PinThreadResponse | PinThread sets or clears the pinned bit on a thread. Idempotent — re-pinning an already-pinned thread is a no-op (the pinned_at timestamp is preserved on idempotent re-pin so the UI's "pinned 2d ago" caption stays stable). Tenant + user identity are resolved from JWT context; the thread MUST belong to the calling user — RLS enforces tenant isolation, the service layer additionally enforces caller-is-owner. | | GetSuggestions | GetSuggestionsRequest | GetSuggestionsResponse | GetSuggestions returns context-aware chips for the Quad chat input. The redesigned client portal /quad page shows these chips as one-tap prompts beneath the empty thread state and (forthcoming) inline within active threads.

V1 (this RPC, today): a deterministic StaticSuggestionProvider returns chips drawn from a small curated catalog mirroring the design mock. The same four chips are returned on every call. userID and threadID flow through but are ignored.

V2 (follow-up): an LLMSuggestionProvider personalises chips per user + per thread by reading recent thread events. The wire shape is unchanged — only the provider implementation swaps. Tests substitute a deterministic fake; the handler nil-checks the provider so older tests don't have to wire one (returns CodeUnimplemented when absent).

Tenant + user identity flow from gateway-injected JWT headers (x-tenant-id, x-user-id) — never from the request body. Same convention as ListThreads and PinThread. | | SubmitTask | SubmitTaskRequest | SubmitTaskResponse | SubmitTask turns a Quad conversation into a routed, executing Task (MVP work-routing spine, ADR-0015, issue #1230). It is the seam where Quad stops being a chat-only proxy: a user's ask becomes a Task.

Two side effects, in order:

  1. Creates the Task via the canonical TaskService path (upsquad.task.v1.TaskService.CreateTask) ON BEHALF OF the caller — requester_member_id is forced to the caller's member identity (from the gateway-injected JWT context, NEVER the request body). The canonical handler's self-scoping rule applies unchanged: a member-scoped caller may only create their OWN task, so a clearance / permission mismatch surfaces as PERMISSION_DENIED from the Task handler — Quad does not re-implement that check, it relies on it.

  2. Publishes a CONTROL(submit_task) envelope onto the tenant Coordinator queue (internal/bus). The envelope's task_id names the canonical Task created in step 1, and the Coordinator ADOPTS it (adopt-don't-create, ADR-0015): it attaches only its internal execution context and drives every status transition through the canonical Transitioner on that SAME row, so StreamTaskStatus observes the routed lifecycle. The Coordinator (B8) does the actual team routing + delegation; Quad stays thin and does NOT route. The envelope carries a deterministic idempotency_key derived from (org, thread_id, caller, intent-hash) so a double-tap from the same conversation does not double-create or double-route.

intent_tags are explicit deterministic routing hints for the MVP — the client sends them and they are forwarded to the Coordinator's TeamRouter as labels. An LLM-assisted planner that infers tags from the conversation is a post-MVP follow-up; this RPC is the stable seam for it.

Returns the canonical task_id (correlate StreamTaskStatus to it) and the initial status (PENDING). | | StreamTaskStatus | StreamTaskStatusRequest | TaskStatusEvent stream | StreamTaskStatus server-streams the lifecycle transitions of a Task to the caller until the Task reaches a terminal state (COMPLETED / FAILED / REJECTED / CANCELLED) or the caller disconnects.

The caller may only stream a Task they requested — the canonical TaskService self-scoping rule applies, so another member's task id reads as NOT_FOUND (its existence is itself not visible to a self-scoped caller).

The first event always reports the current status (so a late subscriber still learns where the Task is); subsequent events are emitted only on a status change. The stream mechanism is a bounded poll of the canonical Task store (the simplest correct mechanism available today) — a push-based status-event subscription is a post-MVP optimisation behind the same wire contract. |

Scalar Value Types

.proto TypeNotesC++JavaPythonGoC#PHPRuby
doubledoubledoublefloatfloat64doublefloatFloat
floatfloatfloatfloatfloat32floatfloatFloat
int32Uses variable-length encoding. Inefficient for encoding negative numbers – if your field is likely to have negative values, use sint32 instead.int32intintint32intintegerBignum or Fixnum (as required)
int64Uses variable-length encoding. Inefficient for encoding negative numbers – if your field is likely to have negative values, use sint64 instead.int64longint/longint64longinteger/stringBignum
uint32Uses variable-length encoding.uint32intint/longuint32uintintegerBignum or Fixnum (as required)
uint64Uses variable-length encoding.uint64longint/longuint64ulonginteger/stringBignum or Fixnum (as required)
sint32Uses variable-length encoding. Signed int value. These more efficiently encode negative numbers than regular int32s.int32intintint32intintegerBignum or Fixnum (as required)
sint64Uses variable-length encoding. Signed int value. These more efficiently encode negative numbers than regular int64s.int64longint/longint64longinteger/stringBignum
fixed32Always four bytes. More efficient than uint32 if values are often greater than 2^28.uint32intintuint32uintintegerBignum or Fixnum (as required)
fixed64Always eight bytes. More efficient than uint64 if values are often greater than 2^56.uint64longint/longuint64ulonginteger/stringBignum
sfixed32Always four bytes.int32intintint32intintegerBignum or Fixnum (as required)
sfixed64Always eight bytes.int64longint/longint64longinteger/stringBignum
boolboolbooleanbooleanboolboolbooleanTrueClass/FalseClass
stringA string must always contain UTF-8 encoded or 7-bit ASCII text.stringStringstr/unicodestringstringstringString (UTF-8)
bytesMay contain any arbitrary sequence of bytes.stringByteStringstr[]byteByteStringstringString (ASCII-8BIT)