wolli 0.0.3 → 0.0.4

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/docs/sdk.md CHANGED
@@ -7,7 +7,7 @@ The SDK provides programmatic access to a wolli agent. It has two faces, and whi
7
7
 
8
8
  > Wolli's RPC transport is **HTTP/SSE over a loopback socket**, not stdin/stdout JSONL. There is no `--mode rpc` subprocess. A client attaches to a running daemon over `http://127.0.0.1:<port>`; the daemon owns the agent's lifecycle and outlives any one client.
9
9
 
10
- If you are building a client against an already-deployed agent, you almost always want the daemon face — start at [Quick Start](#quick-start). If you are embedding the engine itself, read [Core Concepts](#core-concepts).
10
+ If you are building a client against an existing agent, you almost always want the daemon face — start at [Quick Start](#quick-start). If you are embedding the engine itself, read [Core Concepts](#core-concepts).
11
11
 
12
12
  ## Table of Contents
13
13
 
@@ -132,7 +132,7 @@ const { repo, session, env, cwd } = await openAgentSession("my-agent", {
132
132
  });
133
133
  ```
134
134
 
135
- `AgentRuntime` (exported as `AgentRuntime`, `AgentRuntimeOptions`) is the **daemon's internal engine**. It owns N resident sessions keyed by id, the single extension + integration runners, the model registry, auth, reload, and cleanup. The daemon (`runDaemon`) constructs one `AgentRuntime` and wraps the [HTTP/SSE routes](#rpc-mode) around it; `AgentRuntime` is what `createAgentSession` ultimately feeds.
135
+ `AgentRuntime` (exported as `AgentRuntime`, `AgentRuntimeOptions`) is the **daemon's internal engine**. It owns N resident sessions keyed by id, the single integration, workflow, and hook runners, the model registry, auth, reload, and cleanup. The daemon (`runDaemon`) constructs one `AgentRuntime` and wraps the [HTTP/SSE routes](#rpc-mode) around it; `AgentRuntime` is what `createAgentSession` ultimately feeds.
136
136
 
137
137
  > `AgentRuntime` and its per-session `AgentSession` are documented here as the engine the daemon runs, **not** as an embedding path. They are not a turnkey `new AgentRuntime(...)` SDK surface — their options (`authStorage`, `integrationAccounts`, `integrationStore`, a resolved `model`) are the daemon's to assemble. To embed in-process, use `createAgentSession()` + the harness; to drive an agent remotely, use the [daemon client](#rpc-mode).
138
138
 
@@ -195,7 +195,7 @@ harness.subscribe((event) => {
195
195
  });
196
196
  ```
197
197
 
198
- > Wolli does **not** emit `compaction_start`/`compaction_end`, `auto_retry_start`/`auto_retry_end`, or `extension_error` on the forwarded stream. It **does** add `model_update`, `thinking_level_update`, and (over the daemon) `scoped_models_update`. See [Events over the daemon](#events-1) for the exact forwarded allowlist.
198
+ > Wolli does **not** emit `compaction_start`/`compaction_end` or `auto_retry_start`/`auto_retry_end` on the forwarded stream. It **does** add `model_update`, `thinking_level_update`, and (over the daemon) `scoped_models_update`. See [Events over the daemon](#events-1) for the exact forwarded allowlist.
199
199
 
200
200
  ## Options Reference
201
201
 
@@ -249,7 +249,7 @@ Build the frozen prompt with `buildSystemPrompt()`, then pass the resulting stri
249
249
  import { buildSystemPrompt } from "@opsyhq/wolli";
250
250
 
251
251
  const systemPrompt = buildSystemPrompt({
252
- config, // AgentConfig (name, purpose, deployedAt)
252
+ config, // AgentConfig (name)
253
253
  soul, // frozen SOUL.md snapshot ("" when absent)
254
254
  memory, // frozen MEMORY.md snapshot
255
255
  user, // frozen USER.md snapshot
@@ -259,7 +259,7 @@ const systemPrompt = buildSystemPrompt({
259
259
  });
260
260
  ```
261
261
 
262
- The prompt is composed from the agent's identity (name + purpose), a frozen snapshot of curated memory (SOUL / MEMORY / USER), a deployed-vs-forming instruction block, and a docs-guidance block. It is **frozen for the session's lifetime** — edits to memory take effect next session.
262
+ The prompt is composed from the agent's identity (its name), a frozen snapshot of curated memory (SOUL / MEMORY / USER), an onboarding block while the SOUL.md snapshot is empty, and a docs-guidance block. It is **frozen for the session's lifetime** — edits to memory take effect next session.
263
263
 
264
264
  ### Tools
265
265
 
@@ -269,38 +269,37 @@ Pass an `AgentTool[]` to `createAgentSession({ tools })`. Wolli ships built-in t
269
269
  import {
270
270
  createReadTool, createWriteTool, createEditTool,
271
271
  createBashTool, createGrepTool, createFindTool, createLsTool,
272
- createMemoryTool, createDeployTool,
272
+ createMemoryTool,
273
273
  } from "@opsyhq/wolli";
274
274
  ```
275
275
 
276
276
  > There is no `tools: ["read", "bash"]` string allowlist and no `noTools`/`excludeTools` here — you build the `AgentTool[]` explicitly and pass it. The daemon assembles the active set itself; over the client, read it with [`listTools()`](#sessionhandle).
277
277
 
278
- ### Extensions, Skills, Context Files, Slash Commands
278
+ ### Skills, Context Files, Slash Commands
279
279
 
280
- `createAgentSession()` accepts `resources?: AgentHarnessResources` — skills and prompt templates pre-mapped into the harness shapes for explicit invocation (`harness.skill()` / `harness.promptFromTemplate()`). Full discovery (extensions, skills, prompt templates, integrations) is the `AgentRuntime`'s job, not a `createAgentSession()` option.
280
+ `createAgentSession()` accepts `resources?: AgentHarnessResources` — skills and prompt templates pre-mapped into the harness shapes for explicit invocation (`harness.skill()` / `harness.promptFromTemplate()`). Full discovery (integrations, workflows, hooks, tools, providers, skills, prompt templates) is the `AgentRuntime`'s job, not a `createAgentSession()` option.
281
281
 
282
282
  Helpers for assembling resources yourself:
283
283
 
284
284
  ```typescript
285
- import { loadSkills, BUILTIN_SLASH_COMMANDS, discoverAndLoadExtensions } from "@opsyhq/wolli";
285
+ import { loadSkills, BUILTIN_SLASH_COMMANDS } from "@opsyhq/wolli";
286
286
  ```
287
287
 
288
- Over the daemon client, inspect the resolved set with [`listSkills()` / `listContexts()` / `getCommands()` / `listTools()` / `listIntegrations()`](#sessionhandle). See [extensions.md](extensions.md), [skills.md](skills.md), and [integrations.md](integrations.md).
288
+ Over the daemon client, inspect the resolved set with [`listSkills()` / `listContexts()` / `getCommands()` / `listTools()` / `listIntegrations()`](#sessionhandle). See [skills.md](skills.md) and [integrations.md](integrations.md).
289
289
 
290
290
  ### Session Management
291
291
 
292
- In-process, sessions come from [`openAgentSession()`](#openagentsession-and-agentruntime-internal-engine) (resume latest / by id / fresh) backed by a `JsonlSessionRepo` tree. Read a stored session tree with `SessionManager`. Over the daemon, the runtime owns session replacement — see [`Agent.createSession()`](#agent) and the `create_session` command. A **forming** (not-yet-deployed) agent refuses new sessions: it stays in its birth session until `deploy`.
292
+ In-process, sessions come from [`openAgentSession()`](#openagentsession-and-agentruntime-internal-engine) (resume latest / by id / fresh) backed by a `JsonlSessionRepo` tree. Read a stored session tree with `SessionManager`. Over the daemon, the runtime owns session replacement — see [`Agent.createSession()`](#agent) and the `create_session` command.
293
293
 
294
294
  ### Settings Management
295
295
 
296
- `AgentSettingsManager` reads and writes the agent's `agent.json` (`AgentConfig`: name, purpose, createdAt, port, token, the `deployedAt` deploy latch, and a `settings` override block — the default model lives in `settings.defaultModel`, read via `getDefaultModel()`) and the shared settings.
296
+ `AgentSettingsManager` reads and writes the agent's `agent.json` (`AgentConfig`: name, createdAt, port, token, and a `settings` override block — the default model lives in `settings.defaultModel`, read via `getDefaultModel()`) and the shared settings.
297
297
 
298
298
  ```typescript
299
299
  import { AgentSettingsManager } from "@opsyhq/wolli";
300
300
 
301
301
  const store = AgentSettingsManager.create("my-agent");
302
302
  store.getDefaultModel();
303
- store.getAgentDeployed();
304
303
  ```
305
304
 
306
305
  ## Return Value
@@ -313,7 +312,7 @@ interface CreateAgentSessionResult {
313
312
  }
314
313
  ```
315
314
 
316
- That is the whole result — no extensions result, no model-fallback message. Everything else (events, model state, the session tree) is reached through the harness and the `session`/`repo` you passed in.
315
+ That is the whole result — no model-fallback message. Everything else (events, model state, the session tree) is reached through the harness and the `session`/`repo` you passed in.
317
316
 
318
317
  ## Complete Example
319
318
 
@@ -410,7 +409,7 @@ AuthStorage, ModelRegistry // credentials + model resolution
410
409
  AgentSettingsManager // agent.json (AgentConfig)
411
410
  SessionManager // session tree
412
411
  createReadTool, createWriteTool, createEditTool, createBashTool,
413
- createGrepTool, createFindTool, createLsTool, createMemoryTool, createDeployTool
412
+ createGrepTool, createFindTool, createLsTool, createMemoryTool
414
413
 
415
414
  // ── Daemon engine (internal, but exported) ──
416
415
  AgentRuntime, type AgentRuntimeOptions
@@ -442,7 +441,7 @@ GET /events (SSE) root control stream: agent snapshot + sessio
442
441
  GET /sessions the session list (DaemonAgentState)
443
442
  GET /sessions/:id/events (SSE) one session's curated event stream (attaching makes it live)
444
443
  POST /sessions/:id/control a command for that session; its sync response is the body
445
- POST /sessions/:id/ui-response a client's answer to that session's parked extension dialog
444
+ POST /sessions/:id/ui-response a client's answer to that session's parked dialog
446
445
  GET /health liveness; the only route with no auth
447
446
  ```
448
447
 
@@ -469,7 +468,7 @@ Authorization: Bearer <token>
469
468
 
470
469
  **Correlation id.** Set `id` on a command to match its response; events never carry an `id`.
471
470
 
472
- **Replay ring.** Each session's broadcaster keeps the last **256** events (`RING_SIZE`) with a monotonic sequence id as the SSE `id:`. On reconnect, send `Last-Event-ID: <n>` and the daemon replays buffered frames with `id > n` (bounded by the watermark captured at attach, so live and replayed frames stay disjoint). Extension-UI request frames and control-stream lifecycle frames carry **no** `id` and are not replayable.
471
+ **Replay ring.** Each session's broadcaster keeps the last **256** events (`RING_SIZE`) with a monotonic sequence id as the SSE `id:`. On reconnect, send `Last-Event-ID: <n>` and the daemon replays buffered frames with `id > n` (bounded by the watermark captured at attach, so live and replayed frames stay disjoint). Dialog-UI request frames and control-stream lifecycle frames carry **no** `id` and are not replayable.
473
472
 
474
473
  ### Commands
475
474
 
@@ -483,16 +482,15 @@ Authorization: Bearer <token>
483
482
  | | `compact` | `{ customInstructions? }` |
484
483
  | | `wait_for_idle` | resolves when the turn loop is idle |
485
484
  | | `clear_queue` | returns the cleared `{ steering, followUp }` |
486
- | Session | `create_session` | additive; returns the new session snapshot; a forming agent refuses |
487
- | | `reload` | re-discover extensions/skills/prompts and rebuild the runner |
488
- | | `deploy` | flip the deploy latch, install the OS unit, swap to a fresh deployed session |
485
+ | Session | `create_session` | additive; returns the new session snapshot |
486
+ | | `reload` | re-discover integrations/workflows/hooks/tools/providers/skills/prompts and rebuild the runners |
489
487
  | | `shutdown` | ack, then self-exit (frees the fixed port) |
490
488
  | State | `get_state` | the session snapshot |
491
489
  | | `get_messages` / `get_entries` | conversation messages / tree entries |
492
- | | `get_commands` | slash commands (extension + prompt + skill) |
490
+ | | `get_commands` | slash commands (prompt + skill) |
493
491
  | | `get_resource_summary` | counts + diagnostics |
494
492
  | | `get_tool_info` / `get_integration_info` / `get_skills` / `get_plugins` / `get_context_info` | capability reads |
495
- | Mutation | `seed_assistant_message` / `append_message` | birth opener seed / resumed-message append |
493
+ | Mutation | `seed_assistant_message` / `append_message` | opener seed / resumed-message append |
496
494
  | Plugins | `install_plugin` / `remove_plugin` / `update_plugins` | single-writer; the daemon reloads itself after |
497
495
  | | `onboard_plugin` | runs the just-installed plugin's integration onboarding |
498
496
  | Model | `set_thinking_level` | `{ level }` |
@@ -502,7 +500,7 @@ Authorization: Bearer <token>
502
500
  | Auth | `login` / `logout` | `{ provider, authType }`; runs daemon-side, credentials never cross the wire |
503
501
  | | `get_login_providers` / `get_logout_providers` | eligible providers |
504
502
 
505
- > This is wolli's set, not pi's. There is no `cycle_model`, `cycle_thinking_level`, `set_steering_mode`/`set_follow_up_mode`, `bash`, `fork`/`clone`/`switch_session`, `export_html`, `get_session_stats`, or `set_session_name`. Wolli adds `deploy`, `shutdown`, `reload`, `create_session`, `install_plugin`/`remove_plugin`/`update_plugins`/`onboard_plugin`, `login`/`logout`/`get_login_providers`/`get_logout_providers`, `get_available_models`, `set_scoped_models`/`set_enabled_models`, and the granular `get_*_info` reads.
503
+ > This is wolli's set, not pi's. There is no `cycle_model`, `cycle_thinking_level`, `set_steering_mode`/`set_follow_up_mode`, `bash`, `fork`/`clone`/`switch_session`, `export_html`, `get_session_stats`, or `set_session_name`. Wolli adds `shutdown`, `reload`, `create_session`, `install_plugin`/`remove_plugin`/`update_plugins`/`onboard_plugin`, `login`/`logout`/`get_login_providers`/`get_logout_providers`, `get_available_models`, `set_scoped_models`/`set_enabled_models`, and the granular `get_*_info` reads.
506
504
 
507
505
  Example — `set_model`:
508
506
 
@@ -526,7 +524,7 @@ Each session streams a **curated subset** of the harness's event surface out of
526
524
  | `thinking_level_update` | thinking level changed (`.level`) |
527
525
  | `scoped_models_update` | session model scope changed (`.scopedModels`) — host-originated, not a harness own-event |
528
526
 
529
- > Wolli forwards `model_update`, `thinking_level_update`, and `scoped_models_update` (none of which pi's RPC has) and drops `compaction_*`, `auto_retry_*`, and `extension_error`. `scoped_models_update` is bridged onto the session broadcaster by the runtime after `setScopedModels()` resolves.
527
+ > Wolli forwards `model_update`, `thinking_level_update`, and `scoped_models_update` (none of which pi's RPC has) and drops `compaction_*` and `auto_retry_*`. `scoped_models_update` is bridged onto the session broadcaster by the runtime after `setScopedModels()` resolves.
530
528
 
531
529
  **Control stream (`GET /events`).** A low-volume root stream whose `hello` frame is the agent snapshot (`DaemonAgentState`) and whose later frames are session-lifecycle events, so a client tracking the open-session list never has to poll:
532
530
 
@@ -536,29 +534,29 @@ Each session streams a **curated subset** of the harness's event surface out of
536
534
  { "type": "session_renamed", "sessionId": "abc123", "sessionName": "my-feature-work" }
537
535
  ```
538
536
 
539
- ### Extension UI Protocol
537
+ ### Dialog UI Protocol
540
538
 
541
- When a daemon-side extension calls `ctx.ui.select()`, `ctx.ui.confirm()`, etc., the daemon translates it into a request/response sub-protocol layered on the session stream.
539
+ When a daemon-side workflow or integration calls `ctx.ui.select()`, `ctx.ui.confirm()`, etc., the daemon translates it into a request/response sub-protocol layered on the session stream.
542
540
 
543
- - **Dialog methods** (`select`, `confirm`, `input`, `editor`) push an `extension_ui_request` frame, park a promise keyed by `id`, and block until the client answers with `POST /sessions/:id/ui-response`.
544
- - **Fire-and-forget methods** (`notify`, `setStatus`, `setWidget`, `setTitle`, `setEditorText`) push a request frame with no expected response.
541
+ - **Awaited dialogs** (`select`, `confirm`, `input`) push a `ui_request` frame, park a promise keyed by `id`, and block until the client answers with `POST /sessions/:id/ui-response`.
542
+ - **The fire-and-forget `notify`** pushes a request frame with no expected response.
545
543
 
546
- Request frames are **not** `AgentHarnessEvent`s: they bypass the curated forwarded set and the replay ring (no SSE `id`, so a reconnect never re-delivers a stale dialog). All nine `method` literals are **camelCase** — `select`, `confirm`, `input`, `editor`, `notify`, `setStatus`, `setWidget`, `setTitle`, `setEditorText`.
544
+ Request frames are **not** `AgentHarnessEvent`s: they bypass the curated forwarded set and the replay ring (no SSE `id`, so a reconnect never re-delivers a stale dialog). All four `method` literals are lowercase — `select`, `confirm`, `input`, `notify`.
547
545
 
548
546
  ```json
549
- { "type": "extension_ui_request", "id": "uuid-1", "method": "select",
547
+ { "type": "ui_request", "id": "uuid-1", "method": "select",
550
548
  "title": "Allow command?", "options": ["Allow", "Block"], "timeout": 10000 }
551
549
  ```
552
550
 
553
551
  Answer (`POST /sessions/:id/ui-response`):
554
552
 
555
553
  ```json
556
- { "type": "extension_ui_response", "id": "uuid-1", "value": "Allow" }
557
- { "type": "extension_ui_response", "id": "uuid-2", "confirmed": true }
558
- { "type": "extension_ui_response", "id": "uuid-3", "cancelled": true }
554
+ { "type": "ui_response", "id": "uuid-1", "value": "Allow" }
555
+ { "type": "ui_response", "id": "uuid-2", "confirmed": true }
556
+ { "type": "ui_response", "id": "uuid-3", "cancelled": true }
559
557
  ```
560
558
 
561
- A dialog with a `timeout` (ms) auto-resolves to its default when it expires. Surfaces that need real TUI access are degraded daemon-side: `custom()` returns `undefined`; `getEditorText()` returns `""`; `getToolsExpanded()` returns `false`; `setWorkingMessage`/`setWorkingIndicator`/`setFooter`/`setHeader`/`setEditorComponent` are no-ops; `pasteToEditor()` delegates to `setEditorText`; the theme family is inert. When the last client detaches, the session's parked dialogs resolve as cancelled (so a signal-less `editor` never hangs forever).
559
+ A dialog with a `timeout` (ms) auto-resolves to its default when it expires. When the last client detaches, the session's parked dialogs resolve as cancelled (so a signal-less dialog never hangs forever).
562
560
 
563
561
  ### Error Handling
564
562
 
@@ -645,9 +643,9 @@ The agent collection on disk — holds no required state.
645
643
  import { Wolli } from "@opsyhq/wolli";
646
644
 
647
645
  const wolli = new Wolli();
648
- wolli.list(); // Agent[] — every agent under the agents root
649
- wolli.get("my-agent"); // Agent | undefined
650
- wolli.create("my-agent", { purpose, model }); // create the home tree, return the handle
646
+ wolli.list(); // Agent[] — every agent under the agents root
647
+ wolli.get("my-agent"); // Agent | undefined
648
+ await wolli.create("my-agent", { model }); // create the home tree, install + start the OS unit, return the handle
651
649
  ```
652
650
 
653
651
  #### Agent
@@ -664,7 +662,6 @@ await agent.getSession(id); // open (or return cached) SessionHandle
664
662
  await agent.getLatestSession(); // the most-recent session (the daemon guarantees one exists)
665
663
 
666
664
  await agent.createSession(); // additive: a fresh session snapshot (caller switches to it)
667
- await agent.deploy(); // commit the deploy + drive the stop-then-start daemon handoff
668
665
  await agent.restart(); // bounce the daemon so it picks up code changes
669
666
  await agent.delete(); // uninstall the OS unit, stop the daemon, delete the home dir
670
667
 
@@ -672,13 +669,13 @@ const off = agent.on("sessionAdded", (s) => { /* … */ }); // control-stream l
672
669
  agent.close(); // close every session stream + the control stream
673
670
  ```
674
671
 
675
- > `connect()` opens **no** session — call `getSession(id)` / `getLatestSession()` afterward. `createSession`/`deploy` are agent-level (they spawn a session and may swap the transport), so they live on `Agent`, not `SessionHandle`.
672
+ > `connect()` opens **no** session — call `getSession(id)` / `getLatestSession()` afterward. `createSession` is agent-level (it spawns a session), so it lives on `Agent`, not `SessionHandle`.
676
673
 
677
674
  #### SessionHandle
678
675
 
679
676
  The per-session proxy. Verbs round-trip through `agent.send(sessionId, …)`; the session's SSE feeds the local snapshot/queue caches.
680
677
 
681
- > **Ordering.** Opening a handle (`getSession`/`getLatestSession`) attaches the SSE stream — which is also what makes the session **live** on the daemon (rehydrating it if idle). Call `subscribe(...)` (and set `onUiRequest`) **before** `prompt(...)`, or you miss the leading deltas and any extension dialog that turn raises. `prompt()` resolves on acceptance; await `waitForIdle()` for completion. When the last handle closes, the daemon evicts the session (unless a turn is still streaming) — so keep the handle open for the whole turn, and remember a closed handle's parked dialogs resolve as cancelled.
678
+ > **Ordering.** Opening a handle (`getSession`/`getLatestSession`) attaches the SSE stream — which is also what makes the session **live** on the daemon (rehydrating it if idle). Call `subscribe(...)` (and set `onUiRequest`) **before** `prompt(...)`, or you miss the leading deltas and any dialog that turn raises. `prompt()` resolves on acceptance; await `waitForIdle()` for completion. When the last handle closes, the daemon evicts the session (unless a turn is still streaming) — so keep the handle open for the whole turn, and remember a closed handle's parked dialogs resolve as cancelled.
682
679
 
683
680
  ```typescript
684
681
  const session = await agent.getLatestSession();
@@ -692,7 +689,7 @@ await session.clearQueue();
692
689
 
693
690
  // Subscribe to the curated session events.
694
691
  const unsubscribe = session.subscribe((event) => { /* AgentHarnessEvent */ });
695
- session.onUiRequest = (req) => { /* extension-UI dialog frame */ };
692
+ session.onUiRequest = (req) => { /* dialog-UI request frame */ };
696
693
  await session.respondUi(req.id, { value: "Allow" });
697
694
 
698
695
  // Reads (cached snapshot vs round-trip).
@@ -723,11 +720,9 @@ await session.onboardPlugin(source);
723
720
  session.close();
724
721
  ```
725
722
 
726
- > The client's extension surface is **inert** — the runner lives server-side: `getShortcuts()` returns an empty map, `getMessageRenderer()` returns `undefined`, `emitUserBash()` resolves `undefined`, and `createShortcutContext()` throws. The only live extension bridge is `onUiRequest` + `respondUi`.
727
-
728
723
  ## Integrations
729
724
 
730
- Integrations (per-agent service connections with their own onboarding and producer loops) are configured and onboarded over the daemon (`onboard_plugin`, `login`) and inspected with `SessionHandle.listIntegrations()`. The integration authoring API (`createIntegrationRuntime`, `Integration`, `IntegrationAction`, onboarding context) is re-exported from the barrel. See [integrations.md](integrations.md) for the full surface.
725
+ Integrations (per-agent service connections with their own onboarding and producer loops) are configured and onboarded over the daemon (`onboard_plugin`, `login`) and inspected with `SessionHandle.listIntegrations()`. The integration authoring API (`defineIntegration`, `Integration`, `IntegrationAction`, onboarding context) is re-exported from the barrel. See [integrations.md](integrations.md) for the full surface.
731
726
 
732
727
  ## Configuration and Environment
733
728
 
@@ -748,13 +743,13 @@ Per-agent on-disk layout (`~/.wolli/agents/<name>/`):
748
743
 
749
744
  ```
750
745
  agents/<name>/
751
- agent.json AgentConfig: name, purpose, port, token, deployedAt latch, settings (incl. defaultModel)
752
- SOUL.md who the agent is / what it's for (authored at deploy)
746
+ agent.json AgentConfig: name, port, token, settings (incl. defaultModel)
747
+ SOUL.md who the agent is / what it's for (authored by the agent; first line is its purpose)
753
748
  MEMORY.md durable notes (edited via the memory tool)
754
749
  USER.md facts about the human
755
750
  sessions/ JsonlSessionRepo session tree
756
751
  workspace/ the stable cwd passed to every session
757
- integrations.json per-(service, account) credential registry
752
+ integrations.json per-service credential registry
758
753
  store/ per-integration runtime state, one file per service
759
754
  approvals.json durable host-escalation prefix rules
760
755
  ```
package/docs/tools.md ADDED
@@ -0,0 +1,81 @@
1
+ # Tools
2
+
3
+ Typed actions the model calls, authored as one file per tool under `tools/`.
4
+
5
+ A tool is a typed action the model can call during a turn (an HTTP request, a conversion, a query against a service you run). Authored tools run in the daemon process alongside the built-in suite: `bash`, `read`, `write`, `edit`, `grep`, `find`, `ls`, and `memory`. The built-ins route file and shell work through the agent's sandboxed environments; author a tool when the model needs an action that lives outside them. wolli loads every file under `tools/` into session tooling at startup. There is no mid-session registration.
6
+
7
+ ## Define a tool
8
+
9
+ One file defines one tool, default-exporting `defineTool`:
10
+
11
+ `~/.wolli/agents/assistant/tools/http_get.ts`
12
+
13
+ ```ts
14
+ import { defineTool } from "wolli";
15
+ import { Type } from "typebox";
16
+
17
+ export default defineTool({
18
+ name: "http_get",
19
+ label: "HTTP GET",
20
+ description:
21
+ "Fetch a URL over HTTP GET and return the status line and body. Bodies over 50KB are truncated.",
22
+ promptSnippet: "Fetch a URL over HTTP GET",
23
+ parameters: Type.Object({
24
+ url: Type.String({ description: "Absolute http(s) URL" }),
25
+ }),
26
+ async execute(toolCallId, { url }, signal) {
27
+ const response = await fetch(url, { signal });
28
+ const body = (await response.text()).slice(0, 50_000);
29
+ return {
30
+ content: [{ type: "text", text: `${response.status} ${response.statusText}\n\n${body}` }],
31
+ details: { status: response.status },
32
+ };
33
+ },
34
+ });
35
+ ```
36
+
37
+ The `name` field is the identifier the model uses in tool calls; keep it matched to the filename. `label` is the human-readable name clients display for the running call. wolli evaluates the file at runtime; there is no build step.
38
+
39
+ ## What the model sees
40
+
41
+ The model decides whether and how to call a tool from the definition's prompt-facing fields. `description` is the contract: state what the tool does, what it returns, and its limits, written for the model. `promptSnippet` is a one-line entry in the Available tools section of the default system prompt; a tool without one stays callable but is not advertised there. `promptGuidelines` appends bullets to the system prompt's Guidelines section while the tool is active, the place for usage rules ("Prefer `http_get` over `bash` with curl.").
42
+
43
+ `parameters` is a TypeBox schema. wolli validates every call's arguments against it before `execute` runs, and per-property `description` strings reach the model, so constraints belong in the schema rather than in prose.
44
+
45
+ ## The `execute` function
46
+
47
+ wolli calls `execute` in the daemon once the arguments pass validation. `toolCallId` identifies this call; the same id marks the call in the session record. `params` arrives typed from the `parameters` schema. `signal` fires when the turn is aborted; pass it to cancellation-aware work, as the example does with `fetch`.
48
+
49
+ Long-running tools stream progress through `onUpdate`, which accepts the same shape as the return value:
50
+
51
+ ```ts
52
+ onUpdate?.({ content: [{ type: "text", text: `fetched ${done} of ${total} pages` }], details: { done } });
53
+ ```
54
+
55
+ Clients render each partial result in place while the call runs. Both `signal` and `onUpdate` are optional to handle; a short tool can ignore them. The fifth argument, `ctx`, carries `session`, the session facade for the session that made the call, and `integration`, which takes an imported [integration](./integrations.md) definition as a typed key and returns its flat action handle — the same resolver workflow handlers get, so a tool can call the actions of the integration it ships with.
56
+
57
+ The return value carries `content`, the text or image parts the model reads, and `details`, structured data for logs and client rendering that stays out of model context. Throw an `Error` on failure instead of encoding it in `content`.
58
+
59
+ ## Execution
60
+
61
+ Tool calls run as steps of the turn. Every call is recorded with its `toolCallId` as a child step under the `session.prompt` step that produced it, so a routed conversation shows exactly which tools each turn used. See [Workflows](./workflows.md) for the run tree these steps land in.
62
+
63
+ `executionMode` controls concurrency within a batch of calls. Set `"sequential"` when a tool must not overlap other tool calls (it mutates shared state, say), or `"parallel"` when overlapping is safe; omitted, the session default applies.
64
+
65
+ Do not return secrets, credentials, or unbounded output from a tool. Everything in `content` enters the model's context and persists in the session record; filter, bound, and redact results before returning them, the way the example caps the body at 50KB.
66
+
67
+ ## Tool, skill, or workflow
68
+
69
+ | Need | Use |
70
+ | --- | --- |
71
+ | A typed action the model invokes mid-turn (an API call, a query) | A tool |
72
+ | A procedure or reference the model reads and follows | A [skill](./skills.md) |
73
+ | Code that fires on events and routes work into sessions | A [workflow](./workflows.md) |
74
+
75
+ A tool extends what the model can do, a skill extends what it knows, and a workflow runs without being asked. Prefer a skill when prose over the built-in suite covers the job; author a tool only when the model needs a new typed action.
76
+
77
+ ## What to read next
78
+
79
+ - [Workflows](./workflows.md): event-triggered routing code and the run tree tool calls nest under
80
+ - [Skills](./skills.md): markdown capability documents the model loads when relevant
81
+ - [Integrations](./integrations.md): transports that connect the agent to outside services
@@ -0,0 +1,170 @@
1
+ # Workflows
2
+
3
+ Route events into sessions and automate the agent from `workflows/`.
4
+
5
+ A workflow is a typed reaction to an event, or a callable operation, authored under `~/.wolli/agents/<name>/workflows/`. A file may define several — one per exported `defineWorkflow` result — and each export's name is the workflow's name (a `default` export takes the filename). Each firing of the trigger is a run, and everything the handler does through `ctx` is recorded as steps. Keep module scope free of state; wolli may reload the file at any time.
6
+
7
+ A chat channel is a few workflows in one file: the inbound router binds each chat to its own session by tag, and the reply ships the answer back on the same tag.
8
+
9
+ `~/.wolli/agents/assistant/workflows/telegram-chat.ts`
10
+
11
+ ```ts
12
+ import { wolli } from "wolli";
13
+ import telegram from "../integrations/telegram";
14
+
15
+ // msg is typed from the event schema
16
+ export const inbound = telegram.on("message", async (msg, ctx) => {
17
+ const chatTag = { "telegram:chat": String(msg.chatId) };
18
+ const [match] = await ctx.agent.findSessions(chatTag);
19
+ const session = match
20
+ ? await ctx.agent.openSession(match.id)
21
+ : await ctx.agent.createSession({
22
+ setup: (s) => s.appendTags(chatTag),
23
+ });
24
+ // followUp queues behind a running turn instead of interrupting it.
25
+ await session.sendUserMessage(msg.text, { deliverAs: "followUp" });
26
+ });
27
+
28
+ export const reply = wolli.on("agent_end", async (evt, ctx) => {
29
+ const chat = ctx.session.getTags()["telegram:chat"];
30
+ if (!chat) return; // not a telegram-bound session
31
+ const text = evt.messages
32
+ .filter((m) => m.role === "assistant")
33
+ .at(-1)
34
+ ?.content.filter((c) => c.type === "text")
35
+ .map((c) => c.text)
36
+ .join("")
37
+ .trim();
38
+ if (!text) return; // a pure tool-call turn sends nothing
39
+ await ctx.integration(telegram).sendMessage({ chatId: Number(chat), text });
40
+ });
41
+ ```
42
+
43
+ Two chats run in parallel because each holds its own tagged session, and any workflow can locate that session later with the same tag query.
44
+
45
+ ## Triggers
46
+
47
+ `.on(...)` and `defineWorkflow(...)` are the two ways to author a workflow. `.on` is the sugar; `defineWorkflow` is the primitive it funnels through, and the only way to write a callable. There are three trigger kinds.
48
+
49
+ An integration event: import the integration and bind one of its events with `.on`, as `inbound` does with `telegram.on("message", ...)`. The event name is typed from the integration, and the payload arrives typed and validated. `telegram.on("message", run)` is exactly `defineWorkflow({ on: telegram.events.message, run })` — the descriptor is inert data carrying the (service, event) address and the payload type.
50
+
51
+ An agent lifecycle event: `wolli.on("<event>", run)`, no integration import needed.
52
+
53
+ `workflows/turn-metrics.ts`
54
+
55
+ ```ts
56
+ import { wolli } from "wolli";
57
+
58
+ // evt is typed via AgentEventMap
59
+ export default wolli.on("turn_end", async (evt, ctx) => {
60
+ console.log(`turn ${evt.turnIndex} ran ${evt.toolResults.length} tools`);
61
+ });
62
+ ```
63
+
64
+ A callable: no trigger, so no object to hang `.on` off — use `defineWorkflow` and declare `input` and `output` TypeBox schemas. The agent invokes a callable workflow by name.
65
+
66
+ `workflows/fetch-page.ts`
67
+
68
+ ```ts
69
+ import { defineWorkflow } from "wolli";
70
+ import { Type } from "typebox";
71
+
72
+ export default defineWorkflow({
73
+ input: Type.Object({ url: Type.String() }),
74
+ output: Type.Object({ excerpt: Type.String() }),
75
+ async run(input) {
76
+ const res = await fetch(input.url);
77
+ return { excerpt: (await res.text()).slice(0, 500) };
78
+ },
79
+ });
80
+ ```
81
+
82
+ ### Lifecycle events
83
+
84
+ This is the complete set. Handlers are observe-only; a workflow watches these events and cannot modify them.
85
+
86
+ | Event | Fires when |
87
+ | --- | --- |
88
+ | `session_start` | a session starts, loads, or reloads |
89
+ | `session_shutdown` | a session is torn down on quit, reload, or replacement |
90
+ | `agent_start` | an agent loop begins processing a prompt |
91
+ | `agent_end` | the loop finishes, with the turn's messages |
92
+ | `turn_start` | a turn inside the loop begins |
93
+ | `turn_end` | a turn ends, with the assistant message and tool results |
94
+ | `message_start` | a user, assistant, or tool-result message begins |
95
+ | `message_update` | an assistant message streams a token update |
96
+ | `message_end` | a message completes |
97
+ | `tool_execution_start` | a tool call starts |
98
+ | `tool_execution_update` | a tool call reports partial output |
99
+ | `tool_execution_end` | a tool call finishes, with its result |
100
+ | `model_select` | the session switches models |
101
+ | `thinking_level_select` | the session changes thinking level |
102
+
103
+ ## The handler and ctx
104
+
105
+ The handler receives the trigger payload and a context scoped to the run. `inbound` already used `ctx.agent`, the this-agent surface: `findSessions(tags)` subset-matches session tags newest first, `openSession(id)` rehydrates a stored session, `createSession(opts)` starts a fresh one, `listSessions()` enumerates them, and `cwd` is the agent home path. Lifecycle-triggered runs also carry the session that produced the event:
106
+
107
+ `workflows/greet-new-session.ts`
108
+
109
+ ```ts
110
+ import { wolli } from "wolli";
111
+ import telegram from "../integrations/telegram";
112
+
113
+ export default wolli.on("session_start", async (evt, ctx) => {
114
+ const chat = ctx.session.getTags()["telegram:chat"]; // the producing session
115
+ if (!chat || evt.reason !== "new") return;
116
+ const text = await ctx.step("compose-greeting", () => "Fresh session ready.");
117
+ await ctx.integration(telegram).sendMessage({ chatId: Number(chat), text });
118
+ });
119
+ ```
120
+
121
+ `ctx.integration(telegram)` takes the imported definition as a typed key and returns a flat action handle; parameters are validated on every call. `ctx.step(name, fn)` wraps inline logic in a named, recorded step. `ctx.signal` is the run's `AbortSignal`; pass it to anything long-running.
122
+
123
+ `ctx.session` exists only on lifecycle-triggered runs, as the facade of the producing session (`prompt`, `sendUserMessage`, `getTags`, `setTags`, plus the read-only `getSessionName()` and `model` a router surfaces, e.g. in a `/status` command). Integration-event and callable runs have no producing session, so the field is absent. The same rule gates `ctx.ui`, four dialog primitives (`select`, `confirm`, `input`, `notify`) available only when `ctx.session` exists. Everywhere else the run is headless; a workflow that needs an answer from a user asks through its channel.
124
+
125
+ ## Runs and steps
126
+
127
+ Each trigger firing creates a run named after the workflow, and every call through `ctx` lands in it as a step: `ctx.agent.*` calls, integration actions, `ctx.step` blocks, and session deliveries. `session.prompt` and `session.sendUserMessage` record one step each; what the prompted agent does inside the turn lives in that session's own history, not in the run. A routed chat message and its reply record like this:
128
+
129
+ ```
130
+ run: inbound (telegram:message)
131
+ step: agent.findSessions (auto)
132
+ step: agent.createSession (auto)
133
+ step: session.sendUserMessage (auto)
134
+ run: reply (agent_end)
135
+ step: integration.call sendMessage (auto)
136
+ ```
137
+
138
+ Step results are data. A step that produces a live object records its identity instead: a session step records the session id, and `ctx` rehydrates the handle when the handler touches it. Keep your own `ctx.step` return values serializable. There is no resume; a run that crashes does not continue, and its record shows how far it got.
139
+
140
+ ## Replies ride the session's tags
141
+
142
+ `reply` reads the tag off the producing session, not off the channel: `ctx.session.getTags()["telegram:chat"]`. Because the reply rides the producing session's tags, the answer returns to the chat that started the turn, not to whoever messaged last. This composes across integrations: when a scheduler `due` event prompts a telegram-tagged session, `agent_end` fires with that session, the tag is present, and the digest lands in the chat. Neither workflow knows the other exists.
143
+
144
+ Channel commands are inline logic in the inbound workflow, not a registration system:
145
+
146
+ ```ts
147
+ // In inbound, before the session lookup:
148
+ if (msg.text.startsWith("/")) return handleCommand(msg, ctx); // /new, /status
149
+ ```
150
+
151
+ ## Failure
152
+
153
+ A thrown handler fails the run. wolli records the failure alongside the steps that completed before it, and does not retry. Catch the errors you can act on inside the handler; let the rest fail the run so the record stays honest.
154
+
155
+ ## Workflow vs tool vs skill vs integration
156
+
157
+ | Need | Use |
158
+ | --- | --- |
159
+ | React to an event, or route it into a session | a workflow |
160
+ | Give the model an action it can call mid-turn | a tool |
161
+ | Teach the agent a procedure in prose | a skill |
162
+ | Speak a service's protocol (transport, events, actions) | an integration |
163
+
164
+ Integrations move bytes; workflows decide where they go. When logic could live in either, put the transport in the integration and the decision in a workflow.
165
+
166
+ ## What to read next
167
+
168
+ - [Integrations](./integrations.md): the transport half, `defineIntegration`, events, and actions.
169
+ - [Tools](./tools.md): typed actions loaded into session tooling.
170
+ - [Skills](./skills.md): markdown capability documents the agent reads.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wolli",
3
- "version": "0.0.3",
3
+ "version": "0.0.4",
4
4
  "description": "Persistent, purposeful agent CLI with memory and identity",
5
5
  "type": "module",
6
6
  "piConfig": {