@electric-ax/agents 0.2.3 → 0.3.0
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 +474 -737
- package/dist/index.cjs +470 -733
- package/dist/index.d.cts +68 -35
- package/dist/index.d.ts +69 -36
- package/dist/index.js +489 -751
- package/docs/entities/agents/horton.md +12 -12
- package/docs/entities/agents/worker.md +18 -18
- package/docs/entities/patterns/blackboard.md +6 -6
- package/docs/entities/patterns/dispatcher.md +1 -1
- package/docs/entities/patterns/manager-worker.md +1 -1
- package/docs/entities/patterns/map-reduce.md +1 -1
- package/docs/entities/patterns/pipeline.md +1 -1
- package/docs/entities/patterns/reactive-observers.md +2 -2
- package/docs/examples/playground.md +42 -26
- package/docs/index.md +25 -23
- package/docs/quickstart.md +12 -12
- package/docs/reference/agent-config.md +20 -12
- package/docs/reference/agent-tool.md +1 -1
- package/docs/reference/built-in-collections.md +21 -21
- package/docs/reference/cli.md +39 -30
- package/docs/reference/entity-definition.md +9 -9
- package/docs/reference/entity-handle.md +2 -2
- package/docs/reference/entity-registry.md +1 -1
- package/docs/reference/handler-context.md +34 -18
- package/docs/reference/mcp-registry.md +189 -0
- package/docs/reference/mcp-server-config.md +226 -0
- package/docs/reference/runtime-handler.md +25 -23
- package/docs/reference/shared-state-handle.md +7 -7
- package/docs/reference/state-collection-proxy.md +1 -1
- package/docs/reference/wake-event.md +23 -23
- package/docs/usage/app-setup.md +24 -23
- package/docs/usage/clients-and-react.md +40 -36
- package/docs/usage/configuring-the-agent.md +25 -19
- package/docs/usage/context-composition.md +12 -12
- package/docs/usage/defining-entities.md +36 -36
- package/docs/usage/defining-tools.md +45 -45
- package/docs/usage/embedded-builtins.md +54 -43
- package/docs/usage/managing-state.md +12 -12
- package/docs/usage/mcp-servers.md +354 -0
- package/docs/usage/overview.md +50 -45
- package/docs/usage/programmatic-runtime-client.md +51 -48
- package/docs/usage/shared-state.md +32 -32
- package/docs/usage/spawning-and-coordinating.md +9 -9
- package/docs/usage/testing.md +14 -14
- package/docs/usage/waking-entities.md +13 -13
- package/docs/usage/writing-handlers.md +52 -26
- package/package.json +9 -4
- package/scripts/sync-docs.mjs +42 -0
- package/docs/examples/mega-draw.md +0 -106
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
title: Writing handlers
|
|
3
|
-
titleTemplate:
|
|
3
|
+
titleTemplate: "... - Electric Agents"
|
|
4
4
|
description: >-
|
|
5
5
|
Implement entity handlers using HandlerContext and WakeEvent, with patterns for first wake, messaging, and tool use.
|
|
6
6
|
outline: [2, 3]
|
|
@@ -62,6 +62,7 @@ interface HandlerContext<TState extends StateProxy = StateProxy> {
|
|
|
62
62
|
payload: unknown,
|
|
63
63
|
opts?: { type?: string; afterMs?: number }
|
|
64
64
|
) => void
|
|
65
|
+
recordRun: () => RunHandle
|
|
65
66
|
setTag: (key: string, value: string) => Promise<void>
|
|
66
67
|
removeTag: (key: string) => Promise<void>
|
|
67
68
|
sleep: () => void
|
|
@@ -72,7 +73,7 @@ interface HandlerContext<TState extends StateProxy = StateProxy> {
|
|
|
72
73
|
|
|
73
74
|
| Property | Description |
|
|
74
75
|
| ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
75
|
-
| `firstWake` | `true`
|
|
76
|
+
| `firstWake` | `true` during the initial setup pass while the entity has no persisted manifest entries. Use state checks for one-time plain state initialization. |
|
|
76
77
|
| `tags` | Entity tags -- key/value metadata associated with this entity. |
|
|
77
78
|
| `entityUrl` | The entity's URL path, e.g. `"/assistant/my-chat"`. |
|
|
78
79
|
| `entityType` | The registered type name, e.g. `"assistant"`. |
|
|
@@ -81,7 +82,7 @@ interface HandlerContext<TState extends StateProxy = StateProxy> {
|
|
|
81
82
|
| `state` | Proxy object keyed by collection name. Each property is a [`StateCollectionProxy`](../reference/state-collection-proxy). |
|
|
82
83
|
| `events` | Change events that triggered this wake. |
|
|
83
84
|
| `actions` | Custom non-CRUD action functions from the entity definition's `actions` factory. |
|
|
84
|
-
| `electricTools`
|
|
85
|
+
| `electricTools` | Host-provided runtime-level tools to pass to `useAgent` when needed. May be empty. |
|
|
85
86
|
| `useAgent` | Configures the LLM agent. Returns an `AgentHandle`. See [Configuring the agent](./configuring-the-agent). |
|
|
86
87
|
| `useContext` | Declares context sources with token budgets and cache tiers. See [Context composition](./context-composition). |
|
|
87
88
|
| `timelineMessages` | Projects the entity timeline into LLM messages. See [Context composition](./context-composition#timelinemessages). |
|
|
@@ -94,6 +95,7 @@ interface HandlerContext<TState extends StateProxy = StateProxy> {
|
|
|
94
95
|
| `observe` | Connects to another entity's stream or shared db. See [Reactive observers](../entities/patterns/reactive-observers) and [Shared state](./shared-state). |
|
|
95
96
|
| `mkdb` | Creates a new shared state stream. See [Shared state](./shared-state). |
|
|
96
97
|
| `send` | Sends a message to another entity's inbox. Supports delayed delivery via `afterMs`. |
|
|
98
|
+
| `recordRun` | Records non-LLM work in the built-in `runs` collection so `runFinished` observers are woken. |
|
|
97
99
|
| `setTag` | Sets a tag on this entity. |
|
|
98
100
|
| `removeTag` | Removes a tag from this entity. |
|
|
99
101
|
| `sleep` | Returns the entity to idle without re-waking. |
|
|
@@ -115,36 +117,36 @@ type WakeEvent = {
|
|
|
115
117
|
}
|
|
116
118
|
```
|
|
117
119
|
|
|
118
|
-
| Field | Description
|
|
119
|
-
| ------------ |
|
|
120
|
-
| `source` | The stream or entity that caused the wake.
|
|
120
|
+
| Field | Description |
|
|
121
|
+
| ------------ | -------------------------------------------------------------- |
|
|
122
|
+
| `source` | The stream or entity that caused the wake. |
|
|
121
123
|
| `type` | The wake type: `"message_received"` for inbox messages or `"wake"` for child completion, observed changes, cron, and timeouts. |
|
|
122
|
-
| `fromOffset` | Start offset of the events that triggered this wake.
|
|
123
|
-
| `toOffset` | End offset of the events that triggered this wake.
|
|
124
|
-
| `eventCount` | Number of new events since last wake.
|
|
125
|
-
| `payload` | Optional payload from the trigger event.
|
|
126
|
-
| `summary` | Optional human-readable summary.
|
|
127
|
-
| `fullRef` | Optional full reference string for the trigger.
|
|
124
|
+
| `fromOffset` | Start offset of the events that triggered this wake. |
|
|
125
|
+
| `toOffset` | End offset of the events that triggered this wake. |
|
|
126
|
+
| `eventCount` | Number of new events since last wake. |
|
|
127
|
+
| `payload` | Optional payload from the trigger event. |
|
|
128
|
+
| `summary` | Optional human-readable summary. |
|
|
129
|
+
| `fullRef` | Optional full reference string for the trigger. |
|
|
128
130
|
|
|
129
131
|
## Typical handler pattern
|
|
130
132
|
|
|
131
|
-
Most handlers follow the same structure: initialize state
|
|
133
|
+
Most LLM handlers follow the same structure: initialize missing state idempotently, configure the agent, run the agent.
|
|
132
134
|
|
|
133
135
|
```ts
|
|
134
|
-
registry.define(
|
|
135
|
-
description:
|
|
136
|
+
registry.define("assistant", {
|
|
137
|
+
description: "A general-purpose assistant",
|
|
136
138
|
state: {
|
|
137
|
-
status: { primaryKey:
|
|
139
|
+
status: { primaryKey: "key" },
|
|
138
140
|
},
|
|
139
141
|
|
|
140
142
|
async handler(ctx) {
|
|
141
|
-
if (ctx.
|
|
142
|
-
ctx.db.actions.status_insert({ row: { key:
|
|
143
|
+
if (!ctx.db.collections.status.get("current")) {
|
|
144
|
+
ctx.db.actions.status_insert({ row: { key: "current", value: "idle" } })
|
|
143
145
|
}
|
|
144
146
|
|
|
145
147
|
ctx.useAgent({
|
|
146
|
-
systemPrompt:
|
|
147
|
-
model:
|
|
148
|
+
systemPrompt: "You are a helpful assistant.",
|
|
149
|
+
model: "claude-sonnet-4-5-20250929",
|
|
148
150
|
tools: [...ctx.electricTools],
|
|
149
151
|
})
|
|
150
152
|
await ctx.agent.run()
|
|
@@ -163,25 +165,31 @@ interface AgentConfig {
|
|
|
163
165
|
provider?: KnownProvider
|
|
164
166
|
tools: AgentTool[]
|
|
165
167
|
streamFn?: StreamFn
|
|
168
|
+
getApiKey?: (provider: string) => Promise<string | undefined> | string | undefined
|
|
169
|
+
onPayload?: SimpleStreamOptions["onPayload"]
|
|
166
170
|
testResponses?: string[] | TestResponseFn
|
|
167
171
|
}
|
|
168
172
|
```
|
|
169
173
|
|
|
170
|
-
## firstWake
|
|
174
|
+
## firstWake and initialization
|
|
171
175
|
|
|
172
|
-
`ctx.firstWake` is `true`
|
|
176
|
+
`ctx.firstWake` is `true` during the initial setup pass while the entity has no persisted manifest entries. It is useful for setup that creates manifest-backed resources such as `ctx.spawn()`, `ctx.observe()`, `ctx.mkdb()`, context entries, or schedules.
|
|
177
|
+
|
|
178
|
+
For plain state rows, prefer checking the collection itself so initialization stays idempotent even for entities that do not create manifest entries:
|
|
173
179
|
|
|
174
180
|
```ts
|
|
175
181
|
async handler(ctx) {
|
|
176
|
-
if (ctx.
|
|
182
|
+
if (!ctx.db.collections.status.get("current")) {
|
|
177
183
|
ctx.db.actions.status_insert({ row: { key: 'current', value: 'idle' } })
|
|
184
|
+
}
|
|
185
|
+
if (!ctx.db.collections.counters.get("runs")) {
|
|
178
186
|
ctx.db.actions.counters_insert({ row: { key: 'runs', value: 0 } })
|
|
179
187
|
}
|
|
180
188
|
// ...
|
|
181
189
|
}
|
|
182
190
|
```
|
|
183
191
|
|
|
184
|
-
|
|
192
|
+
After an entity persists manifest entries, subsequent wakes set `firstWake` to `false`.
|
|
185
193
|
|
|
186
194
|
## sleep
|
|
187
195
|
|
|
@@ -200,6 +208,24 @@ async handler(ctx, wake) {
|
|
|
200
208
|
}
|
|
201
209
|
```
|
|
202
210
|
|
|
211
|
+
## recordRun
|
|
212
|
+
|
|
213
|
+
Call `ctx.recordRun()` when a handler does work without `ctx.agent.run()` but still needs to publish run lifecycle events. This is how non-LLM entities can wake parents observing them with `wake: "runFinished"`.
|
|
214
|
+
|
|
215
|
+
```ts
|
|
216
|
+
async handler(ctx) {
|
|
217
|
+
const run = ctx.recordRun()
|
|
218
|
+
try {
|
|
219
|
+
const result = await runExternalJob()
|
|
220
|
+
run.attachResponse(result.summary)
|
|
221
|
+
run.end({ status: "completed" })
|
|
222
|
+
} catch (error) {
|
|
223
|
+
run.end({ status: "failed", finishReason: "error" })
|
|
224
|
+
throw error
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
```
|
|
228
|
+
|
|
203
229
|
## Using spawn args
|
|
204
230
|
|
|
205
231
|
Arguments passed at spawn time are available as `ctx.args`. This is how you parameterize entity behavior:
|
|
@@ -260,8 +286,8 @@ async handler(ctx) {
|
|
|
260
286
|
Use `ctx.send()` to deliver a message to another entity's inbox:
|
|
261
287
|
|
|
262
288
|
```ts
|
|
263
|
-
ctx.send(
|
|
264
|
-
ctx.send(
|
|
289
|
+
ctx.send("/worker/task-1", { action: "process", data: payload })
|
|
290
|
+
ctx.send("/worker/task-1", payload, { type: "custom_type" })
|
|
265
291
|
```
|
|
266
292
|
|
|
267
293
|
The target entity will be woken to process the message.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@electric-ax/agents",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Built-in Electric Agents runtimes such as Horton and worker",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -28,31 +28,33 @@
|
|
|
28
28
|
"./package.json": "./package.json"
|
|
29
29
|
},
|
|
30
30
|
"dependencies": {
|
|
31
|
-
"@anthropic-ai/sdk": "^0.78.0",
|
|
32
31
|
"@durable-streams/state": "npm:@electric-ax/durable-streams-state-beta@^0.3.1",
|
|
33
32
|
"@mariozechner/pi-agent-core": "^0.70.2",
|
|
34
33
|
"@mariozechner/pi-ai": "^0.70.2",
|
|
35
34
|
"@sinclair/typebox": "^0.34.48",
|
|
36
|
-
"agent-session-protocol": "^0.0.2",
|
|
37
35
|
"better-sqlite3": "^11.10.0",
|
|
38
36
|
"nanoid": "^3.3.11",
|
|
39
37
|
"pino": "^10.3.1",
|
|
40
38
|
"pino-pretty": "^13.0.0",
|
|
41
39
|
"sqlite-vec": "^0.1.9",
|
|
42
40
|
"zod": "^4.3.6",
|
|
43
|
-
"@electric-ax/agents-
|
|
41
|
+
"@electric-ax/agents-mcp": "0.2.0",
|
|
42
|
+
"@electric-ax/agents-runtime": "0.1.3"
|
|
44
43
|
},
|
|
45
44
|
"devDependencies": {
|
|
46
45
|
"@types/better-sqlite3": "^7.6.13",
|
|
47
46
|
"@types/node": "^22.19.15",
|
|
48
47
|
"@vitest/coverage-v8": "^4.1.0",
|
|
48
|
+
"cross-env": "^10.1.0",
|
|
49
49
|
"tsdown": "^0.9.0",
|
|
50
|
+
"tsx": "^4.19.0",
|
|
50
51
|
"typescript": "^5.0.0",
|
|
51
52
|
"vitest": "^4.1.0"
|
|
52
53
|
},
|
|
53
54
|
"files": [
|
|
54
55
|
"dist",
|
|
55
56
|
"docs",
|
|
57
|
+
"scripts",
|
|
56
58
|
"skills"
|
|
57
59
|
],
|
|
58
60
|
"sideEffects": false,
|
|
@@ -60,6 +62,9 @@
|
|
|
60
62
|
"scripts": {
|
|
61
63
|
"build": "tsdown",
|
|
62
64
|
"dev": "tsdown --watch",
|
|
65
|
+
"start": "cross-env ELECTRIC_AGENTS_SERVER_URL=http://localhost:4437 tsx --watch src/entrypoint.ts",
|
|
66
|
+
"docs:sync": "node scripts/sync-docs.mjs",
|
|
67
|
+
"docs:clean": "node scripts/sync-docs.mjs --clean",
|
|
63
68
|
"test": "vitest run",
|
|
64
69
|
"coverage": "pnpm exec vitest --coverage",
|
|
65
70
|
"typecheck": "tsc --noEmit",
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import fs from 'node:fs/promises'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
import { fileURLToPath } from 'node:url'
|
|
4
|
+
|
|
5
|
+
const packageRoot = path.resolve(
|
|
6
|
+
path.dirname(fileURLToPath(import.meta.url)),
|
|
7
|
+
`..`
|
|
8
|
+
)
|
|
9
|
+
const repoRoot = path.resolve(packageRoot, `../..`)
|
|
10
|
+
const source = path.join(repoRoot, `website/docs/agents`)
|
|
11
|
+
const target = path.join(packageRoot, `docs`)
|
|
12
|
+
|
|
13
|
+
async function pathExists(value) {
|
|
14
|
+
try {
|
|
15
|
+
await fs.access(value)
|
|
16
|
+
return true
|
|
17
|
+
} catch {
|
|
18
|
+
return false
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async function clean() {
|
|
23
|
+
await fs.rm(target, { recursive: true, force: true })
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (process.argv.includes(`--clean`)) {
|
|
27
|
+
if (await pathExists(path.join(source, `index.md`))) {
|
|
28
|
+
await clean()
|
|
29
|
+
}
|
|
30
|
+
} else {
|
|
31
|
+
if (!(await pathExists(path.join(source, `index.md`)))) {
|
|
32
|
+
if (await pathExists(path.join(target, `index.md`))) {
|
|
33
|
+
console.log(`Agents docs source not found; preserving ${target}`)
|
|
34
|
+
process.exit(0)
|
|
35
|
+
}
|
|
36
|
+
throw new Error(`Agents docs source not found at ${source}`)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
await clean()
|
|
40
|
+
await fs.cp(source, target, { recursive: true })
|
|
41
|
+
console.log(`Synced agents docs from ${source} to ${target}`)
|
|
42
|
+
}
|
|
@@ -1,106 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
title: Mega Draw
|
|
3
|
-
titleTemplate: '... - Electric Agents'
|
|
4
|
-
description: >-
|
|
5
|
-
Multi-agent collaborative drawing example with coordinator-worker patterns and 100 tile agents.
|
|
6
|
-
outline: [2, 3]
|
|
7
|
-
---
|
|
8
|
-
|
|
9
|
-
# Mega Draw
|
|
10
|
-
|
|
11
|
-
A collaborative multi-agent drawing app where 100 AI agents each own a tile of a shared 1000x1000 pixel canvas and work together to produce a drawing from a single text prompt. Located at `examples/mega-draw/` in the repository.
|
|
12
|
-
|
|
13
|
-
## What it demonstrates
|
|
14
|
-
|
|
15
|
-
- **Coordinator + worker pattern** at scale (1 coordinator spawning 100 tile agents)
|
|
16
|
-
- **Custom drawing tools** --- `fill_rect`, `draw_line`, `draw_circle`, `fill_gradient`, `set_pixels`
|
|
17
|
-
- **Shared canvas** --- in-memory pixel buffer flushed to PNG, served via a live viewer
|
|
18
|
-
- **Follow-up instructions** --- send a new prompt and only affected tiles get re-instructed
|
|
19
|
-
- **Two-pass workflow** --- coordinator does a quick first pass for backgrounds, then a detail pass
|
|
20
|
-
|
|
21
|
-
## Architecture
|
|
22
|
-
|
|
23
|
-
```
|
|
24
|
-
User
|
|
25
|
-
│
|
|
26
|
-
│ spawn /coordinator/my-drawing
|
|
27
|
-
│ send "Draw a sunset over mountains"
|
|
28
|
-
▼
|
|
29
|
-
┌────────────────────────────────┐
|
|
30
|
-
│ Coordinator Agent │
|
|
31
|
-
│ - Receives prompt │
|
|
32
|
-
│ - Plans composition + palette │
|
|
33
|
-
│ - Spawns 100 tile agents │
|
|
34
|
-
│ - Can re-instruct tiles │
|
|
35
|
-
└──────────┬─────────────────────┘
|
|
36
|
-
│ spawn tile-agent (10×10 grid)
|
|
37
|
-
▼
|
|
38
|
-
┌────────┐ ┌────────┐
|
|
39
|
-
│Tile 0,0│ │Tile 1,0│ ... (10 columns)
|
|
40
|
-
└────────┘ └────────┘
|
|
41
|
-
┌────────┐ ┌────────┐
|
|
42
|
-
│Tile 0,1│ │Tile 1,1│ ...
|
|
43
|
-
└────────┘ └────────┘
|
|
44
|
-
... ... (10 rows = 100 tiles)
|
|
45
|
-
```
|
|
46
|
-
|
|
47
|
-
Each tile agent:
|
|
48
|
-
|
|
49
|
-
- Owns a 100x100 pixel region
|
|
50
|
-
- Can **see** 50px beyond its borders (200x200 viewport) for edge coordination
|
|
51
|
-
- Can only **draw** within its own tile
|
|
52
|
-
- Receives drawing instructions from the coordinator
|
|
53
|
-
|
|
54
|
-
## Key files
|
|
55
|
-
|
|
56
|
-
### `src/server.ts`
|
|
57
|
-
|
|
58
|
-
Entry point. Creates the registry, runtime handler, and two HTTP servers (one for the Electric Agents webhook, one for the canvas viewer).
|
|
59
|
-
|
|
60
|
-
```ts
|
|
61
|
-
const registry = createEntityRegistry()
|
|
62
|
-
registerCoordinator(registry, WEB_PORT)
|
|
63
|
-
registerTileAgent(registry)
|
|
64
|
-
|
|
65
|
-
const runtime = createRuntimeHandler({
|
|
66
|
-
baseUrl: ELECTRIC_AGENTS_URL,
|
|
67
|
-
serveEndpoint: `${SERVE_URL}/webhook`,
|
|
68
|
-
registry,
|
|
69
|
-
})
|
|
70
|
-
```
|
|
71
|
-
|
|
72
|
-
### `src/coordinator.ts`
|
|
73
|
-
|
|
74
|
-
The coordinator entity. Defines two custom tools:
|
|
75
|
-
|
|
76
|
-
- `set_drawing_plan` --- sets the composition description and color palette
|
|
77
|
-
- `instruct_tile` --- spawns or re-instructs a tile agent with drawing directions
|
|
78
|
-
|
|
79
|
-
### `src/tile-agent.ts`
|
|
80
|
-
|
|
81
|
-
The tile agent entity. Each instance gets drawing tools scoped to its tile:
|
|
82
|
-
|
|
83
|
-
- `read_viewport` --- see current pixel state (own tile + neighbors)
|
|
84
|
-
- `fill_rect`, `draw_line`, `draw_circle`, `fill_gradient`, `set_pixels`
|
|
85
|
-
|
|
86
|
-
All coordinates are tile-relative (0--99) and automatically clipped to tile bounds.
|
|
87
|
-
|
|
88
|
-
## Running it
|
|
89
|
-
|
|
90
|
-
```bash
|
|
91
|
-
cd examples/mega-draw
|
|
92
|
-
pnpm install
|
|
93
|
-
cp ../../.env.template .env # Set ANTHROPIC_API_KEY
|
|
94
|
-
pnpm dev
|
|
95
|
-
```
|
|
96
|
-
|
|
97
|
-
Requires a running Electric Agents runtime server at `http://localhost:4437`.
|
|
98
|
-
|
|
99
|
-
Then in another terminal:
|
|
100
|
-
|
|
101
|
-
```bash
|
|
102
|
-
npx electric-ax agents spawn /coordinator/my-drawing
|
|
103
|
-
npx electric-ax agents send /coordinator/my-drawing 'Draw a sunset over mountains'
|
|
104
|
-
```
|
|
105
|
-
|
|
106
|
-
View the canvas live at `http://localhost:3000/my-drawing` --- it auto-refreshes as tiles draw.
|