From 24a5190a248d6e1b7cac94bd92c5c9b2b830edf5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9o=20Monnom?= Date: Thu, 8 Oct 2026 08:42:16 -0700 Subject: [PATCH 1/8] agent thread: history and tasks messages, shared conversation schema --- .changeset/agent-thread-content.md | 6 + livekit/agent/livekit_agent_thread.pb.go | 561 +++++++++++++++++++-- livekit/agent/thread_schema.go | 27 + livekit/agent/thread_schema_v1.sql | 26 + protobufs/agent/livekit_agent_thread.proto | 66 ++- 5 files changed, 647 insertions(+), 39 deletions(-) create mode 100644 .changeset/agent-thread-content.md create mode 100644 livekit/agent/thread_schema.go create mode 100644 livekit/agent/thread_schema_v1.sql diff --git a/.changeset/agent-thread-content.md b/.changeset/agent-thread-content.md new file mode 100644 index 000000000..c92dca73c --- /dev/null +++ b/.changeset/agent-thread-content.md @@ -0,0 +1,6 @@ +--- +"@livekit/protocol": minor +"github.com/livekit/protocol": minor +--- + +AgentThread history and tasks messages; shared conversation schema (thread_schema_v1.sql) diff --git a/livekit/agent/livekit_agent_thread.pb.go b/livekit/agent/livekit_agent_thread.pb.go index 1117a4e70..de54bc95d 100644 --- a/livekit/agent/livekit_agent_thread.pb.go +++ b/livekit/agent/livekit_agent_thread.pb.go @@ -13,15 +13,17 @@ // limitations under the License. // AgentThread: the durable conversation between an agent and a person, across -// channels and over time. Identified by an "AT_" id; keys (such as a phone -// number) find it again, at most one thread per key in a project. Holds a scope, -// attributes, an idle TTL and the AgentDB database with its state. Served by -// cloud-protocol's AgentThreads. +// channels and over time. Identified by an "AT_" id. Holds a scope, attributes, +// an idle TTL and the AgentDB database created with it. An internal key (a +// LiveKit phone number) can find it again. Served by cloud-protocol's +// AgentThreads. The conversation inside follows thread_schema_v1.sql, shared +// by the agents framework, which writes it, and the platform, which reads and +// merges it. // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.36.12 -// protoc v7.35.1 +// protoc v7.36.2 // source: agent/livekit_agent_thread.proto package agent @@ -31,6 +33,7 @@ import ( protoreflect "google.golang.org/protobuf/reflect/protoreflect" protoimpl "google.golang.org/protobuf/runtime/protoimpl" durationpb "google.golang.org/protobuf/types/known/durationpb" + structpb "google.golang.org/protobuf/types/known/structpb" timestamppb "google.golang.org/protobuf/types/known/timestamppb" reflect "reflect" sync "sync" @@ -544,8 +547,8 @@ func (*AgentThread_DeleteResponse) Descriptor() ([]byte, []int) { return file_agent_livekit_agent_thread_proto_rawDescGZIP(), []int{0, 7} } -// Marks thread_id as merged into another thread, after its items were copied -// there. +// Copies every agent's history and tasks of thread_id into another thread, +// as agents named "@", then marks thread_id merged. type AgentThread_MergeThreadRequest struct { state protoimpl.MessageState `protogen:"open.v1"` ThreadId string `protobuf:"bytes,1,opt,name=thread_id,json=threadId,proto3" json:"thread_id,omitempty"` @@ -598,6 +601,443 @@ func (x *AgentThread_MergeThreadRequest) GetInto() string { return "" } +// The conversation, oldest first. +type AgentThread_HistoryRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + ThreadId string `protobuf:"bytes,1,opt,name=thread_id,json=threadId,proto3" json:"thread_id,omitempty"` + // One agent's history; empty for every agent's. + Agent string `protobuf:"bytes,2,opt,name=agent,proto3" json:"agent,omitempty"` + // Cursor from the previous HistoryResponse; empty for the first page. + After string `protobuf:"bytes,3,opt,name=after,proto3" json:"after,omitempty"` + Limit int32 `protobuf:"varint,4,opt,name=limit,proto3" json:"limit,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *AgentThread_HistoryRequest) Reset() { + *x = AgentThread_HistoryRequest{} + mi := &file_agent_livekit_agent_thread_proto_msgTypes[10] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *AgentThread_HistoryRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AgentThread_HistoryRequest) ProtoMessage() {} + +func (x *AgentThread_HistoryRequest) ProtoReflect() protoreflect.Message { + mi := &file_agent_livekit_agent_thread_proto_msgTypes[10] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use AgentThread_HistoryRequest.ProtoReflect.Descriptor instead. +func (*AgentThread_HistoryRequest) Descriptor() ([]byte, []int) { + return file_agent_livekit_agent_thread_proto_rawDescGZIP(), []int{0, 9} +} + +func (x *AgentThread_HistoryRequest) GetThreadId() string { + if x != nil { + return x.ThreadId + } + return "" +} + +func (x *AgentThread_HistoryRequest) GetAgent() string { + if x != nil { + return x.Agent + } + return "" +} + +func (x *AgentThread_HistoryRequest) GetAfter() string { + if x != nil { + return x.After + } + return "" +} + +func (x *AgentThread_HistoryRequest) GetLimit() int32 { + if x != nil { + return x.Limit + } + return 0 +} + +type AgentThread_HistoryResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Items []*AgentThread_Item `protobuf:"bytes,1,rep,name=items,proto3" json:"items,omitempty"` + // Empty at the end. + After string `protobuf:"bytes,2,opt,name=after,proto3" json:"after,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *AgentThread_HistoryResponse) Reset() { + *x = AgentThread_HistoryResponse{} + mi := &file_agent_livekit_agent_thread_proto_msgTypes[11] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *AgentThread_HistoryResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AgentThread_HistoryResponse) ProtoMessage() {} + +func (x *AgentThread_HistoryResponse) ProtoReflect() protoreflect.Message { + mi := &file_agent_livekit_agent_thread_proto_msgTypes[11] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use AgentThread_HistoryResponse.ProtoReflect.Descriptor instead. +func (*AgentThread_HistoryResponse) Descriptor() ([]byte, []int) { + return file_agent_livekit_agent_thread_proto_rawDescGZIP(), []int{0, 10} +} + +func (x *AgentThread_HistoryResponse) GetItems() []*AgentThread_Item { + if x != nil { + return x.Items + } + return nil +} + +func (x *AgentThread_HistoryResponse) GetAfter() string { + if x != nil { + return x.After + } + return "" +} + +// One chat item, as the agents framework stores it (items in +// thread_schema_v1.sql). +type AgentThread_Item struct { + state protoimpl.MessageState `protogen:"open.v1"` + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` + Agent string `protobuf:"bytes,2,opt,name=agent,proto3" json:"agent,omitempty"` + // Position in the agent's history. + Seq int64 `protobuf:"varint,3,opt,name=seq,proto3" json:"seq,omitempty"` + // "message", "function_call", "function_call_output" or "agent_handoff". + Type string `protobuf:"bytes,4,opt,name=type,proto3" json:"type,omitempty"` + // The A2A message the item came from. + MessageId string `protobuf:"bytes,5,opt,name=message_id,json=messageId,proto3" json:"message_id,omitempty"` + Payload *structpb.Struct `protobuf:"bytes,6,opt,name=payload,proto3" json:"payload,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *AgentThread_Item) Reset() { + *x = AgentThread_Item{} + mi := &file_agent_livekit_agent_thread_proto_msgTypes[12] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *AgentThread_Item) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AgentThread_Item) ProtoMessage() {} + +func (x *AgentThread_Item) ProtoReflect() protoreflect.Message { + mi := &file_agent_livekit_agent_thread_proto_msgTypes[12] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use AgentThread_Item.ProtoReflect.Descriptor instead. +func (*AgentThread_Item) Descriptor() ([]byte, []int) { + return file_agent_livekit_agent_thread_proto_rawDescGZIP(), []int{0, 11} +} + +func (x *AgentThread_Item) GetId() string { + if x != nil { + return x.Id + } + return "" +} + +func (x *AgentThread_Item) GetAgent() string { + if x != nil { + return x.Agent + } + return "" +} + +func (x *AgentThread_Item) GetSeq() int64 { + if x != nil { + return x.Seq + } + return 0 +} + +func (x *AgentThread_Item) GetType() string { + if x != nil { + return x.Type + } + return "" +} + +func (x *AgentThread_Item) GetMessageId() string { + if x != nil { + return x.MessageId + } + return "" +} + +func (x *AgentThread_Item) GetPayload() *structpb.Struct { + if x != nil { + return x.Payload + } + return nil +} + +// The thread's A2A tasks, oldest first. +type AgentThread_ListTasksRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + ThreadId string `protobuf:"bytes,1,opt,name=thread_id,json=threadId,proto3" json:"thread_id,omitempty"` + Agent string `protobuf:"bytes,2,opt,name=agent,proto3" json:"agent,omitempty"` + After string `protobuf:"bytes,3,opt,name=after,proto3" json:"after,omitempty"` + Limit int32 `protobuf:"varint,4,opt,name=limit,proto3" json:"limit,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *AgentThread_ListTasksRequest) Reset() { + *x = AgentThread_ListTasksRequest{} + mi := &file_agent_livekit_agent_thread_proto_msgTypes[13] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *AgentThread_ListTasksRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AgentThread_ListTasksRequest) ProtoMessage() {} + +func (x *AgentThread_ListTasksRequest) ProtoReflect() protoreflect.Message { + mi := &file_agent_livekit_agent_thread_proto_msgTypes[13] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use AgentThread_ListTasksRequest.ProtoReflect.Descriptor instead. +func (*AgentThread_ListTasksRequest) Descriptor() ([]byte, []int) { + return file_agent_livekit_agent_thread_proto_rawDescGZIP(), []int{0, 12} +} + +func (x *AgentThread_ListTasksRequest) GetThreadId() string { + if x != nil { + return x.ThreadId + } + return "" +} + +func (x *AgentThread_ListTasksRequest) GetAgent() string { + if x != nil { + return x.Agent + } + return "" +} + +func (x *AgentThread_ListTasksRequest) GetAfter() string { + if x != nil { + return x.After + } + return "" +} + +func (x *AgentThread_ListTasksRequest) GetLimit() int32 { + if x != nil { + return x.Limit + } + return 0 +} + +type AgentThread_ListTasksResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Tasks []*AgentThread_Task `protobuf:"bytes,1,rep,name=tasks,proto3" json:"tasks,omitempty"` + After string `protobuf:"bytes,2,opt,name=after,proto3" json:"after,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *AgentThread_ListTasksResponse) Reset() { + *x = AgentThread_ListTasksResponse{} + mi := &file_agent_livekit_agent_thread_proto_msgTypes[14] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *AgentThread_ListTasksResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AgentThread_ListTasksResponse) ProtoMessage() {} + +func (x *AgentThread_ListTasksResponse) ProtoReflect() protoreflect.Message { + mi := &file_agent_livekit_agent_thread_proto_msgTypes[14] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use AgentThread_ListTasksResponse.ProtoReflect.Descriptor instead. +func (*AgentThread_ListTasksResponse) Descriptor() ([]byte, []int) { + return file_agent_livekit_agent_thread_proto_rawDescGZIP(), []int{0, 13} +} + +func (x *AgentThread_ListTasksResponse) GetTasks() []*AgentThread_Task { + if x != nil { + return x.Tasks + } + return nil +} + +func (x *AgentThread_ListTasksResponse) GetAfter() string { + if x != nil { + return x.After + } + return "" +} + +type AgentThread_Task struct { + state protoimpl.MessageState `protogen:"open.v1"` + TaskId string `protobuf:"bytes,1,opt,name=task_id,json=taskId,proto3" json:"task_id,omitempty"` + Agent string `protobuf:"bytes,2,opt,name=agent,proto3" json:"agent,omitempty"` + // "submitted", "working", "input-required", "completed", "failed", + // "canceled" or "rejected". + Status string `protobuf:"bytes,3,opt,name=status,proto3" json:"status,omitempty"` + StatusMessage *structpb.Struct `protobuf:"bytes,4,opt,name=status_message,json=statusMessage,proto3" json:"status_message,omitempty"` + History *structpb.ListValue `protobuf:"bytes,5,opt,name=history,proto3" json:"history,omitempty"` + Artifacts *structpb.ListValue `protobuf:"bytes,6,opt,name=artifacts,proto3" json:"artifacts,omitempty"` + // The task that submitted this one, if any. + Parent string `protobuf:"bytes,7,opt,name=parent,proto3" json:"parent,omitempty"` + UpdatedAt *timestamppb.Timestamp `protobuf:"bytes,8,opt,name=updated_at,json=updatedAt,proto3" json:"updated_at,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *AgentThread_Task) Reset() { + *x = AgentThread_Task{} + mi := &file_agent_livekit_agent_thread_proto_msgTypes[15] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *AgentThread_Task) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AgentThread_Task) ProtoMessage() {} + +func (x *AgentThread_Task) ProtoReflect() protoreflect.Message { + mi := &file_agent_livekit_agent_thread_proto_msgTypes[15] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use AgentThread_Task.ProtoReflect.Descriptor instead. +func (*AgentThread_Task) Descriptor() ([]byte, []int) { + return file_agent_livekit_agent_thread_proto_rawDescGZIP(), []int{0, 14} +} + +func (x *AgentThread_Task) GetTaskId() string { + if x != nil { + return x.TaskId + } + return "" +} + +func (x *AgentThread_Task) GetAgent() string { + if x != nil { + return x.Agent + } + return "" +} + +func (x *AgentThread_Task) GetStatus() string { + if x != nil { + return x.Status + } + return "" +} + +func (x *AgentThread_Task) GetStatusMessage() *structpb.Struct { + if x != nil { + return x.StatusMessage + } + return nil +} + +func (x *AgentThread_Task) GetHistory() *structpb.ListValue { + if x != nil { + return x.History + } + return nil +} + +func (x *AgentThread_Task) GetArtifacts() *structpb.ListValue { + if x != nil { + return x.Artifacts + } + return nil +} + +func (x *AgentThread_Task) GetParent() string { + if x != nil { + return x.Parent + } + return "" +} + +func (x *AgentThread_Task) GetUpdatedAt() *timestamppb.Timestamp { + if x != nil { + return x.UpdatedAt + } + return nil +} + type AgentThread_Thread struct { state protoimpl.MessageState `protogen:"open.v1"` ThreadId string `protobuf:"bytes,1,opt,name=thread_id,json=threadId,proto3" json:"thread_id,omitempty"` @@ -622,7 +1062,7 @@ type AgentThread_Thread struct { func (x *AgentThread_Thread) Reset() { *x = AgentThread_Thread{} - mi := &file_agent_livekit_agent_thread_proto_msgTypes[10] + mi := &file_agent_livekit_agent_thread_proto_msgTypes[16] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -634,7 +1074,7 @@ func (x *AgentThread_Thread) String() string { func (*AgentThread_Thread) ProtoMessage() {} func (x *AgentThread_Thread) ProtoReflect() protoreflect.Message { - mi := &file_agent_livekit_agent_thread_proto_msgTypes[10] + mi := &file_agent_livekit_agent_thread_proto_msgTypes[16] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -647,7 +1087,7 @@ func (x *AgentThread_Thread) ProtoReflect() protoreflect.Message { // Deprecated: Use AgentThread_Thread.ProtoReflect.Descriptor instead. func (*AgentThread_Thread) Descriptor() ([]byte, []int) { - return file_agent_livekit_agent_thread_proto_rawDescGZIP(), []int{0, 9} + return file_agent_livekit_agent_thread_proto_rawDescGZIP(), []int{0, 15} } func (x *AgentThread_Thread) GetThreadId() string { @@ -724,7 +1164,7 @@ var File_agent_livekit_agent_thread_proto protoreflect.FileDescriptor const file_agent_livekit_agent_thread_proto_rawDesc = "" + "\n" + - " agent/livekit_agent_thread.proto\x12\rlivekit.agent\x1a\x1egoogle/protobuf/duration.proto\x1a\x1fgoogle/protobuf/timestamp.proto\x1a\x14logger/options.proto\"\x83\r\n" + + " agent/livekit_agent_thread.proto\x12\rlivekit.agent\x1a\x1egoogle/protobuf/duration.proto\x1a\x1cgoogle/protobuf/struct.proto\x1a\x1fgoogle/protobuf/timestamp.proto\x1a\x14logger/options.proto\"\xd1\x14\n" + "\vAgentThread\x1a\x96\x02\n" + "\rCreateRequest\x12\x10\n" + "\x03key\x18\x02 \x01(\tR\x03key\x12\x19\n" + @@ -765,7 +1205,41 @@ const file_agent_livekit_agent_thread_proto_rawDesc = "" + "\x0eDeleteResponse\x1aR\n" + "\x12MergeThreadRequest\x12(\n" + "\tthread_id\x18\x01 \x01(\tB\v\xbaP\bthreadIDR\bthreadId\x12\x12\n" + - "\x04into\x18\x02 \x01(\tR\x04into\x1a\xa7\x04\n" + + "\x04into\x18\x02 \x01(\tR\x04into\x1a|\n" + + "\x0eHistoryRequest\x12(\n" + + "\tthread_id\x18\x01 \x01(\tB\v\xbaP\bthreadIDR\bthreadId\x12\x14\n" + + "\x05agent\x18\x02 \x01(\tR\x05agent\x12\x14\n" + + "\x05after\x18\x03 \x01(\tR\x05after\x12\x14\n" + + "\x05limit\x18\x04 \x01(\x05R\x05limit\x1a^\n" + + "\x0fHistoryResponse\x125\n" + + "\x05items\x18\x01 \x03(\v2\x1f.livekit.agent.AgentThread.ItemR\x05items\x12\x14\n" + + "\x05after\x18\x02 \x01(\tR\x05after\x1a\xa9\x01\n" + + "\x04Item\x12\x0e\n" + + "\x02id\x18\x01 \x01(\tR\x02id\x12\x14\n" + + "\x05agent\x18\x02 \x01(\tR\x05agent\x12\x10\n" + + "\x03seq\x18\x03 \x01(\x03R\x03seq\x12\x12\n" + + "\x04type\x18\x04 \x01(\tR\x04type\x12\x1d\n" + + "\n" + + "message_id\x18\x05 \x01(\tR\tmessageId\x126\n" + + "\apayload\x18\x06 \x01(\v2\x17.google.protobuf.StructB\x03\xc0P\x01R\apayload\x1a~\n" + + "\x10ListTasksRequest\x12(\n" + + "\tthread_id\x18\x01 \x01(\tB\v\xbaP\bthreadIDR\bthreadId\x12\x14\n" + + "\x05agent\x18\x02 \x01(\tR\x05agent\x12\x14\n" + + "\x05after\x18\x03 \x01(\tR\x05after\x12\x14\n" + + "\x05limit\x18\x04 \x01(\x05R\x05limit\x1a`\n" + + "\x11ListTasksResponse\x125\n" + + "\x05tasks\x18\x01 \x03(\v2\x1f.livekit.agent.AgentThread.TaskR\x05tasks\x12\x14\n" + + "\x05after\x18\x02 \x01(\tR\x05after\x1a\xdf\x02\n" + + "\x04Task\x12\x17\n" + + "\atask_id\x18\x01 \x01(\tR\x06taskId\x12\x14\n" + + "\x05agent\x18\x02 \x01(\tR\x05agent\x12\x16\n" + + "\x06status\x18\x03 \x01(\tR\x06status\x12C\n" + + "\x0estatus_message\x18\x04 \x01(\v2\x17.google.protobuf.StructB\x03\xc0P\x01R\rstatusMessage\x129\n" + + "\ahistory\x18\x05 \x01(\v2\x1a.google.protobuf.ListValueB\x03\xc0P\x01R\ahistory\x12=\n" + + "\tartifacts\x18\x06 \x01(\v2\x1a.google.protobuf.ListValueB\x03\xc0P\x01R\tartifacts\x12\x16\n" + + "\x06parent\x18\a \x01(\tR\x06parent\x129\n" + + "\n" + + "updated_at\x18\b \x01(\v2\x1a.google.protobuf.TimestampR\tupdatedAt\x1a\xa7\x04\n" + "\x06Thread\x12(\n" + "\tthread_id\x18\x01 \x01(\tB\v\xbaP\bthreadIDR\bthreadId\x12\x10\n" + "\x03key\x18\x03 \x01(\tR\x03key\x12\x19\n" + @@ -801,7 +1275,7 @@ func file_agent_livekit_agent_thread_proto_rawDescGZIP() []byte { return file_agent_livekit_agent_thread_proto_rawDescData } -var file_agent_livekit_agent_thread_proto_msgTypes = make([]protoimpl.MessageInfo, 14) +var file_agent_livekit_agent_thread_proto_msgTypes = make([]protoimpl.MessageInfo, 20) var file_agent_livekit_agent_thread_proto_goTypes = []any{ (*AgentThread)(nil), // 0: livekit.agent.AgentThread (*AgentThread_CreateRequest)(nil), // 1: livekit.agent.AgentThread.CreateRequest @@ -813,30 +1287,45 @@ var file_agent_livekit_agent_thread_proto_goTypes = []any{ (*AgentThread_DeleteRequest)(nil), // 7: livekit.agent.AgentThread.DeleteRequest (*AgentThread_DeleteResponse)(nil), // 8: livekit.agent.AgentThread.DeleteResponse (*AgentThread_MergeThreadRequest)(nil), // 9: livekit.agent.AgentThread.MergeThreadRequest - (*AgentThread_Thread)(nil), // 10: livekit.agent.AgentThread.Thread - nil, // 11: livekit.agent.AgentThread.CreateRequest.AttributesEntry - nil, // 12: livekit.agent.AgentThread.UpdateRequest.AttributesEntry - nil, // 13: livekit.agent.AgentThread.Thread.AttributesEntry - (*durationpb.Duration)(nil), // 14: google.protobuf.Duration - (*timestamppb.Timestamp)(nil), // 15: google.protobuf.Timestamp + (*AgentThread_HistoryRequest)(nil), // 10: livekit.agent.AgentThread.HistoryRequest + (*AgentThread_HistoryResponse)(nil), // 11: livekit.agent.AgentThread.HistoryResponse + (*AgentThread_Item)(nil), // 12: livekit.agent.AgentThread.Item + (*AgentThread_ListTasksRequest)(nil), // 13: livekit.agent.AgentThread.ListTasksRequest + (*AgentThread_ListTasksResponse)(nil), // 14: livekit.agent.AgentThread.ListTasksResponse + (*AgentThread_Task)(nil), // 15: livekit.agent.AgentThread.Task + (*AgentThread_Thread)(nil), // 16: livekit.agent.AgentThread.Thread + nil, // 17: livekit.agent.AgentThread.CreateRequest.AttributesEntry + nil, // 18: livekit.agent.AgentThread.UpdateRequest.AttributesEntry + nil, // 19: livekit.agent.AgentThread.Thread.AttributesEntry + (*durationpb.Duration)(nil), // 20: google.protobuf.Duration + (*structpb.Struct)(nil), // 21: google.protobuf.Struct + (*structpb.ListValue)(nil), // 22: google.protobuf.ListValue + (*timestamppb.Timestamp)(nil), // 23: google.protobuf.Timestamp } var file_agent_livekit_agent_thread_proto_depIdxs = []int32{ - 11, // 0: livekit.agent.AgentThread.CreateRequest.attributes:type_name -> livekit.agent.AgentThread.CreateRequest.AttributesEntry - 14, // 1: livekit.agent.AgentThread.CreateRequest.idle_ttl:type_name -> google.protobuf.Duration - 10, // 2: livekit.agent.AgentThread.CreateResponse.thread:type_name -> livekit.agent.AgentThread.Thread - 10, // 3: livekit.agent.AgentThread.ListResponse.threads:type_name -> livekit.agent.AgentThread.Thread - 12, // 4: livekit.agent.AgentThread.UpdateRequest.attributes:type_name -> livekit.agent.AgentThread.UpdateRequest.AttributesEntry - 14, // 5: livekit.agent.AgentThread.UpdateRequest.idle_ttl:type_name -> google.protobuf.Duration - 13, // 6: livekit.agent.AgentThread.Thread.attributes:type_name -> livekit.agent.AgentThread.Thread.AttributesEntry - 15, // 7: livekit.agent.AgentThread.Thread.created_at:type_name -> google.protobuf.Timestamp - 15, // 8: livekit.agent.AgentThread.Thread.last_active:type_name -> google.protobuf.Timestamp - 15, // 9: livekit.agent.AgentThread.Thread.expires_at:type_name -> google.protobuf.Timestamp - 14, // 10: livekit.agent.AgentThread.Thread.idle_ttl:type_name -> google.protobuf.Duration - 11, // [11:11] is the sub-list for method output_type - 11, // [11:11] is the sub-list for method input_type - 11, // [11:11] is the sub-list for extension type_name - 11, // [11:11] is the sub-list for extension extendee - 0, // [0:11] is the sub-list for field type_name + 17, // 0: livekit.agent.AgentThread.CreateRequest.attributes:type_name -> livekit.agent.AgentThread.CreateRequest.AttributesEntry + 20, // 1: livekit.agent.AgentThread.CreateRequest.idle_ttl:type_name -> google.protobuf.Duration + 16, // 2: livekit.agent.AgentThread.CreateResponse.thread:type_name -> livekit.agent.AgentThread.Thread + 16, // 3: livekit.agent.AgentThread.ListResponse.threads:type_name -> livekit.agent.AgentThread.Thread + 18, // 4: livekit.agent.AgentThread.UpdateRequest.attributes:type_name -> livekit.agent.AgentThread.UpdateRequest.AttributesEntry + 20, // 5: livekit.agent.AgentThread.UpdateRequest.idle_ttl:type_name -> google.protobuf.Duration + 12, // 6: livekit.agent.AgentThread.HistoryResponse.items:type_name -> livekit.agent.AgentThread.Item + 21, // 7: livekit.agent.AgentThread.Item.payload:type_name -> google.protobuf.Struct + 15, // 8: livekit.agent.AgentThread.ListTasksResponse.tasks:type_name -> livekit.agent.AgentThread.Task + 21, // 9: livekit.agent.AgentThread.Task.status_message:type_name -> google.protobuf.Struct + 22, // 10: livekit.agent.AgentThread.Task.history:type_name -> google.protobuf.ListValue + 22, // 11: livekit.agent.AgentThread.Task.artifacts:type_name -> google.protobuf.ListValue + 23, // 12: livekit.agent.AgentThread.Task.updated_at:type_name -> google.protobuf.Timestamp + 19, // 13: livekit.agent.AgentThread.Thread.attributes:type_name -> livekit.agent.AgentThread.Thread.AttributesEntry + 23, // 14: livekit.agent.AgentThread.Thread.created_at:type_name -> google.protobuf.Timestamp + 23, // 15: livekit.agent.AgentThread.Thread.last_active:type_name -> google.protobuf.Timestamp + 23, // 16: livekit.agent.AgentThread.Thread.expires_at:type_name -> google.protobuf.Timestamp + 20, // 17: livekit.agent.AgentThread.Thread.idle_ttl:type_name -> google.protobuf.Duration + 18, // [18:18] is the sub-list for method output_type + 18, // [18:18] is the sub-list for method input_type + 18, // [18:18] is the sub-list for extension type_name + 18, // [18:18] is the sub-list for extension extendee + 0, // [0:18] is the sub-list for field type_name } func init() { file_agent_livekit_agent_thread_proto_init() } @@ -854,7 +1343,7 @@ func file_agent_livekit_agent_thread_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_agent_livekit_agent_thread_proto_rawDesc), len(file_agent_livekit_agent_thread_proto_rawDesc)), NumEnums: 0, - NumMessages: 14, + NumMessages: 20, NumExtensions: 0, NumServices: 0, }, diff --git a/livekit/agent/thread_schema.go b/livekit/agent/thread_schema.go new file mode 100644 index 000000000..e3b5d766f --- /dev/null +++ b/livekit/agent/thread_schema.go @@ -0,0 +1,27 @@ +// Copyright 2026 LiveKit, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package agent + +import _ "embed" + +// ThreadSchemaVersion is the newest conversation schema a thread's database +// can hold. +const ThreadSchemaVersion = 1 + +// ThreadSchemaV1 creates version 1 of the conversation schema, one statement +// per ";". +// +//go:embed thread_schema_v1.sql +var ThreadSchemaV1 string diff --git a/livekit/agent/thread_schema_v1.sql b/livekit/agent/thread_schema_v1.sql new file mode 100644 index 000000000..952d54532 --- /dev/null +++ b/livekit/agent/thread_schema_v1.sql @@ -0,0 +1,26 @@ +-- The conversation inside a thread's database, version 1. The agents framework +-- creates it on a thread's first write; the platform reads items and tasks and +-- merges threads. schema_version, items, tasks and lanes.lane are shared: a +-- change to them is additive and ships as a new version file. Other tables and +-- lane columns belong to the framework. +CREATE TABLE schema_version (version INTEGER NOT NULL, applied_at INTEGER NOT NULL); +CREATE TABLE items (lane TEXT NOT NULL, id TEXT PRIMARY KEY, seq INTEGER NOT NULL, type TEXT NOT NULL, + message_id TEXT, payload JSONB NOT NULL); +CREATE INDEX items_lane_seq ON items (lane, seq); +CREATE UNIQUE INDEX items_lane_message ON items (lane, message_id) WHERE message_id IS NOT NULL; +CREATE TABLE events (seq INTEGER PRIMARY KEY, ts_ms INTEGER NOT NULL, lane TEXT NOT NULL, run_id TEXT NOT NULL, + author TEXT NOT NULL, type TEXT NOT NULL, payload JSONB NOT NULL); +CREATE INDEX events_lane_seq ON events (lane, seq); +CREATE TABLE lanes (lane TEXT PRIMARY KEY, version INTEGER NOT NULL, state JSONB, agent_stack JSONB, + lease_holder TEXT, lease_token TEXT, lease_until INTEGER NOT NULL DEFAULT 0, waiter TEXT, + drained_seq INTEGER NOT NULL DEFAULT 0); +CREATE TABLE inbox (seq INTEGER PRIMARY KEY, lane TEXT NOT NULL, task_id TEXT NOT NULL, message_id TEXT NOT NULL, + message JSONB NOT NULL, received_at INTEGER NOT NULL); +CREATE INDEX inbox_lane_seq ON inbox (lane, seq); +CREATE UNIQUE INDEX inbox_lane_message ON inbox (lane, message_id); +CREATE TABLE tasks (task_id TEXT PRIMARY KEY, lane TEXT NOT NULL, seq INTEGER NOT NULL, status TEXT NOT NULL, + message_id TEXT, task JSONB NOT NULL, agent TEXT NOT NULL, due_at INTEGER, deadline INTEGER, + ask JSONB, status_message JSONB, history JSONB NOT NULL, artifacts JSONB, parent TEXT, + push_configs JSONB, attempts INTEGER NOT NULL DEFAULT 0, updated_at INTEGER NOT NULL); +CREATE INDEX tasks_lane_status_due ON tasks (lane, status, due_at); +CREATE UNIQUE INDEX tasks_lane_message ON tasks (lane, message_id) WHERE message_id IS NOT NULL; diff --git a/protobufs/agent/livekit_agent_thread.proto b/protobufs/agent/livekit_agent_thread.proto index 8e23be095..2590f0caf 100644 --- a/protobufs/agent/livekit_agent_thread.proto +++ b/protobufs/agent/livekit_agent_thread.proto @@ -16,7 +16,9 @@ // channels and over time. Identified by an "AT_" id. Holds a scope, attributes, // an idle TTL and the AgentDB database created with it. An internal key (a // LiveKit phone number) can find it again. Served by cloud-protocol's -// AgentThreads. +// AgentThreads. The conversation inside follows thread_schema_v1.sql, shared +// by the agents framework, which writes it, and the platform, which reads and +// merges it. syntax = "proto3"; @@ -25,6 +27,7 @@ package livekit.agent; option go_package = "github.com/livekit/protocol/livekit/agent"; import "google/protobuf/duration.proto"; +import "google/protobuf/struct.proto"; import "google/protobuf/timestamp.proto"; import "logger/options.proto"; @@ -84,13 +87,70 @@ message AgentThread { message DeleteResponse {} - // Marks thread_id as merged into another thread, after its items were copied - // there. + // Copies every agent's history and tasks of thread_id into another thread, + // as agents named "@", then marks thread_id merged. message MergeThreadRequest { string thread_id = 1 [(logger.name) = "threadID"]; string into = 2; } + // The conversation, oldest first. + message HistoryRequest { + string thread_id = 1 [(logger.name) = "threadID"]; + // One agent's history; empty for every agent's. + string agent = 2; + // Cursor from the previous HistoryResponse; empty for the first page. + string after = 3; + int32 limit = 4; + } + + message HistoryResponse { + repeated Item items = 1; + // Empty at the end. + string after = 2; + } + + // One chat item, as the agents framework stores it (items in + // thread_schema_v1.sql). + message Item { + string id = 1; + string agent = 2; + // Position in the agent's history. + int64 seq = 3; + // "message", "function_call", "function_call_output" or "agent_handoff". + string type = 4; + // The A2A message the item came from. + string message_id = 5; + google.protobuf.Struct payload = 6 [(logger.sensitivity) = SENSITIVITY_PII]; + } + + // The thread's A2A tasks, oldest first. + message ListTasksRequest { + string thread_id = 1 [(logger.name) = "threadID"]; + string agent = 2; + string after = 3; + int32 limit = 4; + } + + message ListTasksResponse { + repeated Task tasks = 1; + string after = 2; + } + + message Task { + string task_id = 1; + string agent = 2; + // "submitted", "working", "input-required", "completed", "failed", + // "canceled" or "rejected". + string status = 3; + google.protobuf.Struct status_message = 4 [(logger.sensitivity) = SENSITIVITY_PII]; + google.protobuf.ListValue history = 5 [(logger.sensitivity) = SENSITIVITY_PII]; + google.protobuf.ListValue artifacts = 6 [(logger.sensitivity) = SENSITIVITY_PII]; + // The task that submitted this one, if any. + string parent = 7; + google.protobuf.Timestamp updated_at = 8; + } + message Thread { string thread_id = 1 [(logger.name) = "threadID"]; reserved 2; From 57e479a96b72054fe282fa6034e9dd309a1670c9 Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 8 Oct 2026 15:43:19 +0000 Subject: [PATCH 2/8] generated protobuf --- livekit/agent/livekit_agent_thread.pb.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/livekit/agent/livekit_agent_thread.pb.go b/livekit/agent/livekit_agent_thread.pb.go index de54bc95d..cceff613b 100644 --- a/livekit/agent/livekit_agent_thread.pb.go +++ b/livekit/agent/livekit_agent_thread.pb.go @@ -23,7 +23,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.36.12 -// protoc v7.36.2 +// protoc v7.35.1 // source: agent/livekit_agent_thread.proto package agent From 2222361b79728dba91ec66c6fc81524407409618 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9o=20Monnom?= Date: Thu, 8 Oct 2026 08:44:46 -0700 Subject: [PATCH 3/8] agent thread: history items are ChatContext.ChatItem --- livekit/agent/livekit_agent_thread.pb.go | 89 +++++++++------------- livekit/agent/thread_schema_v1.sql | 7 +- protobufs/agent/livekit_agent_thread.proto | 18 ++--- 3 files changed, 48 insertions(+), 66 deletions(-) diff --git a/livekit/agent/livekit_agent_thread.pb.go b/livekit/agent/livekit_agent_thread.pb.go index cceff613b..8c42afb14 100644 --- a/livekit/agent/livekit_agent_thread.pb.go +++ b/livekit/agent/livekit_agent_thread.pb.go @@ -725,19 +725,16 @@ func (x *AgentThread_HistoryResponse) GetAfter() string { return "" } -// One chat item, as the agents framework stores it (items in -// thread_schema_v1.sql). +// One chat item of an agent's history. Stored as the JSON of +// ChatContext.ChatItem (items.payload in thread_schema_v1.sql). type AgentThread_Item struct { state protoimpl.MessageState `protogen:"open.v1"` - Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` - Agent string `protobuf:"bytes,2,opt,name=agent,proto3" json:"agent,omitempty"` + Agent string `protobuf:"bytes,1,opt,name=agent,proto3" json:"agent,omitempty"` // Position in the agent's history. - Seq int64 `protobuf:"varint,3,opt,name=seq,proto3" json:"seq,omitempty"` - // "message", "function_call", "function_call_output" or "agent_handoff". - Type string `protobuf:"bytes,4,opt,name=type,proto3" json:"type,omitempty"` + Seq int64 `protobuf:"varint,2,opt,name=seq,proto3" json:"seq,omitempty"` // The A2A message the item came from. - MessageId string `protobuf:"bytes,5,opt,name=message_id,json=messageId,proto3" json:"message_id,omitempty"` - Payload *structpb.Struct `protobuf:"bytes,6,opt,name=payload,proto3" json:"payload,omitempty"` + MessageId string `protobuf:"bytes,3,opt,name=message_id,json=messageId,proto3" json:"message_id,omitempty"` + Item *ChatContext_ChatItem `protobuf:"bytes,4,opt,name=item,proto3" json:"item,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -772,13 +769,6 @@ func (*AgentThread_Item) Descriptor() ([]byte, []int) { return file_agent_livekit_agent_thread_proto_rawDescGZIP(), []int{0, 11} } -func (x *AgentThread_Item) GetId() string { - if x != nil { - return x.Id - } - return "" -} - func (x *AgentThread_Item) GetAgent() string { if x != nil { return x.Agent @@ -793,13 +783,6 @@ func (x *AgentThread_Item) GetSeq() int64 { return 0 } -func (x *AgentThread_Item) GetType() string { - if x != nil { - return x.Type - } - return "" -} - func (x *AgentThread_Item) GetMessageId() string { if x != nil { return x.MessageId @@ -807,9 +790,9 @@ func (x *AgentThread_Item) GetMessageId() string { return "" } -func (x *AgentThread_Item) GetPayload() *structpb.Struct { +func (x *AgentThread_Item) GetItem() *ChatContext_ChatItem { if x != nil { - return x.Payload + return x.Item } return nil } @@ -941,10 +924,10 @@ type AgentThread_Task struct { Agent string `protobuf:"bytes,2,opt,name=agent,proto3" json:"agent,omitempty"` // "submitted", "working", "input-required", "completed", "failed", // "canceled" or "rejected". - Status string `protobuf:"bytes,3,opt,name=status,proto3" json:"status,omitempty"` - StatusMessage *structpb.Struct `protobuf:"bytes,4,opt,name=status_message,json=statusMessage,proto3" json:"status_message,omitempty"` - History *structpb.ListValue `protobuf:"bytes,5,opt,name=history,proto3" json:"history,omitempty"` - Artifacts *structpb.ListValue `protobuf:"bytes,6,opt,name=artifacts,proto3" json:"artifacts,omitempty"` + Status string `protobuf:"bytes,3,opt,name=status,proto3" json:"status,omitempty"` + StatusMessage *structpb.Struct `protobuf:"bytes,4,opt,name=status_message,json=statusMessage,proto3" json:"status_message,omitempty"` + History []*ChatContext_ChatItem `protobuf:"bytes,5,rep,name=history,proto3" json:"history,omitempty"` + Artifacts *structpb.ListValue `protobuf:"bytes,6,opt,name=artifacts,proto3" json:"artifacts,omitempty"` // The task that submitted this one, if any. Parent string `protobuf:"bytes,7,opt,name=parent,proto3" json:"parent,omitempty"` UpdatedAt *timestamppb.Timestamp `protobuf:"bytes,8,opt,name=updated_at,json=updatedAt,proto3" json:"updated_at,omitempty"` @@ -1010,7 +993,7 @@ func (x *AgentThread_Task) GetStatusMessage() *structpb.Struct { return nil } -func (x *AgentThread_Task) GetHistory() *structpb.ListValue { +func (x *AgentThread_Task) GetHistory() []*ChatContext_ChatItem { if x != nil { return x.History } @@ -1164,7 +1147,7 @@ var File_agent_livekit_agent_thread_proto protoreflect.FileDescriptor const file_agent_livekit_agent_thread_proto_rawDesc = "" + "\n" + - " agent/livekit_agent_thread.proto\x12\rlivekit.agent\x1a\x1egoogle/protobuf/duration.proto\x1a\x1cgoogle/protobuf/struct.proto\x1a\x1fgoogle/protobuf/timestamp.proto\x1a\x14logger/options.proto\"\xd1\x14\n" + + " agent/livekit_agent_thread.proto\x12\rlivekit.agent\x1a\x1egoogle/protobuf/duration.proto\x1a\x1cgoogle/protobuf/struct.proto\x1a\x1fgoogle/protobuf/timestamp.proto\x1a!agent/livekit_agent_session.proto\x1a\x14logger/options.proto\"\xbc\x14\n" + "\vAgentThread\x1a\x96\x02\n" + "\rCreateRequest\x12\x10\n" + "\x03key\x18\x02 \x01(\tR\x03key\x12\x19\n" + @@ -1213,15 +1196,13 @@ const file_agent_livekit_agent_thread_proto_rawDesc = "" + "\x05limit\x18\x04 \x01(\x05R\x05limit\x1a^\n" + "\x0fHistoryResponse\x125\n" + "\x05items\x18\x01 \x03(\v2\x1f.livekit.agent.AgentThread.ItemR\x05items\x12\x14\n" + - "\x05after\x18\x02 \x01(\tR\x05after\x1a\xa9\x01\n" + - "\x04Item\x12\x0e\n" + - "\x02id\x18\x01 \x01(\tR\x02id\x12\x14\n" + - "\x05agent\x18\x02 \x01(\tR\x05agent\x12\x10\n" + - "\x03seq\x18\x03 \x01(\x03R\x03seq\x12\x12\n" + - "\x04type\x18\x04 \x01(\tR\x04type\x12\x1d\n" + + "\x05after\x18\x02 \x01(\tR\x05after\x1a\x8b\x01\n" + + "\x04Item\x12\x14\n" + + "\x05agent\x18\x01 \x01(\tR\x05agent\x12\x10\n" + + "\x03seq\x18\x02 \x01(\x03R\x03seq\x12\x1d\n" + "\n" + - "message_id\x18\x05 \x01(\tR\tmessageId\x126\n" + - "\apayload\x18\x06 \x01(\v2\x17.google.protobuf.StructB\x03\xc0P\x01R\apayload\x1a~\n" + + "message_id\x18\x03 \x01(\tR\tmessageId\x12<\n" + + "\x04item\x18\x04 \x01(\v2#.livekit.agent.ChatContext.ChatItemB\x03\xc0P\x01R\x04item\x1a~\n" + "\x10ListTasksRequest\x12(\n" + "\tthread_id\x18\x01 \x01(\tB\v\xbaP\bthreadIDR\bthreadId\x12\x14\n" + "\x05agent\x18\x02 \x01(\tR\x05agent\x12\x14\n" + @@ -1229,13 +1210,13 @@ const file_agent_livekit_agent_thread_proto_rawDesc = "" + "\x05limit\x18\x04 \x01(\x05R\x05limit\x1a`\n" + "\x11ListTasksResponse\x125\n" + "\x05tasks\x18\x01 \x03(\v2\x1f.livekit.agent.AgentThread.TaskR\x05tasks\x12\x14\n" + - "\x05after\x18\x02 \x01(\tR\x05after\x1a\xdf\x02\n" + + "\x05after\x18\x02 \x01(\tR\x05after\x1a\xe8\x02\n" + "\x04Task\x12\x17\n" + "\atask_id\x18\x01 \x01(\tR\x06taskId\x12\x14\n" + "\x05agent\x18\x02 \x01(\tR\x05agent\x12\x16\n" + "\x06status\x18\x03 \x01(\tR\x06status\x12C\n" + - "\x0estatus_message\x18\x04 \x01(\v2\x17.google.protobuf.StructB\x03\xc0P\x01R\rstatusMessage\x129\n" + - "\ahistory\x18\x05 \x01(\v2\x1a.google.protobuf.ListValueB\x03\xc0P\x01R\ahistory\x12=\n" + + "\x0estatus_message\x18\x04 \x01(\v2\x17.google.protobuf.StructB\x03\xc0P\x01R\rstatusMessage\x12B\n" + + "\ahistory\x18\x05 \x03(\v2#.livekit.agent.ChatContext.ChatItemB\x03\xc0P\x01R\ahistory\x12=\n" + "\tartifacts\x18\x06 \x01(\v2\x1a.google.protobuf.ListValueB\x03\xc0P\x01R\tartifacts\x12\x16\n" + "\x06parent\x18\a \x01(\tR\x06parent\x129\n" + "\n" + @@ -1298,9 +1279,10 @@ var file_agent_livekit_agent_thread_proto_goTypes = []any{ nil, // 18: livekit.agent.AgentThread.UpdateRequest.AttributesEntry nil, // 19: livekit.agent.AgentThread.Thread.AttributesEntry (*durationpb.Duration)(nil), // 20: google.protobuf.Duration - (*structpb.Struct)(nil), // 21: google.protobuf.Struct - (*structpb.ListValue)(nil), // 22: google.protobuf.ListValue - (*timestamppb.Timestamp)(nil), // 23: google.protobuf.Timestamp + (*ChatContext_ChatItem)(nil), // 21: livekit.agent.ChatContext.ChatItem + (*structpb.Struct)(nil), // 22: google.protobuf.Struct + (*structpb.ListValue)(nil), // 23: google.protobuf.ListValue + (*timestamppb.Timestamp)(nil), // 24: google.protobuf.Timestamp } var file_agent_livekit_agent_thread_proto_depIdxs = []int32{ 17, // 0: livekit.agent.AgentThread.CreateRequest.attributes:type_name -> livekit.agent.AgentThread.CreateRequest.AttributesEntry @@ -1310,16 +1292,16 @@ var file_agent_livekit_agent_thread_proto_depIdxs = []int32{ 18, // 4: livekit.agent.AgentThread.UpdateRequest.attributes:type_name -> livekit.agent.AgentThread.UpdateRequest.AttributesEntry 20, // 5: livekit.agent.AgentThread.UpdateRequest.idle_ttl:type_name -> google.protobuf.Duration 12, // 6: livekit.agent.AgentThread.HistoryResponse.items:type_name -> livekit.agent.AgentThread.Item - 21, // 7: livekit.agent.AgentThread.Item.payload:type_name -> google.protobuf.Struct + 21, // 7: livekit.agent.AgentThread.Item.item:type_name -> livekit.agent.ChatContext.ChatItem 15, // 8: livekit.agent.AgentThread.ListTasksResponse.tasks:type_name -> livekit.agent.AgentThread.Task - 21, // 9: livekit.agent.AgentThread.Task.status_message:type_name -> google.protobuf.Struct - 22, // 10: livekit.agent.AgentThread.Task.history:type_name -> google.protobuf.ListValue - 22, // 11: livekit.agent.AgentThread.Task.artifacts:type_name -> google.protobuf.ListValue - 23, // 12: livekit.agent.AgentThread.Task.updated_at:type_name -> google.protobuf.Timestamp + 22, // 9: livekit.agent.AgentThread.Task.status_message:type_name -> google.protobuf.Struct + 21, // 10: livekit.agent.AgentThread.Task.history:type_name -> livekit.agent.ChatContext.ChatItem + 23, // 11: livekit.agent.AgentThread.Task.artifacts:type_name -> google.protobuf.ListValue + 24, // 12: livekit.agent.AgentThread.Task.updated_at:type_name -> google.protobuf.Timestamp 19, // 13: livekit.agent.AgentThread.Thread.attributes:type_name -> livekit.agent.AgentThread.Thread.AttributesEntry - 23, // 14: livekit.agent.AgentThread.Thread.created_at:type_name -> google.protobuf.Timestamp - 23, // 15: livekit.agent.AgentThread.Thread.last_active:type_name -> google.protobuf.Timestamp - 23, // 16: livekit.agent.AgentThread.Thread.expires_at:type_name -> google.protobuf.Timestamp + 24, // 14: livekit.agent.AgentThread.Thread.created_at:type_name -> google.protobuf.Timestamp + 24, // 15: livekit.agent.AgentThread.Thread.last_active:type_name -> google.protobuf.Timestamp + 24, // 16: livekit.agent.AgentThread.Thread.expires_at:type_name -> google.protobuf.Timestamp 20, // 17: livekit.agent.AgentThread.Thread.idle_ttl:type_name -> google.protobuf.Duration 18, // [18:18] is the sub-list for method output_type 18, // [18:18] is the sub-list for method input_type @@ -1333,6 +1315,7 @@ func file_agent_livekit_agent_thread_proto_init() { if File_agent_livekit_agent_thread_proto != nil { return } + file_agent_livekit_agent_session_proto_init() file_agent_livekit_agent_thread_proto_msgTypes[3].OneofWrappers = []any{ (*AgentThread_GetRequest_ThreadId)(nil), (*AgentThread_GetRequest_Key)(nil), diff --git a/livekit/agent/thread_schema_v1.sql b/livekit/agent/thread_schema_v1.sql index 952d54532..06185d171 100644 --- a/livekit/agent/thread_schema_v1.sql +++ b/livekit/agent/thread_schema_v1.sql @@ -1,8 +1,9 @@ -- The conversation inside a thread's database, version 1. The agents framework -- creates it on a thread's first write; the platform reads items and tasks and --- merges threads. schema_version, items, tasks and lanes.lane are shared: a --- change to them is additive and ships as a new version file. Other tables and --- lane columns belong to the framework. +-- merges threads. items.payload and tasks.history hold +-- livekit.agent.ChatContext.ChatItem as protobuf JSON. schema_version, items, +-- tasks and lanes.lane are shared: a change to them is additive and ships as a +-- new version file. Other tables and lane columns belong to the framework. CREATE TABLE schema_version (version INTEGER NOT NULL, applied_at INTEGER NOT NULL); CREATE TABLE items (lane TEXT NOT NULL, id TEXT PRIMARY KEY, seq INTEGER NOT NULL, type TEXT NOT NULL, message_id TEXT, payload JSONB NOT NULL); diff --git a/protobufs/agent/livekit_agent_thread.proto b/protobufs/agent/livekit_agent_thread.proto index 2590f0caf..7431145f3 100644 --- a/protobufs/agent/livekit_agent_thread.proto +++ b/protobufs/agent/livekit_agent_thread.proto @@ -29,6 +29,7 @@ option go_package = "github.com/livekit/protocol/livekit/agent"; import "google/protobuf/duration.proto"; import "google/protobuf/struct.proto"; import "google/protobuf/timestamp.proto"; +import "agent/livekit_agent_session.proto"; import "logger/options.proto"; message AgentThread { @@ -110,18 +111,15 @@ message AgentThread { string after = 2; } - // One chat item, as the agents framework stores it (items in - // thread_schema_v1.sql). + // One chat item of an agent's history. Stored as the JSON of + // ChatContext.ChatItem (items.payload in thread_schema_v1.sql). message Item { - string id = 1; - string agent = 2; + string agent = 1; // Position in the agent's history. - int64 seq = 3; - // "message", "function_call", "function_call_output" or "agent_handoff". - string type = 4; + int64 seq = 2; // The A2A message the item came from. - string message_id = 5; - google.protobuf.Struct payload = 6 [(logger.sensitivity) = SENSITIVITY_PII]; + string message_id = 3; + ChatContext.ChatItem item = 4 [(logger.sensitivity) = SENSITIVITY_PII]; } // The thread's A2A tasks, oldest first. @@ -144,7 +142,7 @@ message AgentThread { // "canceled" or "rejected". string status = 3; google.protobuf.Struct status_message = 4 [(logger.sensitivity) = SENSITIVITY_PII]; - google.protobuf.ListValue history = 5 [(logger.sensitivity) = SENSITIVITY_PII]; + repeated ChatContext.ChatItem history = 5 [(logger.sensitivity) = SENSITIVITY_PII]; google.protobuf.ListValue artifacts = 6 [(logger.sensitivity) = SENSITIVITY_PII]; // The task that submitted this one, if any. string parent = 7; From 51a08c0bf038addec379b3ac742fe730df695f48 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9o=20Monnom?= Date: Thu, 8 Oct 2026 08:49:38 -0700 Subject: [PATCH 4/8] thread schema: no semicolon in the header comment --- livekit/agent/thread_schema.go | 2 +- livekit/agent/thread_schema_v1.sql | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/livekit/agent/thread_schema.go b/livekit/agent/thread_schema.go index e3b5d766f..fe2c9a8a9 100644 --- a/livekit/agent/thread_schema.go +++ b/livekit/agent/thread_schema.go @@ -21,7 +21,7 @@ import _ "embed" const ThreadSchemaVersion = 1 // ThreadSchemaV1 creates version 1 of the conversation schema, one statement -// per ";". +// per ";" (comments hold none). // //go:embed thread_schema_v1.sql var ThreadSchemaV1 string diff --git a/livekit/agent/thread_schema_v1.sql b/livekit/agent/thread_schema_v1.sql index 06185d171..abd8471a0 100644 --- a/livekit/agent/thread_schema_v1.sql +++ b/livekit/agent/thread_schema_v1.sql @@ -1,6 +1,6 @@ -- The conversation inside a thread's database, version 1. The agents framework --- creates it on a thread's first write; the platform reads items and tasks and --- merges threads. items.payload and tasks.history hold +-- creates it on a thread's first write, and the platform reads items and tasks +-- and merges threads. items.payload and tasks.history hold -- livekit.agent.ChatContext.ChatItem as protobuf JSON. schema_version, items, -- tasks and lanes.lane are shared: a change to them is additive and ships as a -- new version file. Other tables and lane columns belong to the framework. From 3afce8e820785c7f9a8592c953f90b49da54e220 Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 8 Oct 2026 20:26:30 +0000 Subject: [PATCH 5/8] generated protobuf --- livekit/agent/livekit_agent_thread.pb.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/livekit/agent/livekit_agent_thread.pb.go b/livekit/agent/livekit_agent_thread.pb.go index b3b5650fb..f86914bf3 100644 --- a/livekit/agent/livekit_agent_thread.pb.go +++ b/livekit/agent/livekit_agent_thread.pb.go @@ -22,7 +22,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.36.12 -// protoc v7.36.2 +// protoc v7.35.1 // source: agent/livekit_agent_thread.proto package agent From b2f10c4744390c5a87158630f513548992eb27ec Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 8 Oct 2026 20:28:09 +0000 Subject: [PATCH 6/8] generated protobuf --- livekit/livekit_agentdb.pb.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/livekit/livekit_agentdb.pb.go b/livekit/livekit_agentdb.pb.go index af454f016..f286ed14e 100644 --- a/livekit/livekit_agentdb.pb.go +++ b/livekit/livekit_agentdb.pb.go @@ -19,7 +19,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.36.12 -// protoc v7.36.2 +// protoc v7.35.1 // source: livekit_agentdb.proto package livekit From 343cbbf01171415be1ab250b59284db4b1099ffd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9o=20Monnom?= Date: Thu, 8 Oct 2026 13:29:01 -0700 Subject: [PATCH 7/8] thread schema: one column per line, shared and private tables apart --- livekit/agent/thread_schema_v1.sql | 108 +++++++++++++++++++++++------ 1 file changed, 86 insertions(+), 22 deletions(-) diff --git a/livekit/agent/thread_schema_v1.sql b/livekit/agent/thread_schema_v1.sql index 8ed7723a1..34444b0da 100644 --- a/livekit/agent/thread_schema_v1.sql +++ b/livekit/agent/thread_schema_v1.sql @@ -1,27 +1,91 @@ --- The conversation inside a thread's database, version 1. The agents framework --- creates it on a thread's first write, and the platform reads items and tasks. --- items.payload and tasks.history hold --- livekit.agent.ChatContext.ChatItem as protobuf JSON. schema_version, items, --- tasks and lanes.lane are shared: a change to them is additive and ships as a --- new version file. Other tables and lane columns belong to the framework. -CREATE TABLE schema_version (version INTEGER NOT NULL, applied_at INTEGER NOT NULL); -CREATE TABLE items (lane TEXT NOT NULL, id TEXT PRIMARY KEY, seq INTEGER NOT NULL, type TEXT NOT NULL, - message_id TEXT, payload JSONB NOT NULL); +-- Conversation schema, version 1: the tables inside a thread's database. +-- +-- The agents framework creates them on a thread's first write. The platform +-- reads items and tasks. Shared tables change only by adding a new version +-- file. Private tables belong to the framework. +-- +-- items.payload and tasks.history hold livekit.agent.ChatContext.ChatItem as +-- protobuf JSON. + + +-- Shared --------------------------------------------------------------------- + +CREATE TABLE schema_version ( + version INTEGER NOT NULL, + applied_at INTEGER NOT NULL +); + +-- One chat item per row, in each lane's order. +CREATE TABLE items ( + lane TEXT NOT NULL, + id TEXT PRIMARY KEY, + seq INTEGER NOT NULL, + type TEXT NOT NULL, + message_id TEXT, + payload JSONB NOT NULL +); CREATE INDEX items_lane_seq ON items (lane, seq); CREATE UNIQUE INDEX items_lane_message ON items (lane, message_id) WHERE message_id IS NOT NULL; -CREATE TABLE events (seq INTEGER PRIMARY KEY, ts_ms INTEGER NOT NULL, lane TEXT NOT NULL, run_id TEXT NOT NULL, - author TEXT NOT NULL, type TEXT NOT NULL, payload JSONB NOT NULL); + +-- A2A tasks. +CREATE TABLE tasks ( + task_id TEXT PRIMARY KEY, + lane TEXT NOT NULL, + seq INTEGER NOT NULL, + status TEXT NOT NULL, + message_id TEXT, + task JSONB NOT NULL, + agent TEXT NOT NULL, + due_at INTEGER, + deadline INTEGER, + ask JSONB, + status_message JSONB, + history JSONB NOT NULL, + artifacts JSONB, + parent TEXT, + push_configs JSONB, + attempts INTEGER NOT NULL DEFAULT 0, + updated_at INTEGER NOT NULL +); +CREATE INDEX tasks_lane_status_due ON tasks (lane, status, due_at); +CREATE UNIQUE INDEX tasks_lane_message ON tasks (lane, message_id) WHERE message_id IS NOT NULL; + +-- One row per agent that wrote to the thread. Only lane is shared. +CREATE TABLE lanes ( + lane TEXT PRIMARY KEY, + version INTEGER NOT NULL, + state JSONB, + agent_stack JSONB, + lease_holder TEXT, + lease_token TEXT, + lease_until INTEGER NOT NULL DEFAULT 0, + waiter TEXT, + drained_seq INTEGER NOT NULL DEFAULT 0 +); + + +-- Private -------------------------------------------------------------------- + +-- Run journal. +CREATE TABLE events ( + seq INTEGER PRIMARY KEY, + ts_ms INTEGER NOT NULL, + lane TEXT NOT NULL, + run_id TEXT NOT NULL, + author TEXT NOT NULL, + type TEXT NOT NULL, + payload JSONB NOT NULL +); CREATE INDEX events_lane_seq ON events (lane, seq); -CREATE TABLE lanes (lane TEXT PRIMARY KEY, version INTEGER NOT NULL, state JSONB, agent_stack JSONB, - lease_holder TEXT, lease_token TEXT, lease_until INTEGER NOT NULL DEFAULT 0, waiter TEXT, - drained_seq INTEGER NOT NULL DEFAULT 0); -CREATE TABLE inbox (seq INTEGER PRIMARY KEY, lane TEXT NOT NULL, task_id TEXT NOT NULL, message_id TEXT NOT NULL, - message JSONB NOT NULL, received_at INTEGER NOT NULL); + +-- Messages waiting for a busy lane. +CREATE TABLE inbox ( + seq INTEGER PRIMARY KEY, + lane TEXT NOT NULL, + task_id TEXT NOT NULL, + message_id TEXT NOT NULL, + message JSONB NOT NULL, + received_at INTEGER NOT NULL +); CREATE INDEX inbox_lane_seq ON inbox (lane, seq); CREATE UNIQUE INDEX inbox_lane_message ON inbox (lane, message_id); -CREATE TABLE tasks (task_id TEXT PRIMARY KEY, lane TEXT NOT NULL, seq INTEGER NOT NULL, status TEXT NOT NULL, - message_id TEXT, task JSONB NOT NULL, agent TEXT NOT NULL, due_at INTEGER, deadline INTEGER, - ask JSONB, status_message JSONB, history JSONB NOT NULL, artifacts JSONB, parent TEXT, - push_configs JSONB, attempts INTEGER NOT NULL DEFAULT 0, updated_at INTEGER NOT NULL); -CREATE INDEX tasks_lane_status_due ON tasks (lane, status, due_at); -CREATE UNIQUE INDEX tasks_lane_message ON tasks (lane, message_id) WHERE message_id IS NOT NULL; From fad3160988ca831c3c6c6ffaa5227211b06073d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9o=20Monnom?= Date: Thu, 8 Oct 2026 13:36:03 -0700 Subject: [PATCH 8/8] thread schema: plain comments --- livekit/agent/thread_schema_v1.sql | 23 +++++------------------ 1 file changed, 5 insertions(+), 18 deletions(-) diff --git a/livekit/agent/thread_schema_v1.sql b/livekit/agent/thread_schema_v1.sql index 34444b0da..507a0c7f6 100644 --- a/livekit/agent/thread_schema_v1.sql +++ b/livekit/agent/thread_schema_v1.sql @@ -1,21 +1,12 @@ --- Conversation schema, version 1: the tables inside a thread's database. --- --- The agents framework creates them on a thread's first write. The platform --- reads items and tasks. Shared tables change only by adding a new version --- file. Private tables belong to the framework. --- --- items.payload and tasks.history hold livekit.agent.ChatContext.ChatItem as --- protobuf JSON. - - --- Shared --------------------------------------------------------------------- +-- Tables inside a thread's database, created by the agents framework. The +-- platform reads items and tasks. items.payload and tasks.history hold +-- livekit.agent.ChatContext.ChatItem as protobuf JSON. CREATE TABLE schema_version ( version INTEGER NOT NULL, applied_at INTEGER NOT NULL ); --- One chat item per row, in each lane's order. CREATE TABLE items ( lane TEXT NOT NULL, id TEXT PRIMARY KEY, @@ -27,7 +18,6 @@ CREATE TABLE items ( CREATE INDEX items_lane_seq ON items (lane, seq); CREATE UNIQUE INDEX items_lane_message ON items (lane, message_id) WHERE message_id IS NOT NULL; --- A2A tasks. CREATE TABLE tasks ( task_id TEXT PRIMARY KEY, lane TEXT NOT NULL, @@ -50,7 +40,7 @@ CREATE TABLE tasks ( CREATE INDEX tasks_lane_status_due ON tasks (lane, status, due_at); CREATE UNIQUE INDEX tasks_lane_message ON tasks (lane, message_id) WHERE message_id IS NOT NULL; --- One row per agent that wrote to the thread. Only lane is shared. +-- One row per agent. CREATE TABLE lanes ( lane TEXT PRIMARY KEY, version INTEGER NOT NULL, @@ -63,10 +53,8 @@ CREATE TABLE lanes ( drained_seq INTEGER NOT NULL DEFAULT 0 ); +-- The tables below are private to the framework. --- Private -------------------------------------------------------------------- - --- Run journal. CREATE TABLE events ( seq INTEGER PRIMARY KEY, ts_ms INTEGER NOT NULL, @@ -78,7 +66,6 @@ CREATE TABLE events ( ); CREATE INDEX events_lane_seq ON events (lane, seq); --- Messages waiting for a busy lane. CREATE TABLE inbox ( seq INTEGER PRIMARY KEY, lane TEXT NOT NULL,