Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
58 commits
Select commit Hold shift + click to select a range
16b193b
agent threads: thread messages and grant; agent-db service moves to gRPC
theomonnom Oct 8, 2026
3b58124
generated protobuf
github-actions[bot] Oct 8, 2026
45962ce
agent threads: shorter comments, keys are not PII
theomonnom Oct 8, 2026
baf0858
agent threads: explain AgentThread and AgentDB at the top of their pr…
theomonnom Oct 8, 2026
7f11c3a
generated protobuf
github-actions[bot] Oct 8, 2026
6961016
agent threads: tighter proto headers
theomonnom Oct 8, 2026
b258754
generated protobuf
github-actions[bot] Oct 8, 2026
5e166e4
auth: AgentThreadGrant can limit the agent's endpoints
theomonnom Oct 8, 2026
ed2de68
agent threads: subject, Timestamp/Duration times, CreateThreadDatabas…
theomonnom Oct 8, 2026
f3a28aa
generated protobuf
github-actions[bot] Oct 8, 2026
6448cf4
agent threads: the grant is scope, ids and flags; subject is scope
theomonnom Oct 8, 2026
17ef238
generated protobuf
github-actions[bot] Oct 8, 2026
2912148
auth: AgentThreadGrant.Allows, like AgentEndpointGrant.Allows
theomonnom Oct 8, 2026
5e6b136
auth: AgentThreadGrant.CanCreate, CanList, CanRead, CanWrite, CanDelete
theomonnom Oct 8, 2026
f46c27b
agent threads: no agent_name; keys and listing are per project
theomonnom Oct 8, 2026
7864993
generated protobuf
github-actions[bot] Oct 8, 2026
2345bef
agent threads: one internal key, database created with the thread, no…
theomonnom Oct 8, 2026
c4e191c
generated protobuf
github-actions[bot] Oct 8, 2026
cc88011
agent thread: header matches the single internal key
theomonnom Oct 8, 2026
07d6ab2
generated protobuf
github-actions[bot] Oct 8, 2026
fe85b60
agent thread: no merge for now
theomonnom Oct 8, 2026
fb69b0b
generated protobuf
github-actions[bot] Oct 8, 2026
3c6aea9
agent thread, agentdb: no reserved fields, nothing shipped yet
theomonnom Oct 8, 2026
abbc6ef
generated protobuf
github-actions[bot] Oct 8, 2026
145381d
auth: thread grant reads, writes and deletes by id
theomonnom Oct 8, 2026
0c5f896
agent thread: history, AgentTask and the shared conversation schema
theomonnom Oct 8, 2026
75b36ff
generated protobuf
github-actions[bot] Oct 8, 2026
1de6dc2
agent task: background work, four statuses
theomonnom Oct 8, 2026
5719fb0
generated protobuf
github-actions[bot] Oct 8, 2026
22dafe5
agent thread: no conversation schema in protocol
theomonnom Oct 8, 2026
d7679dd
Revert "agent thread: no conversation schema in protocol"
theomonnom Oct 8, 2026
b696977
generated protobuf
github-actions[bot] Oct 8, 2026
e24af55
Merge branch 'theo/agent-threads-api' of https://github.com/livekit/p…
theomonnom Oct 8, 2026
74fdf84
thread schema: tasks keep queried columns, A2A fields in data, framew…
theomonnom Oct 8, 2026
72224f5
thread schema: say where each table is used
theomonnom Oct 8, 2026
6e1a630
thread schema: wrap comments
theomonnom Oct 8, 2026
c3070f2
thread schema: lane is agent, indexes for every query, no applied_at …
theomonnom Oct 8, 2026
a139187
thread schema: no task attempts, status says a run was lost
theomonnom Oct 8, 2026
44e0aa9
agent thread: get by id only
theomonnom Oct 8, 2026
5f7279e
agent thread: no key in the public messages; create returns the thread
theomonnom Oct 8, 2026
0a02ce5
generated protobuf
github-actions[bot] Oct 8, 2026
6de4de2
agent thread: GetItems and GetTasks requests
theomonnom Oct 8, 2026
e223b92
generated protobuf
github-actions[bot] Oct 8, 2026
dab43d9
thread schema: keep the framework's lane naming
theomonnom Oct 8, 2026
f624709
thread schema: every column names its reader; tasks.internal; log ids
theomonnom Oct 9, 2026
38d1b27
agent task: no agent field, no paging
theomonnom Oct 9, 2026
f965030
generated protobuf
github-actions[bot] Oct 9, 2026
aca3f2c
agentdb: Export instead of Dump
theomonnom Oct 9, 2026
1e517dc
generated protobuf
github-actions[bot] Oct 9, 2026
592b221
agent: tasks.status values and their AgentTask.Status live here
theomonnom Oct 9, 2026
f7ccd2b
agent task: zero status is unspecified, so running shows in JSON
theomonnom Oct 9, 2026
2ad11c8
generated protobuf
github-actions[bot] Oct 9, 2026
85b4c1c
agent task: status doc
theomonnom Oct 9, 2026
ece8415
Merge branch 'theo/agent-threads-api' of https://github.com/livekit/p…
theomonnom Oct 9, 2026
01c6062
auth: AgentThreadGrant.All for project-wide thread access
theomonnom Oct 9, 2026
008dfd7
agentdb: tag batch results with their statement index
theomonnom Oct 6, 2026
089df37
changeset: per-statement batch results
theomonnom Oct 9, 2026
18e535d
generated protobuf
github-actions[bot] Oct 9, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .changeset/agent-threads.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
"github.com/livekit/protocol": minor
"@livekit/protocol": minor
---

Agent threads, the durable conversation an agent has with a person across channels (`agent/livekit_agent_thread.proto`): `AgentThread` (the thread, its items as `ChatContext.ChatItem`) and `AgentTask` (background work an agent runs in it); the conversation tables in `livekit/agent/thread_schema_v1.sql`; `auth.AgentThreadGrant` under the `agentThread` claim (list and create by scope, read, write and delete by thread id, or every thread of the project with `all`); the `AT_` (thread) and `AST_` (agent stream) id prefixes in `utils/guid`.

The agent-db and thread services are internal gRPC in cloud-protocol, so `AgentDBService` and its Twirp code are removed; the public API is public-api-server. A database lives in its project's data region: `AgentDB.CreateRequest.region` is removed, `DumpRequest`/`DumpResponse` are now `ExportRequest`/`ExportResponse`, and AgentDB times are `google.protobuf.Timestamp` / `Duration`. A batch answers one result per statement, tagged with its 0-based `statement` index on `Columns`, `ColumnBatch` and `ExecResult`.
5 changes: 5 additions & 0 deletions auth/accesstoken.go
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,11 @@ func (t *AccessToken) SetAgentEndpointGrant(grant *AgentEndpointGrant) *AccessTo
return t
}

func (t *AccessToken) SetAgentThreadGrant(grant *AgentThreadGrant) *AccessToken {
t.grant.AgentThread = grant
return t
}

func (t *AccessToken) SetMetadata(md string) *AccessToken {
t.grant.Metadata = md
return t
Expand Down
24 changes: 24 additions & 0 deletions auth/accesstoken_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,30 @@ func TestAgentEndpointGrantRoundTrip(t *testing.T) {
require.Equal(t, grant, decoded.AgentEndpoint)
}

func TestAgentThreadGrantRoundTrip(t *testing.T) {
t.Parallel()

apiKey, secret := apiKeypair()
grant := &AgentThreadGrant{Scope: "user-1", ThreadIDs: []string{"AT_1"}, Read: true, Write: true}
raw, err := NewAccessToken(apiKey, secret).
SetAgentThreadGrant(grant).
SetValidFor(time.Minute).
ToJWT()
require.NoError(t, err)

// the claim keys are camelCase
claims := decodeClaims(t, raw)
require.Contains(t, claims, `"agentThread"`)
require.Contains(t, claims, `"scope"`)
require.Contains(t, claims, `"threadIds"`)

v, err := ParseAPIToken(raw)
require.NoError(t, err)
_, decoded, err := v.Verify(secret)
require.NoError(t, err)
require.Equal(t, grant, decoded.AgentThread)
}

func decodeClaims(t *testing.T, raw string) string {
t.Helper()
parts := strings.Split(raw, ".")
Expand Down
90 changes: 90 additions & 0 deletions auth/grants.go
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,7 @@ type ClaimGrants struct {
Inference *InferenceGrant `json:"inference,omitempty"`
Observability *ObservabilityGrant `json:"observability,omitempty"`
AgentEndpoint *AgentEndpointGrant `json:"agentEndpoint,omitempty"`
AgentThread *AgentThreadGrant `json:"agentThread,omitempty"`
// Room configuration to use if this participant initiates the room
RoomConfig *RoomConfiguration `json:"roomConfig,omitempty"`
// Cloud-only, config preset to use
Expand Down Expand Up @@ -216,6 +217,7 @@ func (c *ClaimGrants) Clone() *ClaimGrants {
clone.Inference = c.Inference.Clone()
clone.Observability = c.Observability.Clone()
clone.AgentEndpoint = c.AgentEndpoint.Clone()
clone.AgentThread = c.AgentThread.Clone()
clone.Attributes = maps.Clone(c.Attributes)
clone.RoomConfig = c.RoomConfig.Clone()
if len(c.KindDetails) > 0 {
Expand All @@ -239,6 +241,7 @@ func (c *ClaimGrants) MarshalLogObject(e zapcore.ObjectEncoder) error {
e.AddObject("Inference", c.Inference)
e.AddObject("Observability", c.Observability)
e.AddObject("AgentEndpoint", c.AgentEndpoint)
e.AddObject("AgentThread", c.AgentThread)
e.AddObject("RoomConfig", logger.Proto((*livekit.RoomConfiguration)(c.RoomConfig)))
e.AddString("RoomPreset", c.RoomPreset)
return nil
Expand Down Expand Up @@ -712,6 +715,93 @@ func (s *AgentEndpointGrant) MarshalLogObject(e zapcore.ObjectEncoder) error {

// ------------------------------------------------------------------

// AgentThreadGrant gives a token access to threads, across the project's
// agents: list and create within Scope, and read, write and delete the threads
// in ThreadIDs. All extends it to every thread and scope of the project; the
// action flags still apply. It grants thread data only; which endpoints a token
// may call is agentEndpoint's.
type AgentThreadGrant struct {
All bool `json:"all,omitempty"`
Scope string `json:"scope,omitempty"`
ThreadIDs []string `json:"threadIds,omitempty"`
List bool `json:"list,omitempty"`
Create bool `json:"create,omitempty"`
Write bool `json:"write,omitempty"`
Read bool `json:"read,omitempty"`
Delete bool `json:"delete,omitempty"`
}

// CanCreate reports whether the holder may create threads in scope. A scoped
// grant creates only in its own scope.
func (s *AgentThreadGrant) CanCreate(scope string) bool {
return s != nil && s.Create && s.inScope(scope)
}

// CanList reports whether the holder may list the threads of scope.
func (s *AgentThreadGrant) CanList(scope string) bool {
return s != nil && s.List && s.inScope(scope)
}

// CanUpdate reports whether the holder may change any thread's attributes and
// idle TTL, which only a project-wide grant may.
func (s *AgentThreadGrant) CanUpdate() bool {
return s != nil && s.All && s.Write
}

// CanRead reports whether the holder may read threadID.
func (s *AgentThreadGrant) CanRead(threadID string) bool {
return s != nil && s.Read && s.includes(threadID)
}

// CanWrite reports whether the holder may send to threadID.
func (s *AgentThreadGrant) CanWrite(threadID string) bool {
return s != nil && s.Write && s.includes(threadID)
}

// CanDelete reports whether the holder may delete threadID.
func (s *AgentThreadGrant) CanDelete(threadID string) bool {
return s != nil && s.Delete && s.includes(threadID)
}

func (s *AgentThreadGrant) inScope(scope string) bool {
return s.All || (s.Scope != "" && s.Scope == scope)
}

func (s *AgentThreadGrant) includes(threadID string) bool {
return threadID != "" && (s.All || slices.Contains(s.ThreadIDs, threadID))
}

func (s *AgentThreadGrant) Clone() *AgentThreadGrant {
if s == nil {
return nil
}

clone := *s
if len(s.ThreadIDs) > 0 {
clone.ThreadIDs = append([]string{}, s.ThreadIDs...)
}

return &clone
}

func (s *AgentThreadGrant) MarshalLogObject(e zapcore.ObjectEncoder) error {
if s == nil {
return nil
}

e.AddBool("All", s.All)
e.AddString("Scope", s.Scope)
zap.Strings("ThreadIDs", s.ThreadIDs).AddTo(e)
e.AddBool("List", s.List)
e.AddBool("Create", s.Create)
e.AddBool("Write", s.Write)
e.AddBool("Read", s.Read)
e.AddBool("Delete", s.Delete)
return nil
}

// ------------------------------------------------------------------

func sourceToString(source livekit.TrackSource) string {
return strings.ToLower(source.String())
}
Expand Down
56 changes: 56 additions & 0 deletions auth/grants_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ func TestGrants(t *testing.T) {
require.Same(t, grants.Inference, clone.Inference)
require.Same(t, grants.SIP, clone.SIP)
require.Same(t, grants.AgentEndpoint, clone.AgentEndpoint)
require.Same(t, grants.AgentThread, clone.AgentThread)
require.True(t, reflect.DeepEqual(grants, clone))
require.True(t, reflect.DeepEqual(grants.Video, clone.Video))
})
Expand Down Expand Up @@ -65,6 +66,9 @@ func TestGrants(t *testing.T) {
// require AgentEndpoint
require.Same(t, grants.AgentEndpoint, clone.AgentEndpoint)
require.True(t, reflect.DeepEqual(grants.AgentEndpoint, clone.AgentEndpoint))
// require AgentThread
require.Same(t, grants.AgentThread, clone.AgentThread)
require.True(t, reflect.DeepEqual(grants.AgentThread, clone.AgentThread))
})

t.Run("clone with video", func(t *testing.T) {
Expand Down Expand Up @@ -524,3 +528,55 @@ func TestAgentEndpointGrantCloneIndependent(t *testing.T) {
require.True(t, grants.AgentEndpoint.Call)
require.Equal(t, "a", grants.AgentEndpoint.AgentName)
}

func TestAgentThreadGrantChecks(t *testing.T) {
t.Parallel()

// Scope covers list and create only; read, write and delete need the id.
g := &AgentThreadGrant{Scope: "u1", ThreadIDs: []string{"AT_1"}, List: true, Create: true, Read: true}
require.True(t, g.CanRead("AT_1"))
require.False(t, g.CanRead("AT_2"))
require.False(t, g.CanRead(""))
require.False(t, g.CanWrite("AT_1"))
require.False(t, g.CanDelete("AT_1"))
require.True(t, g.CanList("u1"))
require.False(t, g.CanList("u2"))
require.False(t, g.CanList(""))
require.True(t, g.CanCreate("u1"))
require.False(t, g.CanCreate("u2"))
require.False(t, g.CanUpdate())

idsOnly := &AgentThreadGrant{ThreadIDs: []string{"AT_4"}, Create: true, Write: true}
require.True(t, idsOnly.CanWrite("AT_4"))
require.False(t, idsOnly.CanCreate(""))

// All covers every thread and scope, still gated by the action flags.
all := &AgentThreadGrant{All: true, Read: true, List: true}
require.True(t, all.CanRead("AT_9"))
require.True(t, all.CanList("any"))
require.False(t, all.CanRead(""))
require.False(t, all.CanDelete("AT_9"))
require.False(t, all.CanCreate("any"))
require.False(t, all.CanUpdate())
require.True(t, (&AgentThreadGrant{All: true, Write: true}).CanUpdate())

var none *AgentThreadGrant
require.False(t, none.CanRead("AT_1"))
require.False(t, none.CanCreate("u1"))
require.False(t, none.CanUpdate())
}

func TestAgentThreadGrantCloneIndependent(t *testing.T) {
t.Parallel()

grants := &ClaimGrants{AgentThread: &AgentThreadGrant{Scope: "u1", ThreadIDs: []string{"AT_1"}, Write: true}}
clone := grants.Clone()
require.NotSame(t, grants.AgentThread, clone.AgentThread)
require.True(t, reflect.DeepEqual(grants.AgentThread, clone.AgentThread))

clone.AgentThread.Write = false
clone.AgentThread.ThreadIDs[0] = "AT_2"
clone.AgentThread.ThreadIDs = append(clone.AgentThread.ThreadIDs, "AT_3")
require.True(t, grants.AgentThread.Write)
require.Equal(t, []string{"AT_1"}, grants.AgentThread.ThreadIDs)
}
Loading
Loading