@electric-ax/agents 0.2.1 → 0.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/entrypoint.js +5 -3
- package/dist/index.cjs +5 -3
- package/dist/index.js +5 -3
- package/docs/entities/agents/horton.md +89 -0
- package/docs/entities/agents/worker.md +102 -0
- package/docs/entities/patterns/blackboard.md +111 -0
- package/docs/entities/patterns/dispatcher.md +77 -0
- package/docs/entities/patterns/manager-worker.md +127 -0
- package/docs/entities/patterns/map-reduce.md +81 -0
- package/docs/entities/patterns/pipeline.md +101 -0
- package/docs/entities/patterns/reactive-observers.md +125 -0
- package/docs/examples/mega-draw.md +106 -0
- package/docs/examples/playground.md +46 -0
- package/docs/index.md +208 -0
- package/docs/quickstart.md +201 -0
- package/docs/reference/agent-config.md +82 -0
- package/docs/reference/agent-tool.md +58 -0
- package/docs/reference/built-in-collections.md +334 -0
- package/docs/reference/cli.md +238 -0
- package/docs/reference/entity-definition.md +57 -0
- package/docs/reference/entity-handle.md +63 -0
- package/docs/reference/entity-registry.md +73 -0
- package/docs/reference/handler-context.md +108 -0
- package/docs/reference/runtime-handler.md +136 -0
- package/docs/reference/shared-state-handle.md +74 -0
- package/docs/reference/state-collection-proxy.md +41 -0
- package/docs/reference/wake-event.md +132 -0
- package/docs/usage/app-setup.md +165 -0
- package/docs/usage/clients-and-react.md +191 -0
- package/docs/usage/configuring-the-agent.md +136 -0
- package/docs/usage/context-composition.md +204 -0
- package/docs/usage/defining-entities.md +181 -0
- package/docs/usage/defining-tools.md +229 -0
- package/docs/usage/embedded-builtins.md +180 -0
- package/docs/usage/managing-state.md +93 -0
- package/docs/usage/overview.md +284 -0
- package/docs/usage/programmatic-runtime-client.md +216 -0
- package/docs/usage/shared-state.md +169 -0
- package/docs/usage/spawning-and-coordinating.md +165 -0
- package/docs/usage/testing.md +76 -0
- package/docs/usage/waking-entities.md +148 -0
- package/docs/usage/writing-handlers.md +267 -0
- package/package.json +2 -1
- package/skills/quickstart/scaffold/package.json +16 -3
- package/skills/quickstart/scaffold/tsconfig.json +8 -3
- package/skills/quickstart/scaffold/vite.config.ts +21 -0
- package/skills/quickstart/scaffold-ui/index.html +12 -0
- package/skills/quickstart/scaffold-ui/main.tsx +235 -0
- package/skills/quickstart.md +244 -334
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Clients & React
|
|
3
|
+
titleTemplate: '... - Electric Agents'
|
|
4
|
+
description: >-
|
|
5
|
+
Observe Electric Agents entities from app code, build reactive StreamDB handles,
|
|
6
|
+
and render chat timelines with the React useChat hook.
|
|
7
|
+
outline: [2, 3]
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# Clients & React
|
|
11
|
+
|
|
12
|
+
Use the client APIs when you need to observe agents from application code rather than from inside a handler. They load entity or observation streams into TanStack DB-backed collections that can drive UI.
|
|
13
|
+
|
|
14
|
+
## AgentsClient
|
|
15
|
+
|
|
16
|
+
`createAgentsClient()` creates a small read client for observation sources.
|
|
17
|
+
|
|
18
|
+
```ts
|
|
19
|
+
import {
|
|
20
|
+
createAgentsClient,
|
|
21
|
+
entity,
|
|
22
|
+
entities,
|
|
23
|
+
} from '@electric-ax/agents-runtime'
|
|
24
|
+
|
|
25
|
+
const client = createAgentsClient({ baseUrl: 'http://localhost:4437' })
|
|
26
|
+
|
|
27
|
+
// Observe a single entity stream.
|
|
28
|
+
const entityDb = await client.observe(entity('/horton/onboarding'))
|
|
29
|
+
console.log(entityDb.collections.texts.toArray)
|
|
30
|
+
|
|
31
|
+
// Observe the entity membership stream for a tag query.
|
|
32
|
+
const membersDb = await client.observe(entities({ tags: { project: 'alpha' } }))
|
|
33
|
+
console.log(membersDb.collections.members.toArray)
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
### Types
|
|
37
|
+
|
|
38
|
+
```ts
|
|
39
|
+
interface AgentsClientConfig {
|
|
40
|
+
baseUrl: string
|
|
41
|
+
fetch?: typeof globalThis.fetch
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
interface AgentsClient {
|
|
45
|
+
observe(
|
|
46
|
+
source: ObservationSource
|
|
47
|
+
): Promise<EntityStreamDB | ObservationStreamDB>
|
|
48
|
+
}
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
`observe(entity(url))` returns an `EntityStreamDB`. `observe(entities(...))` and `observe(db(...))` return an `ObservationStreamDB`.
|
|
52
|
+
|
|
53
|
+
:::: warning
|
|
54
|
+
`client.observe(cron(...))` is not currently supported. Use cron sources from handler wake subscriptions, or schedule tools exposed through `ctx.electricTools`.
|
|
55
|
+
::::
|
|
56
|
+
|
|
57
|
+
## Observation Sources
|
|
58
|
+
|
|
59
|
+
The same source helpers used by `ctx.observe()` can be used with `AgentsClient`.
|
|
60
|
+
|
|
61
|
+
| Helper | Use case |
|
|
62
|
+
| -------------------- | --------------------------------------------------- |
|
|
63
|
+
| `entity(url)` | Observe one entity by URL. |
|
|
64
|
+
| `entities({ tags })` | Observe the entity membership stream matching tags. |
|
|
65
|
+
| `db(id, schema)` | Observe a shared-state stream. |
|
|
66
|
+
| `cron(expression)` | Build a cron source for wake subscriptions. |
|
|
67
|
+
|
|
68
|
+
```ts
|
|
69
|
+
import { db } from '@electric-ax/agents-runtime'
|
|
70
|
+
|
|
71
|
+
const shared = await client.observe(db('research-123', researchSchema))
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## React useChat
|
|
75
|
+
|
|
76
|
+
`@electric-ax/agents-runtime/react` exports `useChat()`, a React hook that turns an `EntityStreamDB` into sections suitable for a chat UI.
|
|
77
|
+
|
|
78
|
+
```tsx
|
|
79
|
+
import { useEffect, useState } from 'react'
|
|
80
|
+
import { createAgentsClient, entity } from '@electric-ax/agents-runtime'
|
|
81
|
+
import { useChat } from '@electric-ax/agents-runtime/react'
|
|
82
|
+
import type { EntityStreamDB } from '@electric-ax/agents-runtime'
|
|
83
|
+
|
|
84
|
+
const client = createAgentsClient({ baseUrl: 'http://localhost:4437' })
|
|
85
|
+
|
|
86
|
+
export function AgentConversation({ entityUrl }: { entityUrl: string }) {
|
|
87
|
+
const [db, setDb] = useState<EntityStreamDB | null>(null)
|
|
88
|
+
|
|
89
|
+
useEffect(() => {
|
|
90
|
+
let cancelled = false
|
|
91
|
+
let observedDb: EntityStreamDB | null = null
|
|
92
|
+
client.observe(entity(entityUrl)).then((observed) => {
|
|
93
|
+
observedDb = observed as EntityStreamDB
|
|
94
|
+
if (cancelled) {
|
|
95
|
+
observedDb.close()
|
|
96
|
+
return
|
|
97
|
+
}
|
|
98
|
+
if (!cancelled) setDb(observedDb)
|
|
99
|
+
})
|
|
100
|
+
return () => {
|
|
101
|
+
cancelled = true
|
|
102
|
+
observedDb?.close()
|
|
103
|
+
}
|
|
104
|
+
}, [entityUrl])
|
|
105
|
+
|
|
106
|
+
const chat = useChat(db)
|
|
107
|
+
|
|
108
|
+
return (
|
|
109
|
+
<ol>
|
|
110
|
+
{chat.sections.map((section, index) => (
|
|
111
|
+
<li key={index}>
|
|
112
|
+
{section.kind === 'user_message'
|
|
113
|
+
? section.text
|
|
114
|
+
: section.items.map((item) =>
|
|
115
|
+
item.kind === 'text' ? item.text : item.toolName
|
|
116
|
+
)}
|
|
117
|
+
</li>
|
|
118
|
+
))}
|
|
119
|
+
</ol>
|
|
120
|
+
)
|
|
121
|
+
}
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
### UseChatResult
|
|
125
|
+
|
|
126
|
+
```ts
|
|
127
|
+
interface UseChatResult {
|
|
128
|
+
sections: EntityTimelineSection[]
|
|
129
|
+
state: 'pending' | 'queued' | 'working' | 'idle' | 'error'
|
|
130
|
+
runs: IncludesRun[]
|
|
131
|
+
inbox: IncludesInboxMessage[]
|
|
132
|
+
wakes: IncludesWakeMessage[]
|
|
133
|
+
entities: IncludesEntity[]
|
|
134
|
+
}
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
`sections` are the high-level chat timeline. `runs`, `inbox`, `wakes`, and `entities` expose the normalized underlying data for richer UIs.
|
|
138
|
+
|
|
139
|
+
## Timeline Helpers
|
|
140
|
+
|
|
141
|
+
If you are not using React, the runtime also exports pure timeline helpers:
|
|
142
|
+
|
|
143
|
+
```ts
|
|
144
|
+
import {
|
|
145
|
+
buildSections,
|
|
146
|
+
buildTimelineEntries,
|
|
147
|
+
createEntityIncludesQuery,
|
|
148
|
+
defaultProjection,
|
|
149
|
+
getEntityState,
|
|
150
|
+
materializeTimeline,
|
|
151
|
+
normalizeEntityTimelineData,
|
|
152
|
+
timelineMessages,
|
|
153
|
+
timelineToMessages,
|
|
154
|
+
} from '@electric-ax/agents-runtime'
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
Use these when you already have an `EntityStreamDB` and want to build your own UI integration.
|
|
158
|
+
|
|
159
|
+
| Helper | Purpose |
|
|
160
|
+
| ----------------------------------- | ---------------------------------------------------------------------- |
|
|
161
|
+
| `createEntityIncludesQuery(db)` | Builds the TanStack DB query used by `useChat`. |
|
|
162
|
+
| `normalizeEntityTimelineData()` | Normalizes and sorts nested run, text, tool, wake, and entity data. |
|
|
163
|
+
| `getEntityState(runs, inbox)` | Computes `pending`, `queued`, `working`, `idle`, or `error`. |
|
|
164
|
+
| `buildSections(runs, inbox)` | Builds chat-friendly user/agent sections. |
|
|
165
|
+
| `buildTimelineEntries(runs, inbox)` | Builds keyed timeline entries with response timestamps. |
|
|
166
|
+
| `materializeTimeline(data)` | Converts normalized timeline data into prompt-oriented timeline items. |
|
|
167
|
+
| `defaultProjection(item)` | Projects one timeline item into LLM messages. |
|
|
168
|
+
| `timelineMessages(db, opts?)` | Reads an entity DB and returns timestamped LLM messages. |
|
|
169
|
+
| `timelineToMessages(db)` | Convenience wrapper returning plain LLM messages. |
|
|
170
|
+
|
|
171
|
+
## CLI Entity Stream DB
|
|
172
|
+
|
|
173
|
+
The `electric-ax/entity-stream-db` subpath exposes a convenience loader used by CLI and UI code:
|
|
174
|
+
|
|
175
|
+
```ts
|
|
176
|
+
import { createEntityStreamDB } from 'electric-ax/entity-stream-db'
|
|
177
|
+
|
|
178
|
+
const { db, close } = await createEntityStreamDB({
|
|
179
|
+
baseUrl: 'http://localhost:4437',
|
|
180
|
+
entityUrl: '/horton/onboarding',
|
|
181
|
+
initialOffset: '0',
|
|
182
|
+
})
|
|
183
|
+
|
|
184
|
+
try {
|
|
185
|
+
console.log(db.collections.runs.toArray)
|
|
186
|
+
} finally {
|
|
187
|
+
close()
|
|
188
|
+
}
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
This API fetches entity metadata from the server, opens the entity's main stream, preloads it, and returns an `EntityStreamDB`.
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Configuring the agent
|
|
3
|
+
titleTemplate: '... - Electric Agents'
|
|
4
|
+
description: >-
|
|
5
|
+
Set up LLM agents with ctx.useAgent(), including model selection, system prompts, tools, and test responses.
|
|
6
|
+
outline: [2, 3]
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# Configuring the agent
|
|
10
|
+
|
|
11
|
+
Call `ctx.useAgent()` in your handler to set up the LLM, then `ctx.agent.run()` to execute it.
|
|
12
|
+
|
|
13
|
+
## AgentConfig
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
interface AgentConfig {
|
|
17
|
+
systemPrompt: string
|
|
18
|
+
model: string | Model<any>
|
|
19
|
+
provider?: KnownProvider
|
|
20
|
+
tools: AgentTool[]
|
|
21
|
+
streamFn?: StreamFn
|
|
22
|
+
testResponses?: string[] | TestResponseFn
|
|
23
|
+
}
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
| Field | Required | Description |
|
|
27
|
+
| --------------- | -------- | ---------------------------------------------------------------------------------------------------------------------- |
|
|
28
|
+
| `systemPrompt` | Yes | The system prompt passed to the LLM. |
|
|
29
|
+
| `model` | Yes | Model identifier string or resolved model object. |
|
|
30
|
+
| `provider` | No | Provider to use when `model` is a string. Defaults to `"anthropic"`. |
|
|
31
|
+
| `tools` | Yes | Array of tools available to the agent. Spread `ctx.electricTools` when your runtime host provides runtime-level tools. |
|
|
32
|
+
| `streamFn` | No | Optional streaming callback passed to the underlying agent. |
|
|
33
|
+
| `testResponses` | No | Mock responses for testing without calling the LLM. |
|
|
34
|
+
|
|
35
|
+
## Basic usage
|
|
36
|
+
|
|
37
|
+
```ts
|
|
38
|
+
async handler(ctx) {
|
|
39
|
+
ctx.useAgent({
|
|
40
|
+
systemPrompt: 'You are a helpful assistant.',
|
|
41
|
+
model: 'claude-sonnet-4-5-20250929',
|
|
42
|
+
tools: [...ctx.electricTools],
|
|
43
|
+
})
|
|
44
|
+
await ctx.agent.run()
|
|
45
|
+
}
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
`useAgent` returns an `AgentHandle` and also sets `ctx.agent`. Both references are equivalent.
|
|
49
|
+
|
|
50
|
+
To control what content fills the agent's context window (token budgets, cache tiers, external sources), use `ctx.useContext()` alongside `useAgent`. See [Context composition](./context-composition).
|
|
51
|
+
|
|
52
|
+
## ctx.electricTools
|
|
53
|
+
|
|
54
|
+
`ctx.electricTools` is an array of runtime-provided tools. It may be empty, or it may contain host-provided tools such as schedule management tools. Spread it into the `tools` array when you want the LLM agent to access those runtime-level tools:
|
|
55
|
+
|
|
56
|
+
```ts
|
|
57
|
+
tools: [...ctx.electricTools, myCustomTool, anotherTool]
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Handler-level coordination APIs such as `ctx.spawn`, `ctx.observe`, and `ctx.send` are available on `HandlerContext` regardless of whether you pass `ctx.electricTools` to the LLM.
|
|
61
|
+
|
|
62
|
+
## ctx.agent.run()
|
|
63
|
+
|
|
64
|
+
Executes the agent loop. Blocks until the LLM finishes -- all tool calls are resolved and the final text response is emitted.
|
|
65
|
+
|
|
66
|
+
```ts
|
|
67
|
+
const result = await ctx.agent.run()
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Returns an `AgentRunResult`:
|
|
71
|
+
|
|
72
|
+
```ts
|
|
73
|
+
type AgentRunResult = {
|
|
74
|
+
writes: ChangeEvent[]
|
|
75
|
+
toolCalls: Array<{ name: string; args: unknown; result: unknown }>
|
|
76
|
+
usage: { tokens: number; duration: number }
|
|
77
|
+
}
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
| Field | Description |
|
|
81
|
+
| ----------- | --------------------------------------------------------------------------------------- |
|
|
82
|
+
| `writes` | Currently returned as an empty array placeholder. |
|
|
83
|
+
| `toolCalls` | Currently returned as an empty array placeholder. |
|
|
84
|
+
| `usage` | Currently returned as `{ tokens: 0, duration: 0 }` until usage aggregation is wired in. |
|
|
85
|
+
|
|
86
|
+
## AgentHandle
|
|
87
|
+
|
|
88
|
+
Returned by `useAgent`. Also accessible as `ctx.agent`.
|
|
89
|
+
|
|
90
|
+
```ts
|
|
91
|
+
interface AgentHandle {
|
|
92
|
+
run: (input?: string) => Promise<AgentRunResult>
|
|
93
|
+
}
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
You must call `useAgent` before calling `run()`. Calling `ctx.agent.run()` without prior configuration throws an error.
|
|
97
|
+
|
|
98
|
+
## Model
|
|
99
|
+
|
|
100
|
+
When `model` is a string, the runtime resolves it through the configured `provider` (default `"anthropic"`). You can also pass a resolved `Model` object directly.
|
|
101
|
+
|
|
102
|
+
```ts
|
|
103
|
+
model: 'claude-sonnet-4-5-20250929'
|
|
104
|
+
provider: 'anthropic'
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
## Test responses
|
|
108
|
+
|
|
109
|
+
For testing handlers without making LLM calls, pass `testResponses`. Two forms are supported:
|
|
110
|
+
|
|
111
|
+
**Array of strings** -- selected by the number of prior runs, useful for deterministic repeated wakes:
|
|
112
|
+
|
|
113
|
+
```ts
|
|
114
|
+
ctx.useAgent({
|
|
115
|
+
systemPrompt: '...',
|
|
116
|
+
model: 'claude-sonnet-4-5-20250929',
|
|
117
|
+
tools: [...ctx.electricTools],
|
|
118
|
+
testResponses: ['Hello! How can I help?', 'Sure, I can do that.'],
|
|
119
|
+
})
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
**Function** -- called for each turn with the current message and an `OutboundBridgeHandle`:
|
|
123
|
+
|
|
124
|
+
```ts
|
|
125
|
+
ctx.useAgent({
|
|
126
|
+
// ...
|
|
127
|
+
testResponses: async (message, bridge) => {
|
|
128
|
+
if (message.includes('calculate')) {
|
|
129
|
+
return 'The answer is 42.'
|
|
130
|
+
}
|
|
131
|
+
return undefined // emits no automatic text response
|
|
132
|
+
},
|
|
133
|
+
})
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
See [Testing](./testing) for more on writing tests with `testResponses`.
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Context composition
|
|
3
|
+
titleTemplate: '... - Electric Agents'
|
|
4
|
+
description: >-
|
|
5
|
+
Control what goes into the agent's context window using ctx.useContext() with token-budgeted sources, cache tiers, and imperative context entries.
|
|
6
|
+
outline: [2, 3]
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# Context composition
|
|
10
|
+
|
|
11
|
+
By default, the runtime assembles the agent's context window from the entity's full timeline (messages, tool calls, text responses). `ctx.useContext()` gives you explicit control over what goes in and how much space each piece gets.
|
|
12
|
+
|
|
13
|
+
## When to use it
|
|
14
|
+
|
|
15
|
+
Most entities don't need `useContext` -- the default timeline assembly works well for simple conversational agents. Use `useContext` when you need to:
|
|
16
|
+
|
|
17
|
+
- **Budget token space** across multiple content sources (docs, conversation history, retrieved context)
|
|
18
|
+
- **Mix static and dynamic content** with different caching behavior
|
|
19
|
+
- **Inject external content** (documentation, search results, knowledge bases) alongside conversation history
|
|
20
|
+
|
|
21
|
+
## UseContextConfig
|
|
22
|
+
|
|
23
|
+
```ts
|
|
24
|
+
ctx.useContext({
|
|
25
|
+
sourceBudget: 18_000,
|
|
26
|
+
sources: {
|
|
27
|
+
docs: {
|
|
28
|
+
content: () => '# Reference docs\n...',
|
|
29
|
+
max: 6_000,
|
|
30
|
+
cache: 'stable',
|
|
31
|
+
},
|
|
32
|
+
conversation: {
|
|
33
|
+
content: () => ctx.timelineMessages(),
|
|
34
|
+
max: 12_000,
|
|
35
|
+
cache: 'volatile',
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
})
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
| Field | Type | Description |
|
|
42
|
+
| -------------- | ------------------------------ | -------------------------------------------------------- |
|
|
43
|
+
| `sourceBudget` | `number` | Total token budget across all sources. Required. |
|
|
44
|
+
| `sources` | `Record<string, SourceConfig>` | Named content sources. Must contain at least one source. |
|
|
45
|
+
|
|
46
|
+
### SourceConfig
|
|
47
|
+
|
|
48
|
+
Each source declares a content function, a max token allocation, and a cache tier:
|
|
49
|
+
|
|
50
|
+
| Field | Type | Description |
|
|
51
|
+
| --------- | ------------------------------------------------------ | -------------------------------------------------------------------------------- |
|
|
52
|
+
| `content` | `() => string \| LLMMessage[] \| TimestampedMessage[]` | Function called each agent run to produce the source content. Can be async. |
|
|
53
|
+
| `max` | `number` | Maximum tokens this source may consume. Content is truncated if it exceeds this. |
|
|
54
|
+
| `cache` | `CacheTier` | Caching hint that controls assembly ordering. See [Cache tiers](#cache-tiers). |
|
|
55
|
+
|
|
56
|
+
The `content` function can return:
|
|
57
|
+
|
|
58
|
+
- A **string** -- inserted as a single system message
|
|
59
|
+
- An **`LLMMessage[]`** array -- inserted as-is
|
|
60
|
+
- A **`TimestampedMessage[]`** array -- interleaved by timestamp with other volatile sources
|
|
61
|
+
|
|
62
|
+
### Cache tiers
|
|
63
|
+
|
|
64
|
+
Cache tiers control assembly ordering and caching behavior. Sources are assembled from most stable to most volatile:
|
|
65
|
+
|
|
66
|
+
| Tier | Position | Use for |
|
|
67
|
+
| ----------------- | -------- | -------------------------------------------------------------- |
|
|
68
|
+
| `"pinned"` | First | Content that never changes (system instructions, schemas) |
|
|
69
|
+
| `"stable"` | Second | Content that changes rarely (docs TOC, reference material) |
|
|
70
|
+
| `"slow-changing"` | Third | Content that updates occasionally (summaries, aggregations) |
|
|
71
|
+
| `"volatile"` | Last | Content that changes every wake (conversation, search results) |
|
|
72
|
+
|
|
73
|
+
Non-volatile sources (`pinned`, `stable`, `slow-changing`) have their `max` values summed and validated against `sourceBudget` at registration time. Volatile sources share the remaining budget.
|
|
74
|
+
|
|
75
|
+
## timelineMessages
|
|
76
|
+
|
|
77
|
+
`ctx.timelineMessages()` projects the entity's timeline (inbox messages, agent runs, tool calls) into an ordered array of `TimestampedMessage` objects suitable for passing to an LLM.
|
|
78
|
+
|
|
79
|
+
```ts
|
|
80
|
+
const messages = ctx.timelineMessages()
|
|
81
|
+
// or with options:
|
|
82
|
+
const messages = ctx.timelineMessages({
|
|
83
|
+
since: 42,
|
|
84
|
+
projection: (item) => {
|
|
85
|
+
if (item.kind === 'run') return [{ role: 'assistant', content: '...' }]
|
|
86
|
+
return null // use default projection
|
|
87
|
+
},
|
|
88
|
+
})
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
| Option | Type | Description |
|
|
92
|
+
| ------------ | ---------------------------------------------- | -------------------------------------------------------------------------------------- |
|
|
93
|
+
| `since` | `number` | Only include items after this timeline position. |
|
|
94
|
+
| `projection` | `(item: TimelineItem) => LLMMessage[] \| null` | Custom projection function. Return `null` to use the default projection for that item. |
|
|
95
|
+
|
|
96
|
+
This is typically used as the `content` function of a `volatile` source:
|
|
97
|
+
|
|
98
|
+
```ts
|
|
99
|
+
ctx.useContext({
|
|
100
|
+
sourceBudget: 15_000,
|
|
101
|
+
sources: {
|
|
102
|
+
conversation: {
|
|
103
|
+
content: () => ctx.timelineMessages(),
|
|
104
|
+
max: 15_000,
|
|
105
|
+
cache: 'volatile',
|
|
106
|
+
},
|
|
107
|
+
},
|
|
108
|
+
})
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
## Context entries
|
|
112
|
+
|
|
113
|
+
Context entries are durable key-value items stored in the entity's stream. Unlike sources (which are recomputed each wake), context entries persist across wakes and are projected into the context window automatically when `useContext` is active.
|
|
114
|
+
|
|
115
|
+
Use context entries for information the agent discovers during a run that should remain available in future wakes (e.g. user preferences, extracted facts, accumulated instructions).
|
|
116
|
+
|
|
117
|
+
### insertContext
|
|
118
|
+
|
|
119
|
+
```ts
|
|
120
|
+
ctx.insertContext('user-prefs', {
|
|
121
|
+
name: 'User preferences',
|
|
122
|
+
content: 'Prefers concise responses. Timezone: PST.',
|
|
123
|
+
attrs: { priority: 'high' },
|
|
124
|
+
})
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
Inserting with an existing `id` replaces the previous entry.
|
|
128
|
+
|
|
129
|
+
### removeContext
|
|
130
|
+
|
|
131
|
+
```ts
|
|
132
|
+
ctx.removeContext('user-prefs')
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
### getContext / listContext
|
|
136
|
+
|
|
137
|
+
```ts
|
|
138
|
+
const entry = ctx.getContext('user-prefs')
|
|
139
|
+
// { id: "user-prefs", name: "User preferences", content: "...", insertedAt: 1234 }
|
|
140
|
+
|
|
141
|
+
const all = ctx.listContext()
|
|
142
|
+
// Array<ContextEntry>
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
### ContextEntryInput
|
|
146
|
+
|
|
147
|
+
| Field | Type | Description |
|
|
148
|
+
| --------- | ------------------- | ----------------------------------- |
|
|
149
|
+
| `name` | `string` | Human-readable label for the entry. |
|
|
150
|
+
| `content` | `string` | The text content. |
|
|
151
|
+
| `attrs` | `ContextEntryAttrs` | Optional metadata attributes. |
|
|
152
|
+
|
|
153
|
+
### ContextEntry
|
|
154
|
+
|
|
155
|
+
Extends `ContextEntryInput` with:
|
|
156
|
+
|
|
157
|
+
| Field | Type | Description |
|
|
158
|
+
| ------------ | -------- | --------------------------------- |
|
|
159
|
+
| `id` | `string` | The id passed to `insertContext`. |
|
|
160
|
+
| `insertedAt` | `number` | Timeline position when inserted. |
|
|
161
|
+
|
|
162
|
+
## Full example
|
|
163
|
+
|
|
164
|
+
This example from the built-in Horton assistant shows all three source types working together:
|
|
165
|
+
|
|
166
|
+
```ts
|
|
167
|
+
async handler(ctx, wake) {
|
|
168
|
+
const tools = [...ctx.electricTools, ...customTools]
|
|
169
|
+
|
|
170
|
+
ctx.useContext({
|
|
171
|
+
sourceBudget: 18_000,
|
|
172
|
+
sources: {
|
|
173
|
+
docs_toc: {
|
|
174
|
+
content: () => renderCompressedToc(),
|
|
175
|
+
max: 3_000,
|
|
176
|
+
cache: "stable",
|
|
177
|
+
},
|
|
178
|
+
retrieved_docs: {
|
|
179
|
+
content: () => renderRetrievedDocs(wake, ctx.events),
|
|
180
|
+
max: 6_000,
|
|
181
|
+
cache: "volatile",
|
|
182
|
+
},
|
|
183
|
+
conversation: {
|
|
184
|
+
content: () => ctx.timelineMessages(),
|
|
185
|
+
max: 9_000,
|
|
186
|
+
cache: "volatile",
|
|
187
|
+
},
|
|
188
|
+
},
|
|
189
|
+
})
|
|
190
|
+
|
|
191
|
+
ctx.useAgent({
|
|
192
|
+
systemPrompt: "You are a helpful assistant.",
|
|
193
|
+
model: "claude-sonnet-4-5-20250929",
|
|
194
|
+
tools,
|
|
195
|
+
})
|
|
196
|
+
await ctx.agent.run()
|
|
197
|
+
}
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
The `stable` docs TOC is assembled first and cached across wakes. The two `volatile` sources (retrieved docs and conversation) are recomputed each wake and share the remaining budget.
|
|
201
|
+
|
|
202
|
+
## Entities without useContext
|
|
203
|
+
|
|
204
|
+
Entities that don't call `useContext` are unchanged -- the runtime uses its default timeline assembly, building the full conversation history into the context window automatically. There is no need to migrate existing entities.
|