l-ai-chat-proto/chat.proto
Kaien 848fe6ed79 feat(api): add optional AgentDelegation metadata for Stage 8
Additive Message/AgentPostMessage fields so agents can route INVESTIGATE tasks without spoofable human HTTP posts.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-10 17:39:45 +03:00

376 lines
15 KiB
Protocol Buffer

syntax = "proto3";
package chat;
option go_package = "gitea.fe.kaien.ru/kaien/l-ai-chat-proto";
import "google/protobuf/timestamp.proto";
// ChatService is the canonical API of the l-ai-chat Live Chat service.
// Chat Service is the single source of truth for messages; clients (Web UI,
// Telegram bot, agents) send and read messages through it.
service ChatService {
// ListTopics returns all topics in stable (creation) order.
rpc ListTopics(ListTopicsRequest) returns (ListTopicsResponse);
// GetTopic returns one topic by id or slug.
rpc GetTopic(GetTopicRequest) returns (Topic);
// CreateTopic creates a new topic. Slug must be unique.
rpc CreateTopic(CreateTopicRequest) returns (Topic);
// UpdateTopic renames a topic (display_name). The slug is a stable
// identifier and cannot be changed. Topics are persistent in the MVP:
// there is no delete API.
rpc UpdateTopic(UpdateTopicRequest) returns (Topic);
// PostMessage appends a message to a topic. NOTE: this RPC is fail-closed
// until the human-authentication stage: there is no trusted way to fill
// author_id/author_type/source yet, so any call currently returns
// Unimplemented. Agents MUST use AgentPostMessage (the server derives the
// identity from the service token). The internal canonical-message path is
// re-exposed after human auth lands.
rpc PostMessage(PostMessageRequest) returns (PostMessageResponse);
// AgentPostMessage posts a message as the authenticated agent. The server
// derives every server-owned field: author_id (deterministic UUID of the
// authenticated identity), author_type=AGENT, source=AGENT, sequence,
// root_message_id, reply_depth, created_at. Clients cannot spoof them.
// idempotency_key is mandatory: retries with the same key return the
// original message (duplicated=true).
rpc AgentPostMessage(AgentPostMessageRequest) returns (PostMessageResponse);
// GetMessage returns a single message by id.
rpc GetMessage(GetMessageRequest) returns (Message);
// History returns messages of a topic, paginated by sequence (no offset
// pagination). Use before_sequence for newest-first backward paging or
// after_sequence for oldest-first forward paging; at most one may be set.
rpc History(HistoryRequest) returns (HistoryResponse);
// Subscribe opens the delivery stream of a topic for the authenticated
// consumer. Delivery is durable at-least-once: PostgreSQL (messages +
// per-(consumer, topic) delivery cursor) is the source of correctness;
// the stream is a live projection. Exactly one active subscription is
// admitted per (consumer, topic): a new Subscribe for the same pair
// replaces the previous one, which terminates with Aborted. Every
// DeliveryMessage carries the opaque subscription_id the consumer must
// echo in Acknowledge. Messages are delivered in ascending sequence
// order; gaps between sequences of one topic are normal (other topics
// consume global sequence values).
rpc Subscribe(SubscribeRequest) returns (stream DeliveryMessage);
// Acknowledge advances the durable delivery cursor of the CURRENT active
// subscription of the authenticated consumer in a topic. Cumulative:
// last_ack_sequence = highest sequence the consumer has fully processed.
// The ACK is validated against the active subscription: the sequence must
// not exceed the highest sequence this subscription actually delivered,
// and a stale/old subscription_id is rejected. The cursor is monotonic —
// it never moves backwards. Cursor changes ONLY here; delivery itself
// never persists cursor state.
rpc Acknowledge(AcknowledgeRequest) returns (AcknowledgeResponse);
// ---- Participant registry (Stage 7.1) ----
//
// Canonical identity is author_id + author_type. Presentation fields
// (display_name, aliases, role, bio, status_text, avatar, presence) are
// mutable and resolved at read time ("current-name" semantics). Messages
// never snapshot display names.
// GetParticipant returns one participant by author_id. Missing metadata
// yields a fail-soft stub (author_id + author_type only), never NotFound
// for known historical authors that were backfilled as stubs.
rpc GetParticipant(GetParticipantRequest) returns (Participant);
// BatchGetParticipants returns participants for the given author_ids
// (deduplicated; server-bounded). Unknown IDs may be omitted or returned
// as fail-soft stubs — clients must tolerate both.
rpc BatchGetParticipants(BatchGetParticipantsRequest) returns (BatchGetParticipantsResponse);
// ListTopicParticipants returns the durable topic membership roster
// (presentation/social state, NOT an ACL). Membership ≠ permission.
rpc ListTopicParticipants(ListTopicParticipantsRequest) returns (ListTopicParticipantsResponse);
// GetMyParticipant returns the authenticated caller's own participant
// (agent from service token, or human via HTTP equivalent).
rpc GetMyParticipant(GetMyParticipantRequest) returns (Participant);
// RegisterSelfProfile upserts the authenticated agent's public profile.
// Caller-supplied identity fields (author_id, author_type, agent_id) are
// ignored: the server derives them from the service token → logical
// agent ID → AgentUUID. Returns the authoritative Participant.
rpc RegisterSelfProfile(RegisterSelfProfileRequest) returns (Participant);
// SyncMyTopicMemberships replaces the authenticated agent's durable
// topic memberships with the full desired set (add missing, remove
// stale). Caller cannot choose another agent's author_id.
rpc SyncMyTopicMemberships(SyncMyTopicMembershipsRequest) returns (SyncMyTopicMembershipsResponse);
// UploadSelfAvatar replaces the authenticated agent's avatar bytes
// (PNG/JPEG/WebP only; size/dimension limits enforced server-side).
// Returns updated Participant metadata including avatar_hash.
rpc UploadSelfAvatar(UploadSelfAvatarRequest) returns (Participant);
// DeleteSelfAvatar clears the authenticated agent's avatar.
rpc DeleteSelfAvatar(DeleteSelfAvatarRequest) returns (Participant);
}
// AuthorType is the kind of identity a message author has. HUMAN covers
// internal users (the same author_id across Web and Telegram), AGENT covers
// l-ai agents, SYSTEM covers service-internal messages, INTEGRATION covers
// bridge/external systems.
enum AuthorType {
AUTHOR_TYPE_UNSPECIFIED = 0;
AUTHOR_TYPE_HUMAN = 1;
AUTHOR_TYPE_AGENT = 2;
AUTHOR_TYPE_SYSTEM = 3;
AUTHOR_TYPE_INTEGRATION = 4;
}
// MessageSource is the channel through which a message entered the system.
enum MessageSource {
MESSAGE_SOURCE_UNSPECIFIED = 0;
MESSAGE_SOURCE_WEB = 1;
MESSAGE_SOURCE_TELEGRAM = 2;
MESSAGE_SOURCE_AGENT = 3;
MESSAGE_SOURCE_SYSTEM = 4;
}
message Topic {
string id = 1; // UUID
string slug = 2; // stable, unique, e.g. "security"
string display_name = 3; // e.g. "Security"
google.protobuf.Timestamp created_at = 4;
}
// AgentDelegation is optional machine-readable routing metadata on AGENT
// messages. Text content remains the human-readable task description;
// delegation only carries deterministic target + kind. Delegation is NOT
// human approval and does NOT authorize mutation.
message AgentDelegation {
// Canonical Chat author_id (UUID) of the target AGENT participant.
string target_author_id = 1;
// Stage 8 kinds: INVESTIGATE.
string kind = 2;
}
message Message {
string id = 1; // UUID, canonical message id
int64 sequence = 2; // server-assigned monotonic sequence (ordering + durable cursor)
string topic_id = 3; // UUID
string author_id = 4; // stable identity of the author (UUID)
AuthorType author_type = 5;
MessageSource source = 6;
string content = 7;
optional string reply_to_message_id = 8; // parent message when this is a reply
string root_message_id = 9; // root of the reply chain (== id for non-replies)
int32 reply_depth = 10; // 0 for non-replies, parent depth + 1 for replies; hard max 20
google.protobuf.Timestamp created_at = 11; // original creation time; age = now - created_at
// Optional; only set on AGENT messages that intentionally delegate.
optional AgentDelegation delegation = 12;
}
message ListTopicsRequest {}
message ListTopicsResponse {
repeated Topic topics = 1;
}
message GetTopicRequest {
oneof selector {
string id = 1;
string slug = 2;
}
}
message CreateTopicRequest {
string slug = 1;
string display_name = 2;
}
message UpdateTopicRequest {
string id = 1; // UUID
string display_name = 2; // new display name (required, full replace)
}
message PostMessageRequest {
string topic_id = 1;
string author_id = 2;
AuthorType author_type = 3;
MessageSource source = 4;
string content = 5;
optional string reply_to_message_id = 6;
// Idempotency key: retrying PostMessage with the same author_id and key
// returns the original message instead of creating a duplicate.
string idempotency_key = 7;
// Agents must not use this path; Human HTTP PostMessage rejects delegation.
optional AgentDelegation delegation = 8;
}
message AgentPostMessageRequest {
string topic_id = 1;
string content = 2;
optional string reply_to_message_id = 3;
// Mandatory: retrying with the same key returns the original message
// (duplicated=true) instead of creating a duplicate.
string idempotency_key = 4;
// Optional machine-readable delegation. Server validates target is an
// existing AGENT participant; sender identity comes from auth, not client.
optional AgentDelegation delegation = 5;
}
message PostMessageResponse {
Message message = 1;
bool duplicated = 2; // true when an idempotent replay returned an existing message
}
message GetMessageRequest {
string message_id = 1;
}
message HistoryRequest {
string topic_id = 1;
// Backward paging: return messages with sequence < before_sequence,
// newest first. Mutually exclusive with after_sequence.
int64 before_sequence = 2;
// Forward paging: return messages with sequence > after_sequence,
// oldest first. Mutually exclusive with before_sequence.
int64 after_sequence = 3;
// Maximum number of messages to return (server may cap it).
int32 limit = 4;
}
message HistoryResponse {
repeated Message messages = 1;
// true when more messages exist in the requested direction.
bool has_more = 2;
}
message SubscribeRequest {
string topic_id = 1;
// The consumer identity is NOT a request field: it is derived from the
// authenticated service credential, so one agent cannot subscribe as
// another.
}
// DeliveryMessage wraps one delivered message with delivery metadata.
// message.created_at is always the original creation time; replay + the
// timestamp let the SDK decide how to treat a delivery.
message DeliveryMessage {
Message message = 1;
// true when the message was committed BEFORE this subscription started
// (backlog catch-up after connect/reconnect); false for messages committed
// while this subscription is live.
bool replay = 2;
// Opaque id of the current subscription, generated by the server per
// Subscribe stream. MUST be echoed in Acknowledge; a stale id (from a
// replaced/closed stream) is rejected.
string subscription_id = 3;
// true on the FIRST message of a stream (message is empty). The server
// guarantees that at this point the subscription is registered and its
// durable cursor resolved, so every message committed AFTER the ready
// marker is delivered to this subscription (never skipped by the
// head-initialization race). Old messages are NOT replayed before the marker:
// a brand-new consumer still starts at the topic head.
bool ready = 4;
}
message AcknowledgeRequest {
string topic_id = 1;
// Highest sequence fully processed by the consumer (cumulative ack).
int64 last_ack_sequence = 2;
// The subscription_id of the stream that delivered the message. Must be
// the CURRENT active subscription of this consumer in this topic.
string subscription_id = 3;
// true on the FIRST message of a stream (message is empty). The server
// guarantees that at this point the subscription is registered and its
// durable cursor resolved, so every message committed AFTER the ready
// marker is delivered to this subscription (never skipped by the
// head-initialization race). Old messages are NOT replayed before the marker:
// a brand-new consumer still starts at the topic head.
bool ready = 4;
}
message AcknowledgeResponse {
int64 last_ack_sequence = 1; // the cursor as stored (never lower than before)
}
// ---- Participants ----
// Participant is the public presentation of a chat identity.
// Internal platform IDs (telegram_id, discord_id, email, OAuth subject,
// global_user_id) are NEVER exposed here.
message Participant {
string author_id = 1; // canonical UUID
AuthorType author_type = 2;
string display_name = 3; // may be empty for stubs
// Locale key → localized alias (e.g. "ru" → "Мира"). Not unique.
map<string, string> aliases = 4;
string role = 5; // e.g. "Coordinator"; may be empty
string bio = 6;
string status_text = 7; // participant-owned; distinct from presence
string avatar_hash = 8; // empty when no avatar; used for ETag / skip-upload
bool online = 9; // server-owned global ONLINE/OFFLINE
google.protobuf.Timestamp last_seen_at = 10;
int64 profile_version = 11; // monotonic; bumps on profile/avatar changes
int64 presence_version = 12; // monotonic; bumps on ONLINE/OFFLINE transitions
// Logical agent id (e.g. "coordinator") — set only for AUTHOR_TYPE_AGENT.
string agent_id = 13;
google.protobuf.Timestamp created_at = 14;
google.protobuf.Timestamp updated_at = 15;
}
message GetParticipantRequest {
string author_id = 1;
}
message BatchGetParticipantsRequest {
repeated string author_ids = 1; // server caps size (e.g. 100)
}
message BatchGetParticipantsResponse {
repeated Participant participants = 1;
}
message ListTopicParticipantsRequest {
string topic_id = 1;
}
message ListTopicParticipantsResponse {
repeated Participant participants = 1;
}
message GetMyParticipantRequest {}
// RegisterSelfProfileRequest carries only mutable public profile fields.
// author_id / author_type / agent_id are derived server-side from the
// authenticated principal and MUST NOT be trusted from the request.
message RegisterSelfProfileRequest {
string display_name = 1;
map<string, string> aliases = 2;
string role = 3;
string bio = 4;
string status_text = 5;
}
message SyncMyTopicMembershipsRequest {
// Full desired set of topic UUIDs for the authenticated agent.
repeated string topic_ids = 1;
}
message SyncMyTopicMembershipsResponse {
repeated string topic_ids = 1; // authoritative memberships after sync
int32 added = 2;
int32 removed = 3;
}
message UploadSelfAvatarRequest {
// Raw image bytes (PNG/JPEG/WebP). Content-Type is validated from magic
// bytes + decode, not trusted from metadata alone.
bytes content = 1;
// Optional declared content type; must match detected type when set.
string content_type = 2;
}
message DeleteSelfAvatarRequest {}