Skip to content

Commit 004bea1

Browse files
authored
Render standalone entity stream errors (#4547)
## Summary - include standalone entity stream errors in the main timeline query - render errors with their error code and message in the timeline UI - keep existing run-attached error/fallback rendering unchanged ## Verification - pnpm --dir packages/agents-server-ui typecheck - pnpm --dir packages/agents-runtime test entity-timeline --run Note: full packages/agents-runtime typecheck still fails on existing unrelated missing exports in test/pg-sync-source.test.ts.
1 parent 671a38f commit 004bea1

4 files changed

Lines changed: 111 additions & 5 deletions

File tree

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
'@electric-ax/agents-runtime': patch
3+
'@electric-ax/agents-server-ui': patch
4+
---
5+
6+
Render standalone entity stream errors in the timeline with their error code and message.

packages/agents-runtime/src/entity-timeline.ts

Lines changed: 47 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
eq,
99
gt,
1010
isNull,
11+
isUndefined,
1112
like,
1213
localOnlyCollectionOptions,
1314
or,
@@ -281,6 +282,9 @@ export interface EntityTimelineRunRow {
281282
export type EntityTimelineInboxRow = IncludesInboxMessage
282283
export type EntityTimelineWakeRow = IncludesWakeMessage
283284
export type EntityTimelineSignalRow = IncludesSignal
285+
export type EntityTimelineErrorRow = EntityTimelineErrorItem & {
286+
order: TimelineOrder
287+
}
284288

285289
export type EntityTimelineQueryRow =
286290
| {
@@ -289,6 +293,7 @@ export type EntityTimelineQueryRow =
289293
run?: undefined
290294
wake?: undefined
291295
signal?: undefined
296+
error?: undefined
292297
manifest?: undefined
293298
}
294299
| {
@@ -297,6 +302,7 @@ export type EntityTimelineQueryRow =
297302
run: EntityTimelineRunRow
298303
wake?: undefined
299304
signal?: undefined
305+
error?: undefined
300306
manifest?: undefined
301307
}
302308
| {
@@ -305,6 +311,7 @@ export type EntityTimelineQueryRow =
305311
run?: undefined
306312
wake: EntityTimelineWakeRow
307313
signal?: undefined
314+
error?: undefined
308315
manifest?: undefined
309316
}
310317
| {
@@ -313,6 +320,16 @@ export type EntityTimelineQueryRow =
313320
run?: undefined
314321
wake?: undefined
315322
signal: EntityTimelineSignalRow
323+
error?: undefined
324+
manifest?: undefined
325+
}
326+
| {
327+
$key: string
328+
inbox?: undefined
329+
run?: undefined
330+
wake?: undefined
331+
signal?: undefined
332+
error: EntityTimelineErrorRow
316333
manifest?: undefined
317334
}
318335
| {
@@ -321,6 +338,7 @@ export type EntityTimelineQueryRow =
321338
run?: undefined
322339
wake?: undefined
323340
signal?: undefined
341+
error?: undefined
324342
manifest: ManifestEntry
325343
}
326344

@@ -1356,6 +1374,17 @@ function buildEntityTimelineQuery(
13561374
new_state: signal.new_state,
13571375
}))
13581376

1377+
const errorSource = q
1378+
.from({ error: db.collections.errors })
1379+
.where(({ error }) => or(isNull(error.run_id), isUndefined(error.run_id)))
1380+
.select(({ error }) => ({
1381+
key: error.key,
1382+
order: coalesce(error._timeline_order, `~`),
1383+
error_code: error.error_code,
1384+
message: error.message,
1385+
run_id: error.run_id,
1386+
}))
1387+
13591388
const runItemsSource = q
13601389
.unionAll({
13611390
text: db.collections.texts,
@@ -1490,30 +1519,41 @@ function buildEntityTimelineQuery(
14901519
run: runSource,
14911520
wake: wakeSource,
14921521
signal: signalSource,
1522+
error: errorSource,
14931523
manifest: db.collections.manifests,
14941524
})
1495-
.orderBy(({ inbox, run, wake, signal, manifest }) =>
1525+
.orderBy(({ inbox, run, wake, signal, error, manifest }) =>
14961526
coalesce(
14971527
inbox.order,
14981528
run.order,
14991529
wake.order,
15001530
signal.order,
1531+
error.order,
15011532
manifest._timeline_order,
15021533
`~`
15031534
)
15041535
)
1505-
.orderBy(({ inbox, run, wake, signal, manifest }) =>
1536+
.orderBy(({ inbox, run, wake, signal, error, manifest }) =>
15061537
coalesce(
15071538
caseWhen(inbox.key, `inbox`),
15081539
caseWhen(run.key, `run`),
15091540
caseWhen(wake.key, `wake`),
15101541
caseWhen(signal.key, `signal`),
1542+
caseWhen(error.key, `error`),
15111543
caseWhen(manifest.key, `manifest`),
15121544
``
15131545
)
15141546
)
1515-
.orderBy(({ inbox, run, wake, signal, manifest }) =>
1516-
coalesce(inbox.key, run.key, wake.key, signal.key, manifest.key, ``)
1547+
.orderBy(({ inbox, run, wake, signal, error, manifest }) =>
1548+
coalesce(
1549+
inbox.key,
1550+
run.key,
1551+
wake.key,
1552+
signal.key,
1553+
error.key,
1554+
manifest.key,
1555+
``
1556+
)
15171557
)
15181558
}
15191559

@@ -1751,7 +1791,9 @@ export function createEntityErrorsQuery(
17511791
return (q: InitialQueryBuilder) =>
17521792
q
17531793
.from({ errors: db.collections.errors })
1754-
.where(({ errors }) => isNull(errors.run_id))
1794+
.where(({ errors }) =>
1795+
or(isNull(errors.run_id), isUndefined(errors.run_id))
1796+
)
17551797
.select(({ errors }) => ({
17561798
key: errors.key,
17571799
error_code: errors.error_code,

packages/agents-runtime/test/entity-timeline.test.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,9 @@ import {
66
import {
77
buildEntityTimelineData,
88
compareTimelineOrders,
9+
createEntityErrorsQuery,
910
createEntityIncludesQuery,
11+
createEntityTimelineQuery,
1012
getEntityState,
1113
normalizeEntityTimelineData,
1214
} from '../src/entity-timeline'
@@ -1659,6 +1661,39 @@ describe(`entity includes query`, () => {
16591661
}
16601662
}
16611663

1664+
it(`matches standalone errors when run_id is omitted`, async () => {
1665+
const { collections, sync } = createEntityCollections()
1666+
const errorsQuery = createLiveQueryCollection({
1667+
query: createEntityErrorsQuery({ collections } as any),
1668+
startSync: true,
1669+
})
1670+
const timelineQuery = createLiveQueryCollection({
1671+
query: createEntityTimelineQuery({ collections } as any),
1672+
startSync: true,
1673+
})
1674+
await Promise.all([errorsQuery.preload(), timelineQuery.preload()])
1675+
1676+
sync.errors.insert({
1677+
key: `err-omitted-run`,
1678+
error_code: `HANDLER_FAILED`,
1679+
message: `boom`,
1680+
})
1681+
await new Promise((r) => setTimeout(r, 50))
1682+
1683+
expect(getData(errorsQuery)).toMatchObject([
1684+
{
1685+
key: `err-omitted-run`,
1686+
error_code: `HANDLER_FAILED`,
1687+
message: `boom`,
1688+
},
1689+
])
1690+
expect(getData(timelineQuery)[0]?.error).toMatchObject({
1691+
key: `err-omitted-run`,
1692+
error_code: `HANDLER_FAILED`,
1693+
message: `boom`,
1694+
})
1695+
})
1696+
16621697
it(`reacts to changes in the top-level runs collection`, async () => {
16631698
const { collections, sync } = createEntityCollections()
16641699
const queryFn = createEntityIncludesQuery({ collections } as any)

packages/agents-server-ui/src/components/EntityTimeline.tsx

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -265,6 +265,7 @@ function timelineRowSearchText(
265265
})
266266
}
267267
if (row.signal) return signalSearchText(row.signal)
268+
if (row.error) return `${row.error.error_code} ${row.error.message}`
268269
if (row.manifest) return manifestSearchText(row.manifest)
269270
return runSearchTextByKey.get(row.$key) ?? runSearchTextFromSnapshot(row.run)
270271
}
@@ -274,6 +275,7 @@ function timelineRowLabel(row: RenderTimelineRow): string {
274275
if (row.inbox) return `User message`
275276
if (row.wake) return `Wake`
276277
if (row.signal) return `Signal`
278+
if (row.error) return `Error`
277279
if (row.manifest) return `Manifest item`
278280
return `Agent response`
279281
}
@@ -434,6 +436,23 @@ function SignalTimelineRow({
434436
)
435437
}
436438

439+
function ErrorTimelineRow({
440+
error,
441+
}: {
442+
error: NonNullable<RenderTimelineRow[`error`]>
443+
}): React.ReactElement {
444+
return (
445+
<div className={styles.manifestRow}>
446+
<InlineEventCard
447+
icon={CircleStop}
448+
title={error.error_code || `error`}
449+
summary={error.message}
450+
headerSurface
451+
/>
452+
</div>
453+
)
454+
}
455+
437456
function signalSearchText(
438457
signal: NonNullable<RenderTimelineRow[`signal`]>
439458
): string {
@@ -982,6 +1001,10 @@ const TimelineRow = memo(function TimelineRow({
9821001
return <SignalTimelineRow signal={row.signal} />
9831002
}
9841003

1004+
if (row.error) {
1005+
return <ErrorTimelineRow error={row.error} />
1006+
}
1007+
9851008
if (row.manifest) {
9861009
return (
9871010
<ManifestTimelineRow

0 commit comments

Comments
 (0)