Skip to content

agent threads: thread messages and grant, agent-db service to gRPC - #1841

Merged
theomonnom merged 58 commits into
mainfrom
theo/agent-threads-api
Oct 9, 2026
Merged

theomonnom merged 58 commits into
mainfrom
theo/agent-threads-api

Conversation

@theomonnom

@theomonnom theomonnom commented Oct 8, 2026 •

Copy link
Copy Markdown
Member
  • agent/livekit_agent_thread.proto, one file:
    • AgentThread: a thread (AT_) with scope, attributes, idle TTL and the database created with it; create, get, list, update, delete; items as ChatContext.ChatItem, oldest first
    • AgentTask: background work an agent runs in a thread (name, status: running, completed, failed, canceled); all of a thread's, optionally by status
  • livekit/agent/thread_schema_v1.sql (embedded as agent.ThreadSchemaV1): the conversation tables the agents framework writes and the platform reads
  • auth.AgentThreadGrant (agentThread claim): scope, threadIds, all, list/create/write/read/delete; CanCreate(scope) and CanList(scope) by scope, CanRead, CanWrite, CanDelete(threadID) by id (e.g. a frontend rendering one conversation); all extends it to every thread of the project with the action flags still applying, and CanUpdate() needs it
  • guid prefixes AT_ (thread) and AST_ (agent stream)
  • AgentDBService Twirp removed (served as gRPC from cloud-protocol); AgentDB.CreateRequest.region removed; DumpRequest/DumpResponse renamed ExportRequest/ExportResponse; times are Timestamp / Duration
  • AgentDB batches answer per statement: statement index on Columns, ColumnBatch and ExecResult

AgentThread messages for the durable conversation an agent has with a
person, AgentThreadGrant under the agentThread claim, and the AT_ and AST_
id prefixes.

The agent-db and thread services are internal gRPC in cloud-protocol, so the
AgentDBService Twirp service is removed. A database lives in its project's
data region: AgentDB.CreateRequest.region is reserved.
@changeset-bot

changeset-bot Bot commented Oct 8, 2026 •

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 18e535d

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 2 packages
Name Type
github.com/livekit/protocol Minor
@livekit/protocol Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

github-actions Bot and others added 7 commits October 8, 2026 00:04
Keys stay readable in logs, which is what debugging a lookup needs.
One agent can expose several endpoints (sms, chat, voice). Endpoints names
the ones the holder may call; empty allows all. Threads stay per agent, so
every endpoint continues the same thread.
Comment thread auth/grants.go Outdated
// endpoints the holder may call; empty allows all of them.
type AgentThreadGrant struct {
AgentName string `json:"agentName"`
Sub string `json:"sub,omitempty"`

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this Subject? or Subscription?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It’s Subject, the idea is that you can “group” some threads based on whatever key you want.
This is just there for very simple use cases like: list all the threads from my user (and in this case user is in sub)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will rename to Subject

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should this be a user identity or some sort? since we use identity in RTC land

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah i can use Identity, but the thing is that it isn’t necessarily a “Human” ?

@theomonnom theomonnom Oct 8, 2026 •

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It’s also used in the scope. You can give a JWT token to the client side. With this Subject. And it can read/list the threads.

But I expect most ppl to write their own backend when their usecase becomes more advanced

Comment thread auth/grants.go Outdated
// or by id, with per-action flags. Endpoints limits which of the agent's
// endpoints the holder may call; empty allows all of them.
type AgentThreadGrant struct {
AgentName string `json:"agentName"`

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should this be here? is a thread a piece of data? or is it tied to a specific agent?

it's worth keeping in mind that I may have two separate agents that is capable of picking up the same convo.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good question, so you can have multiple agents picking up the same conversation. But they must share the same agent_name.

remember that with text mode, an agent is agent_name/{endpoint}

Comment thread auth/grants.go Outdated
AgentName string `json:"agentName"`
Sub string `json:"sub,omitempty"`
ThreadIDs []string `json:"threadIds,omitempty"`
Endpoints []string `json:"endpoints,omitempty"`

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

how does this mesh with agentEndpoint grant? what is the relation here?

// When set, create is get-or-create on (agent_name, key).
string key = 2;
// The subject the thread belongs to, as scoped by an AgentThreadGrant.
string sub = 3 [(logger.sensitivity) = SENSITIVITY_PII];

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

does the end user provide subject? or is it something that we generate? how is this determined?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The end user provide it

message DeleteResponse {}

// Creates the thread's database if it has none. Idempotent.
message EnsureStateDatabaseRequest {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not sure about naming.. maybe

Suggested change
message EnsureStateDatabaseRequest {
message CreateThreadDatabaseRequest {

}

// Marks a thread as merged into redirect_to.
message RedirectRequest {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
message RedirectRequest {
message MergeThreadRequest {

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed


message Thread {
string thread_id = 1 [(logger.name) = "threadID"];
string agent_name = 2;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same as earlier comment.. might not want to sticky to agent

map<string, string> attributes = 5 [(logger.sensitivity) = SENSITIVITY_PII];
int64 created_at = 6;
int64 last_active = 7;
int64 expires_at = 8;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

how does expires_at play with idle_ttl_seconds?

what is the unit for time?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will use google.protobuf.Duration/Timestamp

expires_at = last_active + idle_ttl. It's a sliding expiry: every touch or send pushes it out by the idle TTL, and the thread is swept once it passes.

Comment thread protobufs/livekit_agentdb.proto Outdated
message AgentDB {
message CreateRequest {
string region = 1;
// A database lives in its project's data region.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

…e and MergeThread

From review: sub is spelled subject (grant and messages), times are
google.protobuf.Timestamp/Duration (threads and the AgentDB lifecycle),
EnsureStateDatabase is CreateThreadDatabase, Redirect is MergeThread with
merged_into on the thread.
theomonnom added a commit that referenced this pull request Oct 8, 2026
github-actions Bot and others added 2 commits October 8, 2026 03:25
The thread grant covers thread data only, by scope or id across the
project's agents: no agent name and no endpoints, so it never opens a
non-public endpoint (agentEndpoint does that). Send is write. The thread's
subject is its scope.
Comment thread auth/grants.go Outdated

// Allows reports whether a thread is in the grant, by scope or by id. An empty
// Scope or thread id never matches; the caller checks the flags.
func (s *AgentThreadGrant) Allows(scope, threadID string) bool {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

grant.CanRead(threadID)
grant.CanList(scope)
grant.CanDelete(threadID)
grant.CanWrite(threadID)

One check per action. Per-thread checks take the thread, since a grant
matches by scope or by id; create needs only the grant, which creates in
its own scope.
theomonnom and others added 6 commits October 7, 2026 21:23
A conversation is not owned by one agent: any agent of the project can pick
it up. Keys resolve per project, listing is by scope, and GetRequest looks up
by thread id or key.
… touch

Keys are internal (LiveKit phone numbers): one per thread, set at create.
Removes AddKey, RemoveKey, Touch and CreateThreadDatabase; an update counts
as activity and the database comes with the thread.
theomonnom and others added 26 commits October 8, 2026 14:34
A Batch answers one result per statement, in order: Columns and
ColumnBatch frames for a statement that returns rows, one ExecResult for
one that does not, then a single Done for the whole batch. The new
statement field on Columns, ColumnBatch and ExecResult is the 0-based
index within the batch; a plain Exec or Query leaves it 0.

(cherry picked from commit 39517ac)
@theomonnom
theomonnom merged commit 0740380 into main Oct 9, 2026
5 checks passed
@theomonnom
theomonnom deleted the theo/agent-threads-api branch October 9, 2026 20:11
@github-actions github-actions Bot mentioned this pull request Oct 9, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants