syntax = "proto3"; package chat; option go_package = "git.be.kaien.ru/l-ai/l-ai-chat-proto/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); } // 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; } 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 } 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; } 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; } 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; } 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; } message AcknowledgeResponse { int64 last_ack_sequence = 1; // the cursor as stored (never lower than before) }