@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.
Files changed (49) hide show
  1. package/dist/entrypoint.js +5 -3
  2. package/dist/index.cjs +5 -3
  3. package/dist/index.js +5 -3
  4. package/docs/entities/agents/horton.md +89 -0
  5. package/docs/entities/agents/worker.md +102 -0
  6. package/docs/entities/patterns/blackboard.md +111 -0
  7. package/docs/entities/patterns/dispatcher.md +77 -0
  8. package/docs/entities/patterns/manager-worker.md +127 -0
  9. package/docs/entities/patterns/map-reduce.md +81 -0
  10. package/docs/entities/patterns/pipeline.md +101 -0
  11. package/docs/entities/patterns/reactive-observers.md +125 -0
  12. package/docs/examples/mega-draw.md +106 -0
  13. package/docs/examples/playground.md +46 -0
  14. package/docs/index.md +208 -0
  15. package/docs/quickstart.md +201 -0
  16. package/docs/reference/agent-config.md +82 -0
  17. package/docs/reference/agent-tool.md +58 -0
  18. package/docs/reference/built-in-collections.md +334 -0
  19. package/docs/reference/cli.md +238 -0
  20. package/docs/reference/entity-definition.md +57 -0
  21. package/docs/reference/entity-handle.md +63 -0
  22. package/docs/reference/entity-registry.md +73 -0
  23. package/docs/reference/handler-context.md +108 -0
  24. package/docs/reference/runtime-handler.md +136 -0
  25. package/docs/reference/shared-state-handle.md +74 -0
  26. package/docs/reference/state-collection-proxy.md +41 -0
  27. package/docs/reference/wake-event.md +132 -0
  28. package/docs/usage/app-setup.md +165 -0
  29. package/docs/usage/clients-and-react.md +191 -0
  30. package/docs/usage/configuring-the-agent.md +136 -0
  31. package/docs/usage/context-composition.md +204 -0
  32. package/docs/usage/defining-entities.md +181 -0
  33. package/docs/usage/defining-tools.md +229 -0
  34. package/docs/usage/embedded-builtins.md +180 -0
  35. package/docs/usage/managing-state.md +93 -0
  36. package/docs/usage/overview.md +284 -0
  37. package/docs/usage/programmatic-runtime-client.md +216 -0
  38. package/docs/usage/shared-state.md +169 -0
  39. package/docs/usage/spawning-and-coordinating.md +165 -0
  40. package/docs/usage/testing.md +76 -0
  41. package/docs/usage/waking-entities.md +148 -0
  42. package/docs/usage/writing-handlers.md +267 -0
  43. package/package.json +2 -1
  44. package/skills/quickstart/scaffold/package.json +16 -3
  45. package/skills/quickstart/scaffold/tsconfig.json +8 -3
  46. package/skills/quickstart/scaffold/vite.config.ts +21 -0
  47. package/skills/quickstart/scaffold-ui/index.html +12 -0
  48. package/skills/quickstart/scaffold-ui/main.tsx +235 -0
  49. 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.