wolli 0.0.2 → 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/README.md +31 -21
- package/built-in/plugins/discord/README.md +156 -0
- package/built-in/plugins/discord/discord-chat.ts +50 -0
- package/built-in/plugins/discord/index.ts +234 -0
- package/built-in/plugins/discord/package.json +16 -0
- package/built-in/plugins/scheduler/cron.ts +131 -0
- package/built-in/plugins/scheduler/index.ts +240 -0
- package/{plugins → built-in/plugins}/scheduler/package.json +3 -2
- package/built-in/plugins/scheduler/scheduler-due.ts +22 -0
- package/built-in/plugins/telegram/README.md +153 -0
- package/built-in/plugins/telegram/index.ts +349 -0
- package/{plugins → built-in/plugins}/telegram/package.json +2 -2
- package/built-in/plugins/telegram/telegram-chat.ts +93 -0
- package/built-in/skills/.gitkeep +0 -0
- package/dist/cli.js +5739 -5848
- package/docs/hooks.md +103 -0
- package/docs/index.md +10 -8
- package/docs/integrations.md +78 -663
- package/docs/introduction.md +95 -0
- package/docs/plugins.md +43 -36
- package/docs/providers.md +63 -0
- package/docs/sdk.md +42 -47
- package/docs/skills.md +14 -0
- package/docs/tools.md +81 -0
- package/docs/workflows.md +170 -0
- package/package.json +2 -2
- package/docs/extensions.md +0 -2331
- package/plugins/scheduler/index.ts +0 -244
- package/plugins/scheduler/scheduler-chat.ts +0 -164
- package/plugins/telegram/index.ts +0 -311
- package/plugins/telegram/telegram-chat.ts +0 -169
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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,
|
|
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
|
-
###
|
|
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 (
|
|
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
|
|
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 [
|
|
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.
|
|
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,
|
|
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
|
|
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
|
|
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
|
|
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).
|
|
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
|
|
487
|
-
| | `reload` | re-discover
|
|
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 (
|
|
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` |
|
|
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 `
|
|
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_
|
|
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
|
-
###
|
|
537
|
+
### Dialog UI Protocol
|
|
540
538
|
|
|
541
|
-
When a daemon-side
|
|
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
|
-
- **
|
|
544
|
-
- **
|
|
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
|
|
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": "
|
|
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": "
|
|
557
|
-
{ "type": "
|
|
558
|
-
{ "type": "
|
|
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.
|
|
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();
|
|
649
|
-
wolli.get("my-agent");
|
|
650
|
-
wolli.create("my-agent", {
|
|
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
|
|
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
|
|
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) => { /*
|
|
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 (`
|
|
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,
|
|
752
|
-
SOUL.md who the agent is / what it's for (authored
|
|
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-
|
|
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/skills.md
CHANGED
|
@@ -7,6 +7,7 @@ Wolli implements the [Agent Skills standard](https://agentskills.io/specificatio
|
|
|
7
7
|
## Table of Contents
|
|
8
8
|
|
|
9
9
|
- [Locations](#locations)
|
|
10
|
+
- [Built-in Skills](#built-in-skills)
|
|
10
11
|
- [How Skills Work](#how-skills-work)
|
|
11
12
|
- [Skill Commands](#skill-commands)
|
|
12
13
|
- [Skill Structure](#skill-structure)
|
|
@@ -45,6 +46,19 @@ To use skills from Claude Code or OpenAI Codex, add their directories to setting
|
|
|
45
46
|
}
|
|
46
47
|
```
|
|
47
48
|
|
|
49
|
+
## Built-in Skills
|
|
50
|
+
|
|
51
|
+
Wolli ships a catalog of ready-made skills in the package's `built-in/skills/` directory. These are **not loaded automatically** — they are a library to install from, the same way bundled plugins are. The agent browses them and installs the ones it wants.
|
|
52
|
+
|
|
53
|
+
Installing a built-in skill is just a copy into the agent's own `skills/` folder (the agent's working directory is its home, so `skills/` is relative):
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
mkdir -p skills
|
|
57
|
+
cp -r <built-in-skills-dir>/<name> skills/
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
The agent is told the absolute `<built-in-skills-dir>` path in its system prompt. After the copy the skill loads on the next session (or after `/reload`), at which point it appears in the available-skills list and as `/skill:<name>`. The copy lives in the agent's home, so the agent owns it and can edit it freely without affecting the built-in original.
|
|
61
|
+
|
|
48
62
|
## How Skills Work
|
|
49
63
|
|
|
50
64
|
1. At startup, Wolli scans skill locations and extracts names and descriptions
|
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
|
+
"version": "0.0.4",
|
|
4
4
|
"description": "Persistent, purposeful agent CLI with memory and identity",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"piConfig": {
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
},
|
|
13
13
|
"files": [
|
|
14
14
|
"dist",
|
|
15
|
-
"
|
|
15
|
+
"built-in",
|
|
16
16
|
"docs",
|
|
17
17
|
"native"
|
|
18
18
|
],
|