l-ai-chat-proto/chat.proto
Kaien 32d924f965 chore(module): publish chat proto Go module
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-09 22:41:07 +03:00

235 lines
9.1 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);
}
// 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;
// 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)
}