Skip to content

Commit 4d9c36e

Browse files
samwilliscursoragentKyleAMathews
authored
feat(agents) use a TanStack DB union query for the timeline (#4347)
## Summary - Adds `createEntityTimelineQuery`, a multi-source TanStack DB timeline query for agent streams. - Migrates the agents UI timeline to consume the query directly, with live child collections for run items so streamed text updates through DB's fine-grained reactivity instead of rematerializing the whole chat timeline. - Adds stable timeline ordering for streamed and optimistic rows, improves pending queued-message placement, and keeps the chat pinned to the bottom during streaming. - Adds a patch changeset for `@electric-ax/agents-runtime` and `@electric-ax/agents-server-ui`. ## Dependencies This PR depends on TanStack DB support from: - TanStack/db#1536 - TanStack/db#1537 ## Test plan - `pnpm --filter @electric-ax/agents-runtime typecheck` - `pnpm --filter @electric-ax/agents-runtime build` - `pnpm --filter @electric-ax/agents-server-ui typecheck` - Manual browser smoke testing of sending messages and streamed agent responses in the agents UI Made with [Cursor](https://cursor.com) --------- Co-authored-by: Cursor <cursoragent@cursor.com> Co-authored-by: Kyle Mathews <mathews.kyle@gmail.com>
1 parent da26799 commit 4d9c36e

21 files changed

Lines changed: 2066 additions & 761 deletions
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
---
2+
'@electric-ax/agents-runtime': patch
3+
'@electric-ax/agents-mobile': patch
4+
'@electric-ax/agents-server-conformance-tests': patch
5+
'@electric-ax/agents-server-ui': patch
6+
---
7+
8+
Add a fine-grained reactive entity timeline query and migrate the agents UI to use it. Timeline rows are maintained by TanStack DB using multi-source queries and live child collections, so streamed agent responses update incrementally without rematerializing the whole chat timeline. Update the mobile app to consume the row-based timeline shape and pin React to the React Native renderer version. Keep the conformance property-test model aligned with generated entity type names.

‎packages/agents-mobile/package.json‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,8 @@
2727
"expo-router": "~6.0.23",
2828
"expo-status-bar": "~3.0.9",
2929
"expo-web-browser": "~15.0.11",
30-
"react": "^19.1.0",
31-
"react-dom": "^19.1.0",
30+
"react": "19.1.0",
31+
"react-dom": "19.1.0",
3232
"react-native": "0.81.5",
3333
"react-native-gesture-handler": "2.28.0",
3434
"react-native-random-uuid": "^0.1.4",
@@ -40,7 +40,7 @@
4040
"zod": "^4.4.3"
4141
},
4242
"devDependencies": {
43-
"@types/react": "^19.1.17",
43+
"@types/react": "19.1.17",
4444
"@vitest/coverage-v8": "^4.1.0",
4545
"babel-preset-expo": "~54.0.10",
4646
"typescript": "^5.9.3",

‎packages/agents-mobile/src/screens/SessionScreen.tsx‎

Lines changed: 12 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import {
2525
readTextPayload,
2626
} from '@electric-ax/agents-server-ui/src/lib/sendMessage'
2727
import type { OptimisticInboxMessage } from '@electric-ax/agents-server-ui/src/lib/sendMessage'
28+
import type { EntityTimelineQueryRow } from '@electric-ax/agents-runtime/client'
2829
import { Header, HeaderBackButton } from '../components/Header'
2930
import { Icon } from '../components/Icon'
3031
import { Screen } from '../components/Screen'
@@ -50,7 +51,6 @@ const INLINE_QUEUED_TIMEOUT_MS = 15_000
5051
type EntityStreamState = ReturnType<typeof useEntityTimeline>
5152
type EntityStreamDB = EntityStreamState[`db`]
5253
type PendingInboxMessage = EntityStreamState[`pendingInbox`][number]
53-
type TimelineEntry = EntityStreamState[`entries`][number]
5454

5555
export function ChatSessionScreen({
5656
entityUrl,
@@ -161,24 +161,26 @@ export function SessionScreen({
161161
const entity = matches.at(0) ?? null
162162
const streamEntityUrl =
163163
view === `chat` && entity?.status !== `spawning` ? entityUrl : null
164-
const { entries, pendingInbox, db } = useEntityTimeline(
164+
const { timelineRows, pendingInbox, db } = useEntityTimeline(
165165
serverUrl,
166166
streamEntityUrl
167167
)
168168
const manifests = useMemo(
169169
() =>
170-
entries
171-
.filter(isManifestEntry)
172-
.map((entry) => entry.section.manifest as ManifestRecord),
173-
[entries]
170+
timelineRows
171+
.filter((row): row is EntityTimelineQueryRow & { manifest: object } =>
172+
Boolean(row.manifest)
173+
)
174+
.map((row) => row.manifest as ManifestRecord),
175+
[timelineRows]
174176
)
175177
const processedInboxKeySignature = useMemo(
176178
() =>
177-
entries
178-
.filter((entry) => entry.section.kind === `user_message`)
179-
.map((entry) => entry.key.replace(/^inbox:/, ``))
179+
timelineRows
180+
.filter((row) => row.inbox)
181+
.map((row) => row.inbox!.key)
180182
.join(`\0`),
181-
[entries]
183+
[timelineRows]
182184
)
183185
const processedInboxKeys = useMemo(
184186
() =>
@@ -633,12 +635,6 @@ type DrawerEntity = Pick<
633635

634636
type ManifestRecord = Record<string, unknown>
635637

636-
function isManifestEntry(entry: TimelineEntry): entry is TimelineEntry & {
637-
section: { kind: `manifest`; manifest: unknown }
638-
} {
639-
return entry.section.kind === `manifest`
640-
}
641-
642638
type DrawerEntry = {
643639
key: string
644640
groupKey: string

‎packages/agents-runtime/package.json‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -79,16 +79,16 @@
7979
}
8080
},
8181
"dependencies": {
82-
"@electric-ax/agents-mcp": "workspace:*",
8382
"@anthropic-ai/sdk": "^0.78.0",
8483
"@durable-streams/client": "https://pkg.pr.new/durable-streams/durable-streams/@durable-streams/client@5d5c217",
8584
"@durable-streams/state": "https://pkg.pr.new/durable-streams/durable-streams/@durable-streams/state@5d5c217",
85+
"@electric-ax/agents-mcp": "workspace:*",
8686
"@mariozechner/pi-agent-core": "^0.70.2",
8787
"@mariozechner/pi-ai": "^0.70.2",
8888
"@mozilla/readability": "^0.6.0",
8989
"@sinclair/typebox": "^0.34.48",
9090
"@standard-schema/spec": "^1.1.0",
91-
"@tanstack/db": "^0.6.4",
91+
"@tanstack/db": "^0.6.6",
9292
"cron-parser": "^5.5.0",
9393
"diff": "^9.0.0",
9494
"jsdom": "^28.1.0",

‎packages/agents-runtime/src/client.ts‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,10 @@ export { createEntityStreamDB } from './entity-stream-db'
22
export { createAgentsClient } from './agents-client'
33
export {
44
compareTimelineOrders,
5+
createPendingTimelineOrder,
56
createEntityErrorsQuery,
67
createEntityIncludesQuery,
8+
createEntityTimelineQuery,
79
getEntityState,
810
normalizeEntityTimelineData,
911
normalizeTimelineEntities,
@@ -30,8 +32,16 @@ export type {
3032
export type {
3133
EntityTimelineContentItem,
3234
EntityTimelineData,
35+
EntityTimelineInboxMode,
36+
EntityTimelineQueryOptions,
37+
EntityTimelineQueryRow,
38+
EntityTimelineRunRow,
39+
EntityTimelineRunItem,
3340
EntityTimelineSection,
3441
EntityTimelineState,
42+
EntityTimelineTextChunk,
43+
EntityTimelineTextItem,
44+
EntityTimelineToolCallItem,
3545
IncludesEntity,
3646
IncludesInboxMessage,
3747
IncludesRun,

‎packages/agents-runtime/src/entity-schema.ts‎

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ type SequencedPersistedRow<T extends { key?: string | undefined }> = Omit<
4343
> & {
4444
key: string
4545
_seq?: number
46+
_timeline_order?: string
4647
}
4748
type Schema<T> = z.ZodType<T>
4849
type ChildEntityStatusValue = `spawning` | `running` | `idle` | `stopped`
@@ -269,6 +270,10 @@ function createJsonObjectSchema(): Schema<Record<string, JsonValue>> {
269270
>
270271
}
271272

273+
const timelineOrderField = {
274+
_timeline_order: z.string().optional(),
275+
}
276+
272277
function createChildEntityStatusSchema(): Schema<ChildEntityStatusValue> {
273278
return z.enum([`spawning`, `running`, `idle`, `stopped`])
274279
}
@@ -319,6 +324,7 @@ function createWakeConfigSchema(): Schema<WakeConfigValue> {
319324
function createRunSchema(): Schema<RunValue> {
320325
return z.object({
321326
key: z.string().optional(),
327+
...timelineOrderField,
322328
status: z.enum([`started`, `completed`, `failed`]),
323329
finish_reason: z.string().optional(),
324330
})
@@ -327,6 +333,7 @@ function createRunSchema(): Schema<RunValue> {
327333
function createStepSchema(): Schema<StepValue> {
328334
return z.object({
329335
key: z.string().optional(),
336+
...timelineOrderField,
330337
run_id: z.string().optional(),
331338
step_number: z.number().int(),
332339
status: z.enum([`started`, `completed`]),
@@ -340,6 +347,7 @@ function createStepSchema(): Schema<StepValue> {
340347
function createTextSchema(): Schema<TextValue> {
341348
return z.object({
342349
key: z.string().optional(),
350+
...timelineOrderField,
343351
run_id: z.string().optional(),
344352
status: z.enum([`streaming`, `completed`]),
345353
})
@@ -348,6 +356,7 @@ function createTextSchema(): Schema<TextValue> {
348356
function createTextDeltaSchema(): Schema<TextDeltaValue> {
349357
return z.object({
350358
key: z.string().optional(),
359+
...timelineOrderField,
351360
text_id: z.string(),
352361
run_id: z.string(),
353362
delta: z.string(),
@@ -357,6 +366,7 @@ function createTextDeltaSchema(): Schema<TextDeltaValue> {
357366
function createToolCallSchema(): Schema<ToolCallValue> {
358367
return z.object({
359368
key: z.string().optional(),
369+
...timelineOrderField,
360370
run_id: z.string().optional(),
361371
tool_call_id: z.string().optional(),
362372
tool_name: z.string(),
@@ -377,13 +387,15 @@ function createToolCallSchema(): Schema<ToolCallValue> {
377387
function createReasoningSchema(): Schema<ReasoningValue> {
378388
return z.object({
379389
key: z.string().optional(),
390+
...timelineOrderField,
380391
status: z.enum([`streaming`, `completed`]),
381392
})
382393
}
383394

384395
function createErrorEventSchema(): Schema<ErrorEventValue> {
385396
return z.object({
386397
key: z.string().optional(),
398+
...timelineOrderField,
387399
error_code: z.string(),
388400
message: z.string(),
389401
run_id: z.string().optional(),
@@ -395,6 +407,7 @@ function createErrorEventSchema(): Schema<ErrorEventValue> {
395407
function createMessageReceivedSchema(): Schema<MessageReceivedValue> {
396408
return z.object({
397409
key: z.string().optional(),
410+
...timelineOrderField,
398411
from: z.string().optional(),
399412
payload: z.unknown().optional(),
400413
timestamp: z.string().optional(),
@@ -410,6 +423,7 @@ function createMessageReceivedSchema(): Schema<MessageReceivedValue> {
410423
function createWakeSchema(): Schema<WakeEntryValue> {
411424
return z.object({
412425
key: z.string().optional(),
426+
...timelineOrderField,
413427
timestamp: z.string(),
414428
source: z.string(),
415429
timeout: z.boolean(),
@@ -422,6 +436,7 @@ function createWakeSchema(): Schema<WakeEntryValue> {
422436
function createEntityCreatedSchema(): Schema<EntityCreatedValue> {
423437
return z.object({
424438
key: z.string().optional(),
439+
...timelineOrderField,
425440
entity_type: z.string(),
426441
timestamp: z.string(),
427442
args: createJsonObjectSchema(),
@@ -432,6 +447,7 @@ function createEntityCreatedSchema(): Schema<EntityCreatedValue> {
432447
function createEntityStoppedSchema(): Schema<EntityStoppedValue> {
433448
return z.object({
434449
key: z.string().optional(),
450+
...timelineOrderField,
435451
timestamp: z.string(),
436452
reason: z.string().optional(),
437453
})
@@ -440,6 +456,7 @@ function createEntityStoppedSchema(): Schema<EntityStoppedValue> {
440456
function createChildStatusSchema(): Schema<ChildStatusEntryValue> {
441457
return z.object({
442458
key: z.string().optional(),
459+
...timelineOrderField,
443460
entity_url: z.string(),
444461
entity_type: z.string(),
445462
status: createChildEntityStatusSchema(),
@@ -449,12 +466,14 @@ function createChildStatusSchema(): Schema<ChildStatusEntryValue> {
449466
function createTagEntrySchema(): Schema<TagEntryValue> {
450467
return z.object({
451468
key: z.string().optional(),
469+
...timelineOrderField,
452470
value: z.string(),
453471
})
454472
}
455473
function createContextInsertedSchema(): Schema<ContextInsertedValue> {
456474
return z.object({
457475
key: z.string().optional(),
476+
...timelineOrderField,
458477
id: z.string(),
459478
name: z.string(),
460479
attrs: z.record(z.string(), z.union([z.string(), z.number(), z.boolean()])),
@@ -466,6 +485,7 @@ function createContextInsertedSchema(): Schema<ContextInsertedValue> {
466485
function createContextRemovedSchema(): Schema<ContextRemovedValue> {
467486
return z.object({
468487
key: z.string().optional(),
488+
...timelineOrderField,
469489
id: z.string(),
470490
name: z.string(),
471491
timestamp: z.string(),
@@ -483,6 +503,7 @@ function createManifestSchema(): Schema<
483503
return z.union([
484504
z.object({
485505
key: z.string().optional(),
506+
...timelineOrderField,
486507
kind: z.literal(`child`),
487508
id: z.string(),
488509
entity_type: z.string(),
@@ -492,6 +513,7 @@ function createManifestSchema(): Schema<
492513
}),
493514
z.object({
494515
key: z.string().optional(),
516+
...timelineOrderField,
495517
kind: z.literal(`source`),
496518
sourceType: z.string(),
497519
sourceRef: z.string(),
@@ -500,6 +522,7 @@ function createManifestSchema(): Schema<
500522
}),
501523
z.object({
502524
key: z.string().optional(),
525+
...timelineOrderField,
503526
kind: z.literal(`shared-state`),
504527
id: z.string(),
505528
mode: z.enum([`create`, `connect`]),
@@ -514,13 +537,15 @@ function createManifestSchema(): Schema<
514537
}),
515538
z.object({
516539
key: z.string().optional(),
540+
...timelineOrderField,
517541
kind: z.literal(`effect`),
518542
id: z.string(),
519543
function_ref: z.string(),
520544
config: z.unknown(),
521545
}),
522546
z.object({
523547
key: z.string().optional(),
548+
...timelineOrderField,
524549
kind: z.literal(`context`),
525550
id: z.string(),
526551
name: z.string(),
@@ -533,6 +558,7 @@ function createManifestSchema(): Schema<
533558
}),
534559
z.object({
535560
key: z.string().optional(),
561+
...timelineOrderField,
536562
kind: z.literal(`schedule`),
537563
id: z.string(),
538564
scheduleType: z.literal(`cron`),
@@ -543,6 +569,7 @@ function createManifestSchema(): Schema<
543569
}),
544570
z.object({
545571
key: z.string().optional(),
572+
...timelineOrderField,
546573
kind: z.literal(`schedule`),
547574
id: z.string(),
548575
scheduleType: z.literal(`future_send`),

0 commit comments

Comments
 (0)