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