Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
7 changes: 7 additions & 0 deletions .changeset/self-send-wake-rendering.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@electric-ax/agents-runtime': patch
'@electric-ax/agents-server': patch
'@electric-ax/agents-server-ui': patch
---

Render self-send wake notifications with the sent message payload in the agent timeline.
8 changes: 8 additions & 0 deletions packages/agents-runtime/src/entity-schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,10 @@ type WakeChangeEntryValue = {
collection: string
kind: `insert` | `update` | `delete`
key: string
from?: string
payload?: unknown
timestamp?: string
message_type?: string
}
type WakeFinishedChildEntryValue = {
url: string
Expand Down Expand Up @@ -370,6 +374,10 @@ function createWakeChangeSchema(): Schema<WakeChangeEntryValue> {
collection: z.string(),
kind: z.enum([`insert`, `update`, `delete`]),
key: z.string(),
from: z.string().optional(),
payload: z.unknown().optional(),
timestamp: z.string().optional(),
message_type: z.string().optional(),
})
}

Expand Down
69 changes: 62 additions & 7 deletions packages/agents-server-ui/src/components/EntityTimeline.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -258,8 +258,40 @@ function timelineRowLabel(row: RenderTimelineRow): string {
return `Agent response`
}

function wakeReason(section: WakeSection): string {
function firstSelfSendWakeChange(
section: WakeSection,
entityUrl?: string | null
): WakeSection[`payload`][`changes`][number] | null {
if (!entityUrl || section.payload.source !== entityUrl) return null
return (
section.payload.changes.find((change) => change.collection === `inbox`) ??
null
)
}

function isSelfSendWake(
section: WakeSection,
entityUrl?: string | null
): boolean {
return firstSelfSendWakeChange(section, entityUrl) !== null
}

function wakeSelfSendMessage(
section: WakeSection,
entityUrl?: string | null
): string | null {
const change = firstSelfSendWakeChange(section, entityUrl)
if (!change) return null
const payload = change.payload
if (payload == null) return ``
return typeof payload === `string`
? payload
: JSON.stringify(payload, null, 2)
}

function wakeReason(section: WakeSection, entityUrl?: string | null): string {
const { payload } = section
if (isSelfSendWake(section, entityUrl)) return `sent to itself`
if (payload.timeout) return `timeout`
if (payload.finished_child) {
return `child ${payload.finished_child.run_status}`
Expand All @@ -273,23 +305,29 @@ function wakeReason(section: WakeSection): string {
return payload.source
}

function wakeSectionText(section: WakeSection): string {
function wakeSectionText(
section: WakeSection,
entityUrl?: string | null
): string {
return [
`woke`,
wakeReason(section),
wakeReason(section, entityUrl),
section.payload.source,
...wakeDetails(section).map((detail) => `${detail.label} ${detail.value}`),
].join(` `)
}

function WakeTimelineRow({
section,
entityUrl,
}: {
section: WakeSection
entityUrl?: string | null
}): React.ReactElement {
const reason = wakeReason(section)
const details = wakeDetails(section)
const reason = wakeReason(section, entityUrl)
const details = wakeDetails(section, entityUrl)
const childOutput = wakeChildOutput(section)
const selfSendMessage = wakeSelfSendMessage(section, entityUrl)
return (
<div className={styles.manifestRow}>
<InlineEventCard
Expand All @@ -307,6 +345,9 @@ function WakeTimelineRow({
</div>
))}
</div>
{selfSendMessage ? (
<pre className={styles.manifestJson}>{selfSendMessage}</pre>
) : null}
{childOutput ? (
<pre className={styles.manifestJson}>{childOutput.value}</pre>
) : null}
Expand Down Expand Up @@ -364,15 +405,28 @@ function signalSummary(
}

function wakeDetails(
section: WakeSection
section: WakeSection,
entityUrl?: string | null
): Array<{ label: string; value: string }> {
const { payload } = section
const selfSendChange = firstSelfSendWakeChange(section, entityUrl)
const details = [
{ label: `Source`, value: payload.source },
{ label: `Trigger`, value: wakeReason(section) },
{ label: `Trigger`, value: wakeReason(section, entityUrl) },
{ label: `Time`, value: formatAbsoluteDateTimeVerbose(section.timestamp) },
]

if (selfSendChange) {
details.push({
label: `From`,
value: selfSendChange.from ?? payload.source,
})
const message = wakeSelfSendMessage(section, entityUrl)
if (message) {
details.push({ label: `Message`, value: message })
}
}

if (payload.changes.length > 0) {
details.push({
label: `Changes`,
Expand Down Expand Up @@ -845,6 +899,7 @@ const TimelineRow = memo(function TimelineRow({
payload: row.wake.payload,
timestamp: Date.parse(row.wake.payload.timestamp),
}}
entityUrl={entityUrl}
/>
)
}
Expand Down
33 changes: 22 additions & 11 deletions packages/agents-server/src/wake-registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,10 @@ export interface WakeEvalResult {
collection: string
kind: `insert` | `update` | `delete`
key: string
from?: string
payload?: unknown
timestamp?: string
message_type?: string
}>
}
runFinishedStatus?: `completed` | `failed`
Expand Down Expand Up @@ -885,11 +889,7 @@ export class WakeRegistry {
reg: WakeRegistration,
event: Record<string, unknown>
): {
change: {
collection: string
kind: `insert` | `update` | `delete`
key: string
}
change: WakeEvalResult[`wakeMessage`][`changes`][number]
runFinishedStatus?: `completed` | `failed`
} | null {
if (reg.condition === `runFinished`) {
Expand Down Expand Up @@ -935,12 +935,23 @@ export class WakeRegistry {
return null
}

return {
change: {
collection: eventType,
kind,
key: (event.key as string) || ``,
},
const change: WakeEvalResult[`wakeMessage`][`changes`][number] = {
collection: eventType,
kind,
key: (event.key as string) || ``,
}

if (eventType === `inbox`) {
const value = event.value as Record<string, unknown> | undefined
if (typeof value?.from === `string`) change.from = value.from
if (`payload` in (value ?? {})) change.payload = value?.payload
if (typeof value?.timestamp === `string`)
change.timestamp = value.timestamp
if (typeof value?.message_type === `string`) {
change.message_type = value.message_type
}
}

return { change }
}
}
33 changes: 33 additions & 0 deletions packages/agents-server/test/wake-registry.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -277,6 +277,39 @@ describe(`Wake Registry`, () => {
expect(results[0]!.wakeMessage.changes[0]!.kind).toBe(`insert`)
})

it(`includes inbox message details in wake changes`, async () => {
const registry = new WakeRegistry(createMockDb())
await registry.register({
subscriberUrl: `/agent/self`,
sourceUrl: `/agent/self`,
condition: { on: `change`, collections: [`inbox`] },
oneShot: false,
})

const results = registry.evaluate(`/agent/self`, {
type: `inbox`,
key: `msg-1`,
value: {
from: `/principal/agent%3Aself`,
payload: { text: `wake up` },
timestamp: `2026-01-01T00:00:00.000Z`,
message_type: `reminder`,
},
headers: { operation: `insert` },
})

expect(results).toHaveLength(1)
expect(results[0]!.wakeMessage.changes[0]).toEqual({
collection: `inbox`,
kind: `insert`,
key: `msg-1`,
from: `/principal/agent%3Aself`,
payload: { text: `wake up` },
timestamp: `2026-01-01T00:00:00.000Z`,
message_type: `reminder`,
})
})

it(`ignores events for non-matching collections`, async () => {
const registry = new WakeRegistry(createMockDb())
await registry.register({
Expand Down
Loading