wolli 0.0.1 → 0.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +182 -0
- package/README.md +125 -2
- package/dist/cli.js +325722 -0
- package/dist/photon_rs_bg.wasm +0 -0
- package/dist/theme/dark.json +86 -0
- package/dist/theme/light.json +85 -0
- package/dist/theme/theme-schema.json +335 -0
- package/dist/theme/theme.ts +1237 -0
- package/docs/extensions.md +2331 -0
- package/docs/index.md +36 -0
- package/docs/integrations.md +715 -0
- package/docs/plugins.md +299 -0
- package/docs/prompt-templates.md +92 -0
- package/docs/sdk.md +760 -0
- package/docs/skills.md +206 -0
- package/docs/themes.md +274 -0
- package/native/darwin/prebuilds/darwin-arm64/darwin-modifiers.node +0 -0
- package/native/darwin/prebuilds/darwin-x64/darwin-modifiers.node +0 -0
- package/native/win32/prebuilds/win32-arm64/win32-console-mode.node +0 -0
- package/native/win32/prebuilds/win32-x64/win32-console-mode.node +0 -0
- package/package.json +40 -9
- package/plugins/scheduler/index.ts +244 -0
- package/plugins/scheduler/package.json +16 -0
- package/plugins/scheduler/scheduler-chat.ts +164 -0
- package/plugins/telegram/index.ts +311 -0
- package/plugins/telegram/package.json +17 -0
- package/plugins/telegram/telegram-chat.ts +169 -0
- package/index.js +0 -2
package/docs/sdk.md
ADDED
|
@@ -0,0 +1,760 @@
|
|
|
1
|
+
# SDK
|
|
2
|
+
|
|
3
|
+
The SDK provides programmatic access to a wolli agent. It has two faces, and which one you reach for depends on where your code runs relative to the agent's daemon:
|
|
4
|
+
|
|
5
|
+
- **In-process embedding** — `createAgentSession()` plus the `@opsyhq/wolli` barrel. You build the system prompt, model, tools, and session yourself, then drive the returned `AgentHarness` directly in your own Node process. No daemon, no HTTP. Use this when you are constructing an agent runtime from scratch (this is the layer the daemon itself is built on).
|
|
6
|
+
- **The daemon control protocol** — a long-running, per-agent loopback **HTTP/SSE** server (`runDaemon`) and a typed client (`Wolli` / `Agent` / `SessionHandle`). Commands are JSON over `POST`, events stream as SSE, and every session is addressed by a URL path. This is wolli's equivalent of an RPC transport, and it is what the `wolli` TUI and every OS service unit talk to. See [RPC Mode](#rpc-mode).
|
|
7
|
+
|
|
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
|
+
|
|
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).
|
|
11
|
+
|
|
12
|
+
## Table of Contents
|
|
13
|
+
|
|
14
|
+
- [Quick Start](#quick-start)
|
|
15
|
+
- [Installation](#installation)
|
|
16
|
+
- [Core Concepts](#core-concepts)
|
|
17
|
+
- [createAgentSession()](#createagentsession)
|
|
18
|
+
- [AgentHarness](#agentharness)
|
|
19
|
+
- [openAgentSession() and AgentRuntime (internal engine)](#openagentsession-and-agentruntime-internal-engine)
|
|
20
|
+
- [Prompting and Queueing](#prompting-and-queueing)
|
|
21
|
+
- [Events](#events)
|
|
22
|
+
- [Options Reference](#options-reference)
|
|
23
|
+
- [Return Value](#return-value)
|
|
24
|
+
- [Complete Example](#complete-example)
|
|
25
|
+
- [Run Modes](#run-modes)
|
|
26
|
+
- [Exports](#exports)
|
|
27
|
+
- [RPC Mode](#rpc-mode)
|
|
28
|
+
- [Integrations](#integrations)
|
|
29
|
+
- [Configuration and Environment](#configuration-and-environment)
|
|
30
|
+
|
|
31
|
+
## Quick Start
|
|
32
|
+
|
|
33
|
+
The daemon face, end to end: connect to (or spawn) an agent's daemon, open its latest session, subscribe, and prompt.
|
|
34
|
+
|
|
35
|
+
```typescript
|
|
36
|
+
import { Wolli } from "@opsyhq/wolli";
|
|
37
|
+
|
|
38
|
+
const wolli = new Wolli();
|
|
39
|
+
const agent = wolli.get("my-agent"); // a handle if the agent exists on disk
|
|
40
|
+
if (!agent) throw new Error("Unknown agent");
|
|
41
|
+
|
|
42
|
+
// Find the live daemon (or spawn a detached one) and open the control stream.
|
|
43
|
+
await agent.connect();
|
|
44
|
+
|
|
45
|
+
// The daemon always keeps at least one session; open the most recent one.
|
|
46
|
+
const session = await agent.getLatestSession();
|
|
47
|
+
|
|
48
|
+
session.subscribe((event) => {
|
|
49
|
+
if (event.type === "message_update" && event.assistantMessageEvent.type === "text_delta") {
|
|
50
|
+
process.stdout.write(event.assistantMessageEvent.delta);
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
await session.prompt("What did you work on today?");
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
`prompt()` resolves the moment the prompt is **accepted** (handled, queued, or about to run), not when the turn ends. The turn streams over the session's SSE; watch for `agent_end` to know it finished.
|
|
58
|
+
|
|
59
|
+
## Installation
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
npm install @opsyhq/wolli
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Everything below — both faces — is re-exported from the package's barrel (`src/index.ts`). No separate install.
|
|
66
|
+
|
|
67
|
+
## Core Concepts
|
|
68
|
+
|
|
69
|
+
### createAgentSession()
|
|
70
|
+
|
|
71
|
+
The in-process builder. It constructs the high-level `AgentHarness` (the durable session tree is built in) from a **pre-built** system prompt, a model, tools, and resources.
|
|
72
|
+
|
|
73
|
+
```typescript
|
|
74
|
+
import { createAgentSession } from "@opsyhq/wolli";
|
|
75
|
+
|
|
76
|
+
const { harness } = await createAgentSession({
|
|
77
|
+
env, // ExecutionEnv — the file/shell backend
|
|
78
|
+
session, // Session — the durable session tree (from openAgentSession())
|
|
79
|
+
model, // Model<Api>
|
|
80
|
+
systemPrompt, // string, pre-built and frozen for the session's lifetime
|
|
81
|
+
modelRegistry, // ModelRegistry — resolves request-time auth (api keys + headers)
|
|
82
|
+
settingsManager, // AgentSettingsManager — read for provider-attribution headers
|
|
83
|
+
sessionId, // string — threaded into provider-attribution session headers
|
|
84
|
+
});
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
> Unlike pi, `createAgentSession()` returns `{ harness }` (an `AgentHarness`), **not** `{ session }`. There is no `ResourceLoader`, no `authStorage`/`cwd`/`agentDir` option, and no model fallback message. The system prompt is passed in pre-built (you call `buildSystemPrompt()` yourself); the harness re-invokes a constant callback each turn so the prefix cache stays warm.
|
|
88
|
+
|
|
89
|
+
The function resolves request-time auth through `modelRegistry.getApiKeyAndHeaders(model)` (not by reading `AuthStorage` directly), which is what carries custom `models.json` keys and per-model/provider headers, then merges in provider-attribution headers. A keyless (header-only) provider is rejected — every wolli provider has an api key today.
|
|
90
|
+
|
|
91
|
+
To build the inputs `createAgentSession()` needs (`env`, `session`, plus the working `cwd`), use [`openAgentSession()`](#openagentsession-and-agentruntime-internal-engine). To build the `systemPrompt`, use [`buildSystemPrompt()`](#system-prompt).
|
|
92
|
+
|
|
93
|
+
### AgentHarness
|
|
94
|
+
|
|
95
|
+
`createAgentSession()` returns the harness — the object you drive in-process. It owns the agent lifecycle: prompting, the steer/follow-up queue, model and thinking state, compaction, and event streaming. (`AgentHarness` is from `@opsyhq/agent`; wolli re-exports the pieces you build it with.)
|
|
96
|
+
|
|
97
|
+
```typescript
|
|
98
|
+
// Subscribe to events (returns an unsubscribe function)
|
|
99
|
+
const unsubscribe = harness.subscribe((event) => { /* … */ });
|
|
100
|
+
|
|
101
|
+
// Prompt and queue
|
|
102
|
+
await harness.steer("New instruction", { images });
|
|
103
|
+
await harness.followUp("After you're done, also do this", { images });
|
|
104
|
+
await harness.abort();
|
|
105
|
+
await harness.compact(customInstructions);
|
|
106
|
+
await harness.waitForIdle();
|
|
107
|
+
await harness.appendMessage(message);
|
|
108
|
+
|
|
109
|
+
// State
|
|
110
|
+
harness.getModel(); // Model<Api>
|
|
111
|
+
harness.getThinkingLevel(); // ThinkingLevel
|
|
112
|
+
harness.getActiveTools(); // AgentTool[]
|
|
113
|
+
harness.isIdle; // boolean
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
> In-process subscription is `harness.subscribe(...)`. There is **no** `AgentSession.subscribe()` for embedders — the daemon's `AgentSession` and `AgentRuntime` are internal (see below). The public daemon-client per-session subscription is [`SessionHandle.subscribe()`](#sessionhandle).
|
|
117
|
+
|
|
118
|
+
**The harness owns the turn loop; you observe it.** A verb (`steer`/`followUp`/`appendMessage`) resolves on *acceptance*, not on turn completion — the actual response streams out through `subscribe(...)`. To run synchronously, await `harness.waitForIdle()` after queueing, then read the result off the events you collected (or off the `session` tree). Subscribe **before** you queue the first prompt, or you miss the leading deltas of that turn.
|
|
119
|
+
|
|
120
|
+
> There is **no `harness.prompt()`**. The in-process face exposes only `steer` (queue for after the current tool calls, before the next LLM call), `followUp` (queue for when the agent next stops), and `appendMessage` (push a raw message with no turn). To *start* a fresh turn on an idle harness, use `followUp(text)` — it runs immediately when the loop is idle. This is why the [Complete Example](#complete-example) opens with `harness.followUp(...)`. The single-verb `prompt(msg, { streamingBehavior })` only exists on the [daemon client](#sessionhandle), which folds the queue choice into one call.
|
|
121
|
+
|
|
122
|
+
### openAgentSession() and AgentRuntime (internal engine)
|
|
123
|
+
|
|
124
|
+
`openAgentSession(name, opts?)` is the durable-session helper. It resolves the agent's owned workspace as the cwd (sessions are keyed by agent, never by the directory you ran from), builds a `NodeExecutionEnv` and a `JsonlSessionRepo`, then opens the latest stored session, a specific one by `id`, or a fresh one.
|
|
125
|
+
|
|
126
|
+
```typescript
|
|
127
|
+
import { openAgentSession } from "@opsyhq/wolli";
|
|
128
|
+
|
|
129
|
+
const { repo, session, env, cwd } = await openAgentSession("my-agent", {
|
|
130
|
+
fresh: false, // start a new session instead of resuming the latest
|
|
131
|
+
id: undefined, // resume a specific stored session id (ignored when `fresh`)
|
|
132
|
+
});
|
|
133
|
+
```
|
|
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.
|
|
136
|
+
|
|
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
|
+
|
|
139
|
+
### Prompting and Queueing
|
|
140
|
+
|
|
141
|
+
On the in-process harness, prompting splits across three verbs:
|
|
142
|
+
|
|
143
|
+
- `harness.steer(message, { images })` — queue a steering message, delivered after the current assistant turn finishes its tool calls, before the next LLM call.
|
|
144
|
+
- `harness.followUp(message, { images })` — queue a follow-up, delivered only when the agent stops.
|
|
145
|
+
- `harness.appendMessage(message)` — append a raw `AgentMessage` to history without triggering a turn.
|
|
146
|
+
|
|
147
|
+
Over the [daemon client](#sessionhandle), prompting collapses onto a single `prompt()` whose `streamingBehavior` selects the queue while streaming:
|
|
148
|
+
|
|
149
|
+
```typescript
|
|
150
|
+
// Not streaming: a normal prompt.
|
|
151
|
+
await session.prompt("What files are here?");
|
|
152
|
+
|
|
153
|
+
// Streaming: choose how to queue.
|
|
154
|
+
await session.prompt("Stop and do this instead", { streamingBehavior: "steer" });
|
|
155
|
+
await session.prompt("After you're done, also check X", { streamingBehavior: "followUp" });
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
> The daemon client steers and follows up **through `prompt(msg, { streamingBehavior })`** — there are no separate `SessionHandle.steer()` / `SessionHandle.followUp()` methods. (The daemon's own `/control` protocol does have distinct `steer` / `follow_up` commands; the typed client folds them into `prompt`.)
|
|
159
|
+
|
|
160
|
+
`prompt()` acks on acceptance via a preflight signal: success means accepted, queued, or handled immediately; rejection (e.g. an ambiguous mid-stream submit) throws. Failures after acceptance arrive through the event stream, not as a rejected `prompt()`.
|
|
161
|
+
|
|
162
|
+
### Events
|
|
163
|
+
|
|
164
|
+
Subscribe to receive streaming output and lifecycle notifications. The same `AgentHarnessEvent` union flows in-process (`harness.subscribe`) and over the daemon ([`SessionHandle.subscribe`](#sessionhandle)); the daemon forwards a **curated subset** (see [Events over the daemon](#events-1)).
|
|
165
|
+
|
|
166
|
+
```typescript
|
|
167
|
+
harness.subscribe((event) => {
|
|
168
|
+
switch (event.type) {
|
|
169
|
+
case "message_update":
|
|
170
|
+
if (event.assistantMessageEvent.type === "text_delta") {
|
|
171
|
+
process.stdout.write(event.assistantMessageEvent.delta);
|
|
172
|
+
}
|
|
173
|
+
break;
|
|
174
|
+
case "tool_execution_start":
|
|
175
|
+
// event.toolName, event.args
|
|
176
|
+
break;
|
|
177
|
+
case "tool_execution_update": // streaming tool output
|
|
178
|
+
case "tool_execution_end": // event.isError
|
|
179
|
+
break;
|
|
180
|
+
case "message_start":
|
|
181
|
+
case "message_end":
|
|
182
|
+
break;
|
|
183
|
+
case "agent_start": // agent began processing a prompt
|
|
184
|
+
case "agent_end": // agent finished (event.messages)
|
|
185
|
+
break;
|
|
186
|
+
case "turn_start":
|
|
187
|
+
case "turn_end": // event.message, event.toolResults
|
|
188
|
+
break;
|
|
189
|
+
case "queue_update": // event.steering, event.followUp
|
|
190
|
+
break;
|
|
191
|
+
case "model_update": // event.model
|
|
192
|
+
case "thinking_level_update": // event.level
|
|
193
|
+
break;
|
|
194
|
+
}
|
|
195
|
+
});
|
|
196
|
+
```
|
|
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.
|
|
199
|
+
|
|
200
|
+
## Options Reference
|
|
201
|
+
|
|
202
|
+
These configure the in-process `createAgentSession()` and the helpers that feed it. The daemon resolves its own equivalents from the agent's `agent.json` and the shared credential store.
|
|
203
|
+
|
|
204
|
+
### Directories
|
|
205
|
+
|
|
206
|
+
Wolli does not take `cwd`/`agentDir` options. On-disk locations are derived per agent from the home root (`~/.wolli`, override `WOLLI_HOME`) by the `config.ts` getters:
|
|
207
|
+
|
|
208
|
+
```typescript
|
|
209
|
+
import { getAgentDir, getSessionsDir, getWorkspaceDir } from "@opsyhq/wolli";
|
|
210
|
+
|
|
211
|
+
getAgentDir("my-agent"); // ~/.wolli/agents/my-agent
|
|
212
|
+
getSessionsDir("my-agent"); // …/sessions (JsonlSessionRepo root)
|
|
213
|
+
getWorkspaceDir("my-agent"); // …/workspace (the stable cwd for the session)
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
Credentials and the default model live in the **shared** agent dir (`~/.wolli/agent`, override `WOLLI_SHARED_DIR`) so one login works across every agent.
|
|
217
|
+
|
|
218
|
+
### Model
|
|
219
|
+
|
|
220
|
+
Resolve a `Model<Api>` through the `ModelRegistry`, then pass it to `createAgentSession()`:
|
|
221
|
+
|
|
222
|
+
```typescript
|
|
223
|
+
import { AuthStorage, ModelRegistry } from "@opsyhq/wolli";
|
|
224
|
+
|
|
225
|
+
const authStorage = AuthStorage.create();
|
|
226
|
+
const modelRegistry = ModelRegistry.create(authStorage);
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
The daemon resolves its model via `findInitialModel` with the precedence: `agent.json` override → shared default → known-provider defaults → first available. Over the daemon client, list and switch the live model with [`getAvailableModels()` / `setModel()`](#sessionhandle); thinking level is `set_thinking_level` (`"off"`, `"minimal"`, `"low"`, `"medium"`, `"high"`, `"xhigh"`).
|
|
230
|
+
|
|
231
|
+
### API Keys and OAuth
|
|
232
|
+
|
|
233
|
+
Lead with **`/login`** for subscription/OAuth providers — over the daemon, login runs daemon-side (`login` command), so credentials never cross the wire and an OAuth flow prompts the client through the session UI rail. An **API key** (`ANTHROPIC_API_KEY`, etc.) is the alternative: it is read from the environment or `auth.json` as one credential source among several.
|
|
234
|
+
|
|
235
|
+
Auth precedence (handled by `AuthStorage`): runtime → `auth.json` (api key / OAuth) → env var. At request time, `createAgentSession()` routes through `modelRegistry.getApiKeyAndHeaders(model)` so custom `models.json` keys and per-provider headers apply.
|
|
236
|
+
|
|
237
|
+
```typescript
|
|
238
|
+
import { AuthStorage, ModelRegistry } from "@opsyhq/wolli";
|
|
239
|
+
|
|
240
|
+
const authStorage = AuthStorage.create(); // shared ~/.wolli/agent/auth.json
|
|
241
|
+
const modelRegistry = ModelRegistry.create(authStorage);
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
### System Prompt
|
|
245
|
+
|
|
246
|
+
Build the frozen prompt with `buildSystemPrompt()`, then pass the resulting string to `createAgentSession()`:
|
|
247
|
+
|
|
248
|
+
```typescript
|
|
249
|
+
import { buildSystemPrompt } from "@opsyhq/wolli";
|
|
250
|
+
|
|
251
|
+
const systemPrompt = buildSystemPrompt({
|
|
252
|
+
config, // AgentConfig (name, purpose, deployedAt)
|
|
253
|
+
soul, // frozen SOUL.md snapshot ("" when absent)
|
|
254
|
+
memory, // frozen MEMORY.md snapshot
|
|
255
|
+
user, // frozen USER.md snapshot
|
|
256
|
+
skills, // Skill[] formatted into the prompt
|
|
257
|
+
selectedTools, // names of active tools, so guidance can tailor
|
|
258
|
+
appendSystemPrompt // text appended to the end
|
|
259
|
+
});
|
|
260
|
+
```
|
|
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.
|
|
263
|
+
|
|
264
|
+
### Tools
|
|
265
|
+
|
|
266
|
+
Pass an `AgentTool[]` to `createAgentSession({ tools })`. Wolli ships built-in tool factories you compose yourself:
|
|
267
|
+
|
|
268
|
+
```typescript
|
|
269
|
+
import {
|
|
270
|
+
createReadTool, createWriteTool, createEditTool,
|
|
271
|
+
createBashTool, createGrepTool, createFindTool, createLsTool,
|
|
272
|
+
createMemoryTool, createDeployTool,
|
|
273
|
+
} from "@opsyhq/wolli";
|
|
274
|
+
```
|
|
275
|
+
|
|
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
|
+
|
|
278
|
+
### Extensions, Skills, Context Files, Slash Commands
|
|
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.
|
|
281
|
+
|
|
282
|
+
Helpers for assembling resources yourself:
|
|
283
|
+
|
|
284
|
+
```typescript
|
|
285
|
+
import { loadSkills, BUILTIN_SLASH_COMMANDS, discoverAndLoadExtensions } from "@opsyhq/wolli";
|
|
286
|
+
```
|
|
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).
|
|
289
|
+
|
|
290
|
+
### Session Management
|
|
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`.
|
|
293
|
+
|
|
294
|
+
### Settings Management
|
|
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.
|
|
297
|
+
|
|
298
|
+
```typescript
|
|
299
|
+
import { AgentSettingsManager } from "@opsyhq/wolli";
|
|
300
|
+
|
|
301
|
+
const store = AgentSettingsManager.create("my-agent");
|
|
302
|
+
store.getDefaultModel();
|
|
303
|
+
store.getAgentDeployed();
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
## Return Value
|
|
307
|
+
|
|
308
|
+
`createAgentSession()` returns:
|
|
309
|
+
|
|
310
|
+
```typescript
|
|
311
|
+
interface CreateAgentSessionResult {
|
|
312
|
+
harness: AgentHarness;
|
|
313
|
+
}
|
|
314
|
+
```
|
|
315
|
+
|
|
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.
|
|
317
|
+
|
|
318
|
+
## Complete Example
|
|
319
|
+
|
|
320
|
+
In-process embedding: open a durable session, build the prompt, construct tools, drive the harness.
|
|
321
|
+
|
|
322
|
+
```typescript
|
|
323
|
+
import {
|
|
324
|
+
openAgentSession,
|
|
325
|
+
buildSystemPrompt,
|
|
326
|
+
createAgentSession,
|
|
327
|
+
createReadTool, createBashTool, createGrepTool,
|
|
328
|
+
AuthStorage, ModelRegistry, AgentSettingsManager,
|
|
329
|
+
findExactModelReferenceMatch, // model resolution helpers also exported
|
|
330
|
+
} from "@opsyhq/wolli";
|
|
331
|
+
|
|
332
|
+
const name = "my-agent";
|
|
333
|
+
|
|
334
|
+
// Durable session + execution env (cwd is the agent's owned workspace).
|
|
335
|
+
const { session, env, cwd } = await openAgentSession(name, { fresh: true });
|
|
336
|
+
|
|
337
|
+
// Auth + model.
|
|
338
|
+
const authStorage = AuthStorage.create();
|
|
339
|
+
const modelRegistry = ModelRegistry.create(authStorage);
|
|
340
|
+
const settingsManager = AgentSettingsManager.create(name);
|
|
341
|
+
const available = modelRegistry.getAvailable();
|
|
342
|
+
const model = available[0];
|
|
343
|
+
if (!model) throw new Error("No model with credentials. Log in with /login first.");
|
|
344
|
+
|
|
345
|
+
// Frozen system prompt.
|
|
346
|
+
const systemPrompt = buildSystemPrompt({
|
|
347
|
+
config: settingsManager.config,
|
|
348
|
+
selectedTools: ["read", "bash", "grep"],
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
// Tools, built for this cwd/env.
|
|
352
|
+
const tools = [
|
|
353
|
+
createReadTool({ env, cwd }),
|
|
354
|
+
createBashTool({ env, cwd }),
|
|
355
|
+
createGrepTool({ env, cwd }),
|
|
356
|
+
];
|
|
357
|
+
|
|
358
|
+
const { harness } = await createAgentSession({
|
|
359
|
+
env,
|
|
360
|
+
session,
|
|
361
|
+
model,
|
|
362
|
+
systemPrompt,
|
|
363
|
+
tools,
|
|
364
|
+
modelRegistry,
|
|
365
|
+
settingsManager,
|
|
366
|
+
sessionId: session.id,
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
harness.subscribe((event) => {
|
|
370
|
+
if (event.type === "message_update" && event.assistantMessageEvent.type === "text_delta") {
|
|
371
|
+
process.stdout.write(event.assistantMessageEvent.delta);
|
|
372
|
+
}
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
await harness.followUp("List the files here and summarize them.");
|
|
376
|
+
await harness.waitForIdle();
|
|
377
|
+
```
|
|
378
|
+
|
|
379
|
+
> Tool factory signatures vary; check the per-tool exports (`createReadTool`, `createBashTool`, …) in `src/core/tools/` for the exact options they take.
|
|
380
|
+
|
|
381
|
+
## Run Modes
|
|
382
|
+
|
|
383
|
+
Wolli has one run mode that the SDK exposes: the **daemon**. There is no in-process interactive or print mode in this package (the interactive TUI lives in `wolli` and drives the agent over the daemon client).
|
|
384
|
+
|
|
385
|
+
### runDaemon
|
|
386
|
+
|
|
387
|
+
`runDaemon(name, opts?)` resolves the agent's model/auth, starts its `AgentRuntime`, binds the loopback HTTP/SSE server, and blocks until a signal — or a `shutdown` command — tears it down.
|
|
388
|
+
|
|
389
|
+
```typescript
|
|
390
|
+
import { runDaemon } from "@opsyhq/wolli";
|
|
391
|
+
|
|
392
|
+
const exitCode = await runDaemon("my-agent", {
|
|
393
|
+
port: undefined, // override the fixed per-agent port from agent.json (debugging)
|
|
394
|
+
});
|
|
395
|
+
```
|
|
396
|
+
|
|
397
|
+
It binds the agent's fixed host/port from `agent.json` (override host with `WOLLI_DAEMON_HOST`, port with `--port`). The `wolli` `daemon` subcommand and every OS service unit invoke this. See [RPC Mode](#rpc-mode) for the protocol it serves.
|
|
398
|
+
|
|
399
|
+
## Exports
|
|
400
|
+
|
|
401
|
+
The barrel (`src/index.ts`) re-exports the full surface. The load-bearing entries for each face:
|
|
402
|
+
|
|
403
|
+
```typescript
|
|
404
|
+
// ── In-process embedding ──
|
|
405
|
+
createAgentSession // builds the AgentHarness
|
|
406
|
+
type CreateAgentSessionOptions, type CreateAgentSessionResult
|
|
407
|
+
openAgentSession // durable session + env + repo
|
|
408
|
+
buildSystemPrompt // the frozen system prompt
|
|
409
|
+
AuthStorage, ModelRegistry // credentials + model resolution
|
|
410
|
+
AgentSettingsManager // agent.json (AgentConfig)
|
|
411
|
+
SessionManager // session tree
|
|
412
|
+
createReadTool, createWriteTool, createEditTool, createBashTool,
|
|
413
|
+
createGrepTool, createFindTool, createLsTool, createMemoryTool, createDeployTool
|
|
414
|
+
|
|
415
|
+
// ── Daemon engine (internal, but exported) ──
|
|
416
|
+
AgentRuntime, type AgentRuntimeOptions
|
|
417
|
+
runDaemon, type RunDaemonOptions
|
|
418
|
+
|
|
419
|
+
// ── Daemon client ──
|
|
420
|
+
Wolli, Agent, SessionHandle
|
|
421
|
+
|
|
422
|
+
// ── Daemon protocol types ──
|
|
423
|
+
type DaemonCommand, type DaemonResponse, type DaemonControlEvent,
|
|
424
|
+
type DaemonAgentState, type DaemonSessionState, type DaemonSessionSummary,
|
|
425
|
+
type ExtensionUIRequest, type ExtensionUIResponse, type OnboardServiceResult
|
|
426
|
+
```
|
|
427
|
+
|
|
428
|
+
> There is no `loadAgentConfig` export — use `AgentSettingsManager`. `configureHttpDispatcher` is exported but tunes the **outbound LLM** undici dispatcher (idle timeout), not the control transport.
|
|
429
|
+
|
|
430
|
+
## RPC Mode
|
|
431
|
+
|
|
432
|
+
Wolli's RPC equivalent is the per-agent daemon's loopback **HTTP/SSE control protocol**. A client attaches to a running daemon, drives sessions over JSON `POST` commands, and consumes session/agent events as Server-Sent Events.
|
|
433
|
+
|
|
434
|
+
> **Transport.** Not stdin/stdout JSONL, not `--mode rpc`. The wire is HTTP over a loopback socket (`http://127.0.0.1:<port>` by default), session-namespaced by URL path. Commands are a JSON body on `POST`; the synchronous response is that request's JSON body. Events stream as SSE, framed on the blank-line `\n\n` boundary. Every route except `/health` requires `Authorization: Bearer <token>`.
|
|
435
|
+
|
|
436
|
+
### Starting the daemon and its routes
|
|
437
|
+
|
|
438
|
+
Start it with [`runDaemon(name)`](#rundaemon), or let the client spawn one ([`Agent.connect()`](#agent)). It binds the agent's fixed host/port and serves these routes:
|
|
439
|
+
|
|
440
|
+
```
|
|
441
|
+
GET /events (SSE) root control stream: agent snapshot + session lifecycle
|
|
442
|
+
GET /sessions the session list (DaemonAgentState)
|
|
443
|
+
GET /sessions/:id/events (SSE) one session's curated event stream (attaching makes it live)
|
|
444
|
+
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
|
|
446
|
+
GET /health liveness; the only route with no auth
|
|
447
|
+
```
|
|
448
|
+
|
|
449
|
+
The session id always comes from the **URL**, never the body. A session goes live when its first client attaches (rehydrating it if idle) and is evicted when its last client detaches (unless a turn is still in flight).
|
|
450
|
+
|
|
451
|
+
**Bearer auth.** All of `/events`, `/sessions`, and `/sessions/*` are guarded by Hono's `bearerAuth` against the agent's token. The token is the per-agent value in `agent.json`, overridable with `WOLLI_DAEMON_TOKEN`.
|
|
452
|
+
|
|
453
|
+
```
|
|
454
|
+
Authorization: Bearer <token>
|
|
455
|
+
```
|
|
456
|
+
|
|
457
|
+
`/health` answers `{ "status": "ok", "agent": "<name>", "pid": <pid>, "startedAt": "<iso>" }` with no auth, so a client can probe liveness before authenticating.
|
|
458
|
+
|
|
459
|
+
### Protocol Overview
|
|
460
|
+
|
|
461
|
+
**Envelope.** A command is `{ type, id?, …fields }`. The `id` is an optional correlation token echoed back on the response. The response is `{ id?, type: "response", command, success, data? | error }` — `data` is omitted entirely for async-ack commands (e.g. `prompt`).
|
|
462
|
+
|
|
463
|
+
```json
|
|
464
|
+
{ "id": "req-1", "type": "prompt", "message": "Hello" }
|
|
465
|
+
{ "id": "req-1", "type": "response", "command": "prompt", "success": true }
|
|
466
|
+
```
|
|
467
|
+
|
|
468
|
+
**SSE framing.** Each event is `event: <name>\n` + `data: <json>\n\n`. The session stream's first frame is `event: hello` carrying the session snapshot; later frames are `event: message`. A `: ping` comment line is sent every **15s** (`KEEPALIVE_MS`) so idle connections don't drop. Clients split the byte stream on `\n\n`, join multi-line `data:` fields, and skip comment (`:`) and malformed frames.
|
|
469
|
+
|
|
470
|
+
**Correlation id.** Set `id` on a command to match its response; events never carry an `id`.
|
|
471
|
+
|
|
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.
|
|
473
|
+
|
|
474
|
+
### Commands
|
|
475
|
+
|
|
476
|
+
`POST /sessions/:id/control` with a `DaemonCommand` body. The full set (from `src/types.ts`):
|
|
477
|
+
|
|
478
|
+
| Group | `type` | Notes |
|
|
479
|
+
|-------|--------|-------|
|
|
480
|
+
| Prompting | `prompt` | `{ message, images?, streamingBehavior? }`; acks on acceptance |
|
|
481
|
+
| | `steer` / `follow_up` | `{ message, images? }`; queue while streaming / after stop |
|
|
482
|
+
| | `abort` | abort the current turn |
|
|
483
|
+
| | `compact` | `{ customInstructions? }` |
|
|
484
|
+
| | `wait_for_idle` | resolves when the turn loop is idle |
|
|
485
|
+
| | `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 |
|
|
489
|
+
| | `shutdown` | ack, then self-exit (frees the fixed port) |
|
|
490
|
+
| State | `get_state` | the session snapshot |
|
|
491
|
+
| | `get_messages` / `get_entries` | conversation messages / tree entries |
|
|
492
|
+
| | `get_commands` | slash commands (extension + prompt + skill) |
|
|
493
|
+
| | `get_resource_summary` | counts + diagnostics |
|
|
494
|
+
| | `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 |
|
|
496
|
+
| Plugins | `install_plugin` / `remove_plugin` / `update_plugins` | single-writer; the daemon reloads itself after |
|
|
497
|
+
| | `onboard_plugin` | runs the just-installed plugin's integration onboarding |
|
|
498
|
+
| Model | `set_thinking_level` | `{ level }` |
|
|
499
|
+
| | `set_model` | `{ provider, modelId }`; returns the resolved `Model` |
|
|
500
|
+
| | `get_available_models` | `{ models }` |
|
|
501
|
+
| | `set_scoped_models` / `set_enabled_models` | session-only scope / persisted agent-tier shortlist |
|
|
502
|
+
| Auth | `login` / `logout` | `{ provider, authType }`; runs daemon-side, credentials never cross the wire |
|
|
503
|
+
| | `get_login_providers` / `get_logout_providers` | eligible providers |
|
|
504
|
+
|
|
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.
|
|
506
|
+
|
|
507
|
+
Example — `set_model`:
|
|
508
|
+
|
|
509
|
+
```json
|
|
510
|
+
{ "type": "set_model", "provider": "anthropic", "modelId": "claude-opus-4-8" }
|
|
511
|
+
{ "type": "response", "command": "set_model", "success": true, "data": { /* Model */ } }
|
|
512
|
+
```
|
|
513
|
+
|
|
514
|
+
### Events
|
|
515
|
+
|
|
516
|
+
Each session streams a **curated subset** of the harness's event surface out of `GET /sessions/:id/events`. The broadcaster forwards only the allowlisted types (`FORWARDED_EVENT_TYPES` in `src/types.ts`); internal own-events (`save_point`, `settled`, `abort`, `session_*`, `tools_update`, `before_*`, …) stay inside the daemon.
|
|
517
|
+
|
|
518
|
+
| Forwarded event | Description |
|
|
519
|
+
|-----------------|-------------|
|
|
520
|
+
| `agent_start` / `agent_end` | agent begins / completes (`agent_end` carries the run's messages) |
|
|
521
|
+
| `turn_start` / `turn_end` | one assistant response + its tool calls (`turn_end.message`, `.toolResults`) |
|
|
522
|
+
| `message_start` / `message_update` / `message_end` | message lifecycle; `message_update.assistantMessageEvent` carries text/thinking/toolcall deltas |
|
|
523
|
+
| `tool_execution_start` / `tool_execution_update` / `tool_execution_end` | tool lifecycle; correlate by `toolCallId`, `tool_execution_end.isError` |
|
|
524
|
+
| `queue_update` | steering/follow-up queue changed (`.steer`, `.followUp`) |
|
|
525
|
+
| `model_update` | live model switched (`.model`) |
|
|
526
|
+
| `thinking_level_update` | thinking level changed (`.level`) |
|
|
527
|
+
| `scoped_models_update` | session model scope changed (`.scopedModels`) — host-originated, not a harness own-event |
|
|
528
|
+
|
|
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.
|
|
530
|
+
|
|
531
|
+
**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
|
+
|
|
533
|
+
```json
|
|
534
|
+
{ "type": "session_added", "session": { /* DaemonSessionSummary */ } }
|
|
535
|
+
{ "type": "session_removed", "sessionId": "abc123" }
|
|
536
|
+
{ "type": "session_renamed", "sessionId": "abc123", "sessionName": "my-feature-work" }
|
|
537
|
+
```
|
|
538
|
+
|
|
539
|
+
### Extension UI Protocol
|
|
540
|
+
|
|
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.
|
|
542
|
+
|
|
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.
|
|
545
|
+
|
|
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`.
|
|
547
|
+
|
|
548
|
+
```json
|
|
549
|
+
{ "type": "extension_ui_request", "id": "uuid-1", "method": "select",
|
|
550
|
+
"title": "Allow command?", "options": ["Allow", "Block"], "timeout": 10000 }
|
|
551
|
+
```
|
|
552
|
+
|
|
553
|
+
Answer (`POST /sessions/:id/ui-response`):
|
|
554
|
+
|
|
555
|
+
```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 }
|
|
559
|
+
```
|
|
560
|
+
|
|
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).
|
|
562
|
+
|
|
563
|
+
### Error Handling
|
|
564
|
+
|
|
565
|
+
A failed command returns the error arm:
|
|
566
|
+
|
|
567
|
+
```json
|
|
568
|
+
{ "type": "response", "command": "set_model", "success": false, "error": "Model not found: invalid/model" }
|
|
569
|
+
```
|
|
570
|
+
|
|
571
|
+
A malformed JSON body yields `{ "success": false, "error": "Malformed JSON body." }` (command `"unknown"`). An unresolvable session id (no such session) returns the error arm echoing the requested `command`/`id`. The typed client unwraps this: `Agent.send()` throws `new Error(body.error)` on `success: false`.
|
|
572
|
+
|
|
573
|
+
### Types
|
|
574
|
+
|
|
575
|
+
Source of truth: [`src/types.ts`](../src/types.ts) (`DaemonCommand`, `DaemonResponse`, `DaemonControlEvent`, `DaemonAgentState`, `DaemonSessionState`, `DaemonSessionSummary`, `ExtensionUIRequest`, `ExtensionUIResponse`, `OnboardServiceResult`, `FORWARDED_EVENT_TYPES`). Message/event/model types (`Model`, `AgentMessage`, `AgentEvent`) come from `@earendil-works/pi-ai` and `@opsyhq/agent`.
|
|
576
|
+
|
|
577
|
+
```typescript
|
|
578
|
+
interface DaemonSessionState {
|
|
579
|
+
sessionId: string;
|
|
580
|
+
model?: Model<Api>;
|
|
581
|
+
thinkingLevel: ThinkingLevel;
|
|
582
|
+
scopedModels: ScopedModel[];
|
|
583
|
+
isStreaming: boolean;
|
|
584
|
+
sessionName?: string;
|
|
585
|
+
sessionFile?: string;
|
|
586
|
+
messageCount: number;
|
|
587
|
+
pendingMessageCount: number;
|
|
588
|
+
}
|
|
589
|
+
```
|
|
590
|
+
|
|
591
|
+
### Raw HTTP/SSE example
|
|
592
|
+
|
|
593
|
+
Driving the daemon with bare `fetch` (no typed client):
|
|
594
|
+
|
|
595
|
+
```typescript
|
|
596
|
+
const base = "http://127.0.0.1:7777";
|
|
597
|
+
const token = "<agent token from agent.json>";
|
|
598
|
+
const auth = { authorization: `Bearer ${token}` };
|
|
599
|
+
|
|
600
|
+
// 1. The session list.
|
|
601
|
+
const list = (await (await fetch(`${base}/sessions`, { headers: auth })).json()) as { sessions: { sessionId: string }[] };
|
|
602
|
+
const sessionId = list.sessions[0].sessionId;
|
|
603
|
+
|
|
604
|
+
// 2. Open the session's SSE stream and read frames split on "\n\n".
|
|
605
|
+
const events = await fetch(`${base}/sessions/${sessionId}/events`, { headers: auth });
|
|
606
|
+
void (async () => {
|
|
607
|
+
const reader = events.body!.getReader();
|
|
608
|
+
const decoder = new TextDecoder();
|
|
609
|
+
let buffer = "";
|
|
610
|
+
for (;;) {
|
|
611
|
+
const { value, done } = await reader.read();
|
|
612
|
+
if (done) break;
|
|
613
|
+
buffer += decoder.decode(value, { stream: true });
|
|
614
|
+
let i;
|
|
615
|
+
while ((i = buffer.indexOf("\n\n")) >= 0) {
|
|
616
|
+
const raw = buffer.slice(0, i);
|
|
617
|
+
buffer = buffer.slice(i + 2);
|
|
618
|
+
let data = "";
|
|
619
|
+
for (const line of raw.split("\n")) {
|
|
620
|
+
if (line.startsWith(":")) continue; // keepalive
|
|
621
|
+
if (line.startsWith("data:")) data += line.slice(5).replace(/^ /, "");
|
|
622
|
+
}
|
|
623
|
+
if (data) console.log(JSON.parse(data));
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
})();
|
|
627
|
+
|
|
628
|
+
// 3. Send a prompt.
|
|
629
|
+
await fetch(`${base}/sessions/${sessionId}/control`, {
|
|
630
|
+
method: "POST",
|
|
631
|
+
headers: { "content-type": "application/json", ...auth },
|
|
632
|
+
body: JSON.stringify({ type: "prompt", message: "Hello" }),
|
|
633
|
+
});
|
|
634
|
+
```
|
|
635
|
+
|
|
636
|
+
### Typed client: Wolli / Agent / SessionHandle
|
|
637
|
+
|
|
638
|
+
The typed client wraps all of the above. Three classes:
|
|
639
|
+
|
|
640
|
+
#### Wolli
|
|
641
|
+
|
|
642
|
+
The agent collection on disk — holds no required state.
|
|
643
|
+
|
|
644
|
+
```typescript
|
|
645
|
+
import { Wolli } from "@opsyhq/wolli";
|
|
646
|
+
|
|
647
|
+
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
|
|
651
|
+
```
|
|
652
|
+
|
|
653
|
+
#### Agent
|
|
654
|
+
|
|
655
|
+
One agent: registry data, per-agent lifecycle, and the `fetch`/SSE transport to its daemon (the single `send` site, the root control stream, the `SessionHandle` map).
|
|
656
|
+
|
|
657
|
+
```typescript
|
|
658
|
+
const agent = wolli.get("my-agent")!;
|
|
659
|
+
|
|
660
|
+
await agent.connect(); // find a live daemon (/health) or spawn a detached one, open the control stream
|
|
661
|
+
agent.getAgentState(); // DaemonAgentState (config, cwd, sessions) from the control hello
|
|
662
|
+
await agent.listSessions(); // DaemonSessionSummary[] — round-trips GET /sessions
|
|
663
|
+
await agent.getSession(id); // open (or return cached) SessionHandle, with its event stream
|
|
664
|
+
await agent.getLatestSession(); // the most-recent session (the daemon guarantees one exists)
|
|
665
|
+
|
|
666
|
+
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
|
+
await agent.restart(); // bounce the daemon so it picks up code changes
|
|
669
|
+
await agent.delete(); // uninstall the OS unit, stop the daemon, delete the home dir
|
|
670
|
+
|
|
671
|
+
const off = agent.on("sessionAdded", (s) => { /* … */ }); // control-stream lifecycle listeners
|
|
672
|
+
agent.close(); // close every session stream + the control stream
|
|
673
|
+
```
|
|
674
|
+
|
|
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`.
|
|
676
|
+
|
|
677
|
+
#### SessionHandle
|
|
678
|
+
|
|
679
|
+
The per-session proxy. Verbs round-trip through `agent.send(sessionId, …)`; the session's SSE feeds the local snapshot/queue caches.
|
|
680
|
+
|
|
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.
|
|
682
|
+
|
|
683
|
+
```typescript
|
|
684
|
+
const session = await agent.getLatestSession();
|
|
685
|
+
|
|
686
|
+
// Prompting (steer/follow-up via streamingBehavior — no separate steer()/followUp()).
|
|
687
|
+
await session.prompt("Do this", { images, streamingBehavior: "steer" });
|
|
688
|
+
await session.abort();
|
|
689
|
+
await session.compact(customInstructions);
|
|
690
|
+
await session.waitForIdle();
|
|
691
|
+
await session.clearQueue();
|
|
692
|
+
|
|
693
|
+
// Subscribe to the curated session events.
|
|
694
|
+
const unsubscribe = session.subscribe((event) => { /* AgentHarnessEvent */ });
|
|
695
|
+
session.onUiRequest = (req) => { /* extension-UI dialog frame */ };
|
|
696
|
+
await session.respondUi(req.id, { value: "Allow" });
|
|
697
|
+
|
|
698
|
+
// Reads (cached snapshot vs round-trip).
|
|
699
|
+
session.getModel(); session.getThinkingLevel(); session.getScopedModels();
|
|
700
|
+
session.getSessionName(); session.getResourceSummary(); session.getCommands();
|
|
701
|
+
session.getSteeringMessages(); session.getFollowUpMessages();
|
|
702
|
+
await session.getEntries(); await session.buildSessionContext();
|
|
703
|
+
await session.listTools(); await session.listSkills(); await session.listPlugins();
|
|
704
|
+
await session.listIntegrations(); await session.listContexts();
|
|
705
|
+
|
|
706
|
+
// Model / thinking / scope.
|
|
707
|
+
await session.getAvailableModels();
|
|
708
|
+
await session.setModel("anthropic", "claude-opus-4-8");
|
|
709
|
+
await session.setThinkingLevel("high");
|
|
710
|
+
await session.setScopedModels(ids); await session.setEnabledModels(ids);
|
|
711
|
+
|
|
712
|
+
// Auth (daemon-side; OAuth prompts round-trip via respondUi).
|
|
713
|
+
await session.getLoginProviderOptions("oauth");
|
|
714
|
+
await session.login("anthropic", "oauth");
|
|
715
|
+
await session.logout("anthropic");
|
|
716
|
+
|
|
717
|
+
// Plugins (single-writer; the daemon reloads itself after).
|
|
718
|
+
await session.installPlugin(source);
|
|
719
|
+
await session.removePlugin(source);
|
|
720
|
+
await session.updatePlugins(source);
|
|
721
|
+
await session.onboardPlugin(source);
|
|
722
|
+
|
|
723
|
+
session.close();
|
|
724
|
+
```
|
|
725
|
+
|
|
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
|
+
## Integrations
|
|
729
|
+
|
|
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.
|
|
731
|
+
|
|
732
|
+
## Configuration and Environment
|
|
733
|
+
|
|
734
|
+
Daemon and agent-home environment variables (from `config.ts`):
|
|
735
|
+
|
|
736
|
+
| Variable | Purpose | Default |
|
|
737
|
+
|----------|---------|---------|
|
|
738
|
+
| `WOLLI_HOME` | Root config dir holding all agents | `~/.wolli` |
|
|
739
|
+
| `WOLLI_SHARED_DIR` | Shared credential dir (`auth.json`, `settings.json`) | `~/.wolli/agent` |
|
|
740
|
+
| `WOLLI_DAEMON_HOST` | Host the daemon binds | `127.0.0.1` (set `0.0.0.0` for off-box) |
|
|
741
|
+
| `WOLLI_DAEMON_TOKEN` | Bearer token override (else the per-agent `agent.json` token) | unset |
|
|
742
|
+
| `WOLLI_SERVICE_MANAGER` | Force the OS service backend (`none`/`launchd`/`systemd`) | autodetect |
|
|
743
|
+
| `WOLLI_SANDBOX` | File/shell confinement backend (`host`/`local-os`/`docker`/`auto`) | `auto` |
|
|
744
|
+
| `WOLLI_BYPASS_PERMISSIONS` | Auto-approve every host command (`1`/`true`) | unset |
|
|
745
|
+
| `ANTHROPIC_API_KEY` (and peers) | API-key credential source (the **alternative** to `/login`) | unset |
|
|
746
|
+
|
|
747
|
+
Per-agent on-disk layout (`~/.wolli/agents/<name>/`):
|
|
748
|
+
|
|
749
|
+
```
|
|
750
|
+
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)
|
|
753
|
+
MEMORY.md durable notes (edited via the memory tool)
|
|
754
|
+
USER.md facts about the human
|
|
755
|
+
sessions/ JsonlSessionRepo session tree
|
|
756
|
+
workspace/ the stable cwd passed to every session
|
|
757
|
+
integrations.json per-(service, account) credential registry
|
|
758
|
+
store/ per-integration runtime state, one file per service
|
|
759
|
+
approvals.json durable host-escalation prefix rules
|
|
760
|
+
```
|