skillscript-runtime 0.18.2 → 0.18.3

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.
@@ -230,6 +230,114 @@ SKILLSCRIPT_HOME=/path/to/adopter skillfile dashboard --config /path/to/adopter/
230
230
 
231
231
  Each instance reads its own config; ports/paths/db files don't collide.
232
232
 
233
+ ## Wiring the AgentConnector
234
+
235
+ `AgentConnector` is the substrate-neutral delivery surface for `# Output: agent: X` / `# Output: template: X` lifecycle hooks and `notify()` / `exchange()` ops. The runtime calls into the contract; your impl decides where the payload lands (webhook, tmux session, file drop, IPC pipe, Slack thread, your own agent harness, etc.).
236
+
237
+ The full contract surface — methods, payload shapes, receipt shapes, the `agent@session` targeting convention, the graceful-degradation rule — lives in [Connector Contract Reference](connector-contract-reference.md) §AgentConnector. This section covers the **wiring path** for adopters: how to bring an impl online so the runtime uses it.
238
+
239
+ ### Two wiring paths
240
+
241
+ Same shape as the other substrate slots — programmatic (recommended for custom impls today) or declarative (`connectors.json`, restricted to bundled types).
242
+
243
+ **(a) Programmatic — for adopter-written impls.** Construct the connector in your bootstrap and pass it via `BootstrapOpts.agentConnector`:
244
+
245
+ ```typescript
246
+ import { bootstrap } from "skillscript-runtime";
247
+ import { MyAgentConnector } from "./my-agent-connector.js";
248
+
249
+ const { registry, scheduler, server } = await bootstrap({
250
+ agentConnector: new MyAgentConnector({
251
+ endpoint: process.env["MY_AGENT_ENDPOINT"],
252
+ api_key: process.env["MY_AGENT_API_KEY"],
253
+ }),
254
+ });
255
+ ```
256
+
257
+ `bootstrap()` calls `registry.registerAgentConnector("primary", ...)` for you. `health_check()` fires during registration — wiring failures throw at boot, not at first delivery.
258
+
259
+ **(b) Declarative `connectors.json`** — for bundled types and the (deferred) custom-via-dynamic-import path:
260
+
261
+ ```json
262
+ {
263
+ "substrate": {
264
+ "agent_connector": "noop"
265
+ }
266
+ }
267
+ ```
268
+
269
+ Bundled short-form values:
270
+
271
+ | Value | Behavior |
272
+ |---|---|
273
+ | `null` (or omitted) | `NoOpAgentConnector` — silent fallback. `deliver()` / `wake()` log + resolve; `# Output: agent:` declarations complete with a stderr warning. Lets a runtime start with no harness wired. |
274
+ | `"noop"` | Same as `null` but explicitly stated. |
275
+ | Object with `"type": "custom"` | Adopter impl resolved by dynamic-import (deferred — surfaces a clear error today; use programmatic path). |
276
+
277
+ For full configuration shape, see [Configuration](configuration.md) §"The substrate section."
278
+
279
+ ### Precedence
280
+
281
+ Same as other substrate slots:
282
+
283
+ 1. **Programmatic** `BootstrapOpts.agentConnector` — explicit, highest priority.
284
+ 2. **Declarative** `connectors.json` `substrate.agent_connector` — deployment-durable.
285
+ 3. **Built-in default** — `NoOpAgentConnector`. Skills with `# Output: agent:` fire warnings, not errors.
286
+
287
+ The NoOp fallback is the design choice that makes "runtime works out of box without any AgentConnector wiring" hold. Adopters who want strictness should explicitly wire their connector and let `health_check()` throw if it can't start.
288
+
289
+ ### Worked example
290
+
291
+ The canonical bundled example is `examples/connectors/HttpWebhookAgentConnector/` — a complete `AgentConnector` impl against an HTTP-webhook substrate. It demonstrates:
292
+
293
+ - Per-agent URL routing (`HTTP_WEBHOOK_AGENTS` JSON env)
294
+ - Optional `wake_url` per agent — present means wake-capable, absent means degrade-on-wake
295
+ - Bearer + HMAC auth (combinable)
296
+ - Tolerant receipt synthesis (substrate returns substrate-shaped JSON; connector translates to canonical `DeliveryReceipt`)
297
+ - Tests covering the deliver / wake / health-check / request-response surface
298
+
299
+ Three patterns to copy when forking it for your substrate:
300
+
301
+ **Pattern 1 — `agent@session` opaque composite.** Every messaging substrate needs either bare-identity OR specific-live-session addressing. The contract keeps `agent_id` opaque; sessions ride as `"perry@kitchen-terminal"` or via `WakeOpts.session_id`. Substrates that care decompose; substrates that don't ignore. Pick one form for your fork and document it in your impl's README:
302
+
303
+ ```typescript
304
+ async wake(agent_id: string, opts?: WakeOpts): Promise<WakeReceipt> {
305
+ // Form A — composite in agent_id
306
+ const [agent, embeddedSession] = agent_id.split("@");
307
+ // Form B — opts.session_id wins if both supplied
308
+ const session = opts?.session_id ?? embeddedSession;
309
+ // ... route to (agent, session)
310
+ return { woken_at: Date.now(), woken: true, ...(session ? { session_id: session } : {}) };
311
+ }
312
+ ```
313
+
314
+ **Pattern 2 — graceful degradation on wake.** `wake()` must not throw because your substrate lacks interrupt capability. Distinguish capability-gap (degrade) from operational-fault (throw):
315
+
316
+ ```typescript
317
+ async wake(agent_id: string, _opts?: WakeOpts): Promise<WakeReceipt> {
318
+ const cfg = this.agents[agent_id];
319
+ if (!cfg) throw new Error(`agent not configured: ${agent_id}`); // operational fault
320
+ if (!cfg.wake_url) {
321
+ // capability gap — no interrupt channel for this agent — degrade
322
+ return { woken_at: Date.now(), woken: false };
323
+ }
324
+ const response = await fetch(cfg.wake_url, { ... });
325
+ if (!response.ok) throw new DeliveryFailedError(...); // operational fault
326
+ return { woken_at: Date.now(), woken: true };
327
+ }
328
+ ```
329
+
330
+ Callers reading `WakeReceipt.woken` distinguish "the substrate woke them" from "the substrate stored the payload for later" without needing per-substrate knowledge.
331
+
332
+ **Pattern 3 — session echo on receipts.** When your substrate routes to a specific session, echo it back on `DeliveryReceipt.session_id` / `WakeReceipt.session_id`. Dashboards rendering "delivered to perry@kitchen-terminal" rather than just "delivered to perry" depend on this.
333
+
334
+ ### When to fork vs. when to write fresh
335
+
336
+ - **Fork `HttpWebhookAgentConnector`** when your substrate is HTTP-shaped and your changes are: tweaked auth (OAuth, mTLS), retry policy, different routing model. Most production deployments end up here.
337
+ - **Write fresh** when your substrate is fundamentally non-HTTP (tmux, file drop, gRPC, websocket-push). Implement the five required methods (`list_agents`, `deliver`, `wake`, `health_check`, `request_response`) + optional `agent_status`. Use `NoOpAgentConnector` as the minimal-shape reference.
338
+
339
+ In either case: write tests against the contract methods (the bundled example's `tests/HttpWebhookAgentConnector.test.ts` is a useful template), wire via `BootstrapOpts.agentConnector`, and let `health_check()` enforce the "fail-at-boot, not at first delivery" property.
340
+
233
341
  ## Authoring posture — who owns the skills you write
234
342
 
235
343
  Every skill stored in a `SkillStore` carries a `SkillMeta.author` field captured at first-write. The author is then load-bearing at dispatch time: the runtime threads it into `ctx.agentId` so identity-scoped substrates (memory stores, multi-tenant DBs) read and write under that scope.
@@ -4,7 +4,7 @@ How to configure a skillscript-runtime deployment.
4
4
 
5
5
  The single config file is **`~/.skillscript/connectors.json`** (or any path passed via `--connectors`). It has two top-level concerns:
6
6
 
7
- 1. **`substrate`** — which `SkillStore`, `DataStore`, and `LocalModel` the runtime hosts (MCP server + web dashboard) use.
7
+ 1. **`substrate`** — which `SkillStore`, `DataStore`, `LocalModel`, and `AgentConnector` the runtime hosts (MCP server + web dashboard) use.
8
8
  2. **Named MCP connector instances** — `youtrack`, `github`, etc. — invoked via `$ <name>` in skill source.
9
9
 
10
10
  The runtime loads `connectors.json` at startup. Missing file → graceful empty config (substrate defaults to filesystem skills + conditional sqlite memories; no MCP connectors). Malformed JSON or unknown fields → structured errors surfaced at bootstrap.
@@ -123,12 +123,13 @@ A typical out-of-the-box `~/.skillscript/connectors.json`:
123
123
  "substrate": {
124
124
  "skill_store": "filesystem",
125
125
  "data_store": "sqlite",
126
- "local_model": null
126
+ "local_model": null,
127
+ "agent_connector": null
127
128
  }
128
129
  }
129
130
  ```
130
131
 
131
- Equivalent to omitting the file entirely — these are the base config defaults.
132
+ Equivalent to omitting the file entirely — these are the base config defaults. `agent_connector: null` falls back to the silent `NoOpAgentConnector` — skills with `# Output: agent: X` complete cleanly with a stderr warning; replace with `"noop"` for the same behavior stated explicitly, or with a `"custom"` entry to wire an adopter impl.
132
133
 
133
134
  To switch skills storage to SQLite:
134
135
 
@@ -165,14 +166,16 @@ Valid short-form values per slot:
165
166
  | `skill_store` | `"filesystem"` \| `"sqlite"` |
166
167
  | `data_store` | `"sqlite"` |
167
168
  | `local_model` | (none — `"ollama"` requires the object form with `defaultModelTag`; see below) |
169
+ | `agent_connector` | `"noop"` (explicit silent fallback; same behavior as `null`) |
168
170
 
169
171
  ### Null — explicit "no substrate"
170
172
 
171
173
  ```json
172
- "local_model": null
174
+ "local_model": null,
175
+ "agent_connector": null
173
176
  ```
174
177
 
175
- The runtime doesn't register a connector for this slot. Useful for explicitly disabling LocalModel when nothing local is available.
178
+ The runtime doesn't register a real connector for this slot. `local_model: null` leaves `$ llm` un-wired (skills calling it error at execute time). `agent_connector: null` falls back to `NoOpAgentConnector` — `# Output: agent:` declarations complete with a stderr warning instead of throwing, so a runtime can start without any agent harness wired.
176
179
 
177
180
  ### Object form — override defaults
178
181
 
@@ -193,6 +196,7 @@ The runtime doesn't register a connector for this slot. Useful for explicitly di
193
196
  | `sqlite` (skill_store) | `dbPath` (default: `$SKILLSCRIPT_HOME/skills/skills.db`) |
194
197
  | `sqlite` (data_store) | `dbPath` (default: `$SKILLSCRIPT_HOME/data.db`; `DATA_DB` env overrides) |
195
198
  | `ollama` (local_model) | `baseUrl` (default: `OLLAMA_BASE_URL` env or `http://localhost:11434`), **`defaultModelTag` (required — e.g., `"gemma2:9b"`, `"llama3.1:8b"`)** |
199
+ | `noop` (agent_connector) | none — silent fallback (warn + resolve). Real adopter impls use the `custom` form below. |
196
200
 
197
201
  > **`SqliteDataStore` feature surface.** The bundled `sqlite` data_store is a deliberately minimal reference implementation: `supports_writes` + `supports_tag_filter` are true; `supports_semantic`, `supports_pinning`, `supports_decay_model`, `supports_thread_status_filter` are all false. Rich features (semantic retrieval, pinning, decay scoring, thread-status workflow) come from substrate impls — adopters fork `examples/connectors/DataStoreTemplate/` and wire their backing system (memory broker, vector DB, AMP, etc.). The bundled impl exists so the runtime works out-of-box; adopters with richer query semantics write their own.
198
202
 
@@ -218,17 +222,28 @@ Pin the model tag explicitly — must be a tag your Ollama instance has pulled (
218
222
  ```json
219
223
  "skill_store": {
220
224
  "type": "custom",
221
- "module": "./my-amp-skill-store.js",
222
- "export": "AmpSkillStore",
225
+ "module": "./my-skill-store.js",
226
+ "export": "MySkillStore",
223
227
  "config": {
224
228
  "vault": "team"
225
229
  }
226
230
  }
227
231
  ```
228
232
 
229
- References an adopter-written class implementing the relevant contract. `module` is the path to the JS file; `export` is the named export (defaults to `default`); `config` is passed to the constructor.
233
+ References an adopter-written class implementing the relevant contract. `module` is the path to the JS file; `export` is the named export (defaults to `default`); `config` is passed to the constructor. The same shape works for any substrate slot:
230
234
 
231
- > **Limitation**: sync `bootstrap()` can't dynamic-import. Custom-via-connectors.json surfaces a clear error and falls back to the default. Adopters wanting custom impls today write a programmatic bootstrap that calls `registry.registerSkillStore("primary", new AmpSkillStore(...))` directly — same pattern as the runtime's reference `bootstrap()`. Async-bootstrap with dynamic-import support is planned.
235
+ ```json
236
+ "agent_connector": {
237
+ "type": "custom",
238
+ "module": "./my-agent-connector.js",
239
+ "export": "MyHttpWebhookAgentConnector",
240
+ "config": {
241
+ "endpoint": "${AGENT_ENDPOINT}"
242
+ }
243
+ }
244
+ ```
245
+
246
+ > **Limitation**: sync `bootstrap()` can't dynamic-import. Custom-via-connectors.json surfaces a clear error and falls back to the default. Adopters wanting custom impls today write a programmatic bootstrap that calls `registry.registerSkillStore("primary", new MySkillStore(...))` (or `registry.registerAgentConnector(...)`) directly — same pattern as the runtime's reference `bootstrap()`. Async-bootstrap with dynamic-import support is planned.
232
247
 
233
248
  ---
234
249
 
@@ -236,9 +251,9 @@ References an adopter-written class implementing the relevant contract. `module`
236
251
 
237
252
  When multiple config sources speak:
238
253
 
239
- 1. **Programmatic opts** (`opts.skillStore` passed to `bootstrap()`) — explicit, highest priority
254
+ 1. **Programmatic opts** (`opts.skillStore` / `opts.dataStore` / `opts.localModel` / `opts.agentConnector` passed to `bootstrap()`) — explicit, highest priority
240
255
  2. **`connectors.json` substrate section** — declarative, deployment-durable
241
- 3. **Built-in default** — fallback (filesystem skill_store; conditional sqlite data_store; no local_model)
256
+ 3. **Built-in default** — fallback (filesystem skill_store; conditional sqlite data_store; no local_model; NoOpAgentConnector)
242
257
 
243
258
  If two configs disagree, the higher-priority one wins; lower-priority is ignored without error.
244
259
 
@@ -1,6 +1,6 @@
1
1
  # Connector Contract Reference
2
2
 
3
- The substrate-neutral contracts skillscript-runtime exposes for adopters to wire their own substrate behind. This doc is the **canonical source of truth** for the AgentConnector contract as locked at v1.0 by the v0.9.6 audit (Perry's thread `b722bbf4`).
3
+ The substrate-neutral contracts skillscript-runtime exposes for adopters to wire their own substrate behind. This doc is the **canonical source of truth** for the AgentConnector contract. The interface shape was locked at v1.0 by the v0.9.6 audit (thread `b722bbf4`); the wake/deliver receipt shapes carry post-lock refinements per the v0.18.2 session-targeting + graceful-degradation requirements.
4
4
 
5
5
  **Audience**: this doc is written for the agent that's implementing an adopter's AgentConnector — typically an LLM-class agent supervised by a human. If you're a human reading it directly, the same content applies; the prose is tightened for agent comprehension (literal field semantics, explicit precedence rules, worked examples).
6
6
 
@@ -8,7 +8,7 @@ Other contracts (McpConnector, SkillStore, DataStore, LocalModel) audit + lock i
8
8
 
9
9
  ---
10
10
 
11
- ## AgentConnector — v1.0 contract (locked v0.9.6)
11
+ ## AgentConnector — v1.0 contract
12
12
 
13
13
  ### Purpose
14
14
 
@@ -81,14 +81,78 @@ interface DeliveryMeta {
81
81
  interface DeliveryReceipt {
82
82
  delivered_at: number;
83
83
  delivery_id?: string;
84
+ session_id?: string;
84
85
  delivery_skipped?: boolean;
85
86
  }
86
87
  ```
87
88
 
88
89
  - **`delivered_at`**: substrate-acknowledgement timestamp. When the substrate confirmed it accepted the delivery.
89
90
  - **`delivery_id`**: substrate-specific id for callers to correlate later.
91
+ - **`session_id`**: the session that received the delivery. Set when the substrate routes to a specific session (e.g., per-terminal mailbox, per-tab webhook). Omitted when the substrate is agent-level only (Slack DM, email — no session concept) or when the substrate fans out / accepts without committing to a session. See *agent@session targeting* below.
90
92
  - **`delivery_skipped`**: adopter signals "accepted but not pushed to the agent" — offline, rate-limit drop, tmux session exists but agent hasn't read, etc. Distinct from outright failure (which throws). Runtime echoes this on the receipt record for dashboard observability.
91
93
 
94
+ ### WakeOpts + WakeReceipt
95
+
96
+ ```typescript
97
+ interface WakeOpts {
98
+ context?: string;
99
+ when?: "immediate" | number;
100
+ session_id?: string;
101
+ }
102
+
103
+ interface WakeReceipt {
104
+ woken_at: number;
105
+ woken: boolean;
106
+ session_id?: string;
107
+ }
108
+ ```
109
+
110
+ - **`WakeOpts.context`**: optional preamble to prepend to the wake message.
111
+ - **`WakeOpts.when`**: `"immediate"` (default) or a unix-ms timestamp for scheduled wake.
112
+ - **`WakeOpts.session_id`**: structured session targeting. Alternative to embedding `agent@session` in the `agent_id` opaque string. Callers with the session already separated (e.g., a dashboard's per-session "wake this terminal" action) pass it here. When both forms are supplied, `opts.session_id` takes precedence over the embedded suffix.
113
+ - **`WakeReceipt.woken_at`**: substrate's acknowledgement timestamp.
114
+ - **`WakeReceipt.woken`** (required): honest signal of whether the substrate actually interrupted the agent. See *Graceful degradation on wake* below — this is the read every caller does to distinguish interrupted-them from delivered-only.
115
+ - **`WakeReceipt.session_id`**: the session that received the wake (or delivery, if degraded). Set when the substrate knows; omit otherwise.
116
+
117
+ ### agent@session targeting
118
+
119
+ `agent_id` is an opaque string. The substrate may treat it as:
120
+
121
+ - A bare agent identifier (`alice`, an email address, a Slack `@user`, a Discord user ID).
122
+ - A composite `agent@session` (e.g., `"perry@kitchen-terminal"`) when the substrate tracks multiple live sessions per identity.
123
+
124
+ The substrate decomposes the composite if it cares; non-session substrates ignore the suffix or treat the whole string as the address. This keeps the contract substrate-neutral while preserving session-granular routing — every messaging substrate either addresses a bare identity OR a specific live session, and the opaque-composite form covers both without locking adopters into a particular session model.
125
+
126
+ **Two forms, one wire**:
127
+
128
+ ```typescript
129
+ // Form A — composite in agent_id (works for deliver + wake)
130
+ await conn.wake("perry@kitchen-terminal");
131
+
132
+ // Form B — structured WakeOpts.session_id (wake only)
133
+ await conn.wake("perry", { session_id: "kitchen-terminal" });
134
+ ```
135
+
136
+ Substrates that care about sessions read both — `opts.session_id` wins if both are set. Substrates that don't care ignore both. Callers that already have agent + session as separate variables prefer Form B; callers passing an opaque user-supplied address prefer Form A.
137
+
138
+ `DeliveryReceipt.session_id` and `WakeReceipt.session_id` echo the resolved session back to the caller. Useful for dashboards rendering "delivered to perry@kitchen-terminal" rather than just "delivered to perry."
139
+
140
+ ### Graceful degradation on wake
141
+
142
+ Not every substrate can interrupt. A webhook receiver, a file-drop directory, or a store-only adopter has no attention channel — they can persist the payload but can't make the agent look at it now.
143
+
144
+ **The rule**: `wake()` must not throw because the substrate lacks interrupt capability. Conform by degrading: deliver the payload as if it were a `deliver()` call, set `woken: false` on the receipt. Callers reading the receipt distinguish "the substrate woke the agent" from "the substrate stored the payload for later" without needing per-substrate knowledge.
145
+
146
+ | Situation | `wake()` behavior | `WakeReceipt.woken` |
147
+ |---|---|---|
148
+ | Substrate has live interrupt channel + agent is reachable | Send interrupt | `true` |
149
+ | Substrate has no interrupt channel (webhook, file-drop) | Deliver content, no interrupt | `false` |
150
+ | Substrate has interrupt channel but agent unreachable / offline | Best-effort deliver, no interrupt | `false` |
151
+ | Caller misconfiguration (unknown `agent_id`, missing required config) | Throw `DeliveryFailedError` | — |
152
+ | Substrate fault (network, auth) | Throw | — |
153
+
154
+ The distinction `wake-capability` vs `network-fault` matters. The former is structural (this substrate fundamentally can't wake) and degrades silently. The latter is operational (the substrate could wake but something broke) and throws. Adopters writing connectors should keep this distinction explicit — the bundled `HttpWebhookAgentConnector` returns `woken: false` when `wake_url` is unconfigured (capability gap, fixed at config time) but throws on actual HTTP failure (operational fault, surfaces to caller).
155
+
92
156
  ---
93
157
 
94
158
  ## Use-site cross-reference table
@@ -129,7 +193,7 @@ Implementation checklist:
129
193
 
130
194
  2. **Implement `deliver(agent_id, payload)`** — serialize `payload` to your substrate's format. For HTTP: JSON body with `kind`, `content`/`prompt`, and `meta`. For tmux: serialize meta as a header line, write content via `tmux send-keys`. For file-drop: write a file under `<dir>/<dispatch_id>.{json,txt}`.
131
195
 
132
- 3. **Implement `wake(agent_id, opts?)`** — substrate-specific "rouse the agent." Webhook: POST to a `/wake` endpoint. Tmux: send a wake-up sequence. Etc.
196
+ 3. **Implement `wake(agent_id, opts?)`** — substrate-specific "rouse the agent." Wake-capable substrates: send an attention signal (tmux: wake-up sequence; webhook with a `/wake` endpoint: POST it; push channel: send notification). Set `woken: true` on the receipt. Passive substrates (file-drop, store-only, webhook without `/wake`): degrade gracefully — deliver the content, return `woken: false`. NEVER throw because the substrate lacks interrupt capability. Honor `opts.session_id` if your substrate tracks sessions; otherwise ignore it. Echo the resolved session on `WakeReceipt.session_id` so dashboards can render it.
133
197
 
134
198
  4. **Implement `health_check()`** — return `true` if substrate is reachable + configured. Webhook: HEAD/OPTIONS your endpoint. Tmux: check the session exists. File-drop: check the directory is writable.
135
199
 
@@ -203,4 +267,4 @@ The shape-vs-semantics split is deliberate (see [[ARCHITECTURE INVARIANT 88df79c
203
267
 
204
268
  ---
205
269
 
206
- *This doc reflects the v0.9.6 AgentConnector lock + v0.13.8 storage-conventions addition; future contract changes update this file alongside the code.*
270
+ *This doc reflects the v0.9.6 AgentConnector interface lock, v0.13.8 storage-conventions addition, and v0.18.2 receipt-shape refinements (woken-honesty + session targeting + graceful degradation). Future contract changes update this file alongside the code.*
@@ -46,7 +46,7 @@ Three env vars; see `.env.example`.
46
46
  ```
47
47
 
48
48
  - `url` (required) — POST destination for `deliver()` calls
49
- - `wake_url` (optional) — POST destination for `wake()` calls; throws if missing + skill calls `wake()`
49
+ - `wake_url` (optional) — POST destination for `wake()` calls. When absent, `wake()` degrades gracefully: returns `WakeReceipt {woken: false}` instead of throwing. Distinguishes capability-gap (this agent has no interrupt channel) from operational fault (HTTP error), per the contract's graceful-degradation rule.
50
50
  - `status_url` (optional) — GET probe for `agent_status?()` + `health_check()`; skipped if missing
51
51
 
52
52
  `HTTP_WEBHOOK_TIMEOUT_MS` — total request duration (connect + send + receive + parse). Default `5000`.
@@ -105,6 +105,7 @@ Return JSON body matching canonical `DeliveryReceipt`:
105
105
  {
106
106
  "delivered_at": 1700000000005,
107
107
  "delivery_id": "your-substrate-id-optional",
108
+ "session_id": "kitchen-terminal-optional",
108
109
  "delivery_skipped": false
109
110
  }
110
111
  ```
@@ -113,6 +114,7 @@ Return JSON body matching canonical `DeliveryReceipt`:
113
114
 
114
115
  - `delivered_at` ← receiver's value, else `Date.now()`
115
116
  - `delivery_id` ← `delivery_id` / `id` / `ts` (first match)
117
+ - `session_id` ← receiver's value if present (substrate-routed to a specific session)
116
118
  - `delivery_skipped` ← receiver's value if `true`
117
119
 
118
120
  Adopters with strict substrate shape can replace the `synthesizeReceipt()` helper. Bundled example is permissive.
@@ -124,6 +126,25 @@ Adopters with strict substrate shape can replace the `synthesizeReceipt()` helpe
124
126
  - **5xx** → throw `DeliveryFailedError(kind: "http_status")` — transient (adopter with retry policy forks around this)
125
127
  - **Network error / timeout** → throw `DeliveryFailedError(kind: "network" | "timeout")`
126
128
 
129
+ ### Wake — capability gap vs. operational fault
130
+
131
+ `wake()` distinguishes two failure modes per the contract's graceful-degradation rule:
132
+
133
+ | Situation | `wake()` behavior | `WakeReceipt.woken` |
134
+ |---|---|---|
135
+ | `wake_url` configured for `agent_id`, POST returns 2xx | Send attention signal | `true` |
136
+ | `wake_url` NOT configured for `agent_id` (capability gap — this substrate has no interrupt channel for this agent) | No-op, return receipt | `false` |
137
+ | `agent_id` not configured at all (caller misconfiguration) | Throw `Error("agent not configured: ...")` | — |
138
+ | `wake_url` configured but POST fails (operational fault) | Throw `DeliveryFailedError` | — |
139
+
140
+ The capability-gap case (no `wake_url`) is a deliberate fork choice: passive substrates conform by degrading. Callers reading `WakeReceipt.woken` distinguish interrupted-them from delivered-only without needing per-substrate knowledge.
141
+
142
+ ### `agent@session` targeting
143
+
144
+ `agent_id` is opaque to the connector at the wire level — whatever the skill passes flows through to the configured per-agent `url`. Substrates that track multiple live sessions per identity can address them via the `agent@session` composite (e.g., `"perry@kitchen-terminal"`) OR the structured `WakeOpts.session_id` form. The connector forwards the resolved `agent_id` in the POST body; the receiver decomposes if it cares. Echo the resolved session on `DeliveryReceipt.session_id` / `WakeReceipt.session_id` so dashboards can render "delivered to perry@kitchen-terminal" instead of just "delivered to perry."
145
+
146
+ See [Connector Contract Reference](../../../docs/connector-contract-reference.md) §"agent@session targeting" + §"Graceful degradation on wake" for the contract-level rules.
147
+
127
148
  ---
128
149
 
129
150
  ## Three deployment models
@@ -254,6 +275,8 @@ If you fork this example into your codebase:
254
275
  - `request_response()` throws NotImplementedError (Q1 v0.10 deferred)
255
276
  - HMAC signing produces the correct `X-Signature` header value
256
277
  - Bearer auth sets the correct `Authorization` header
278
+ - `wake()` degrades to `woken: false` when `wake_url` is unconfigured (graceful degradation)
279
+ - `wake()` returns `woken: true` when the POST succeeds
257
280
 
258
281
  Run via `vitest run examples/connectors/HttpWebhookAgentConnector/tests/`.
259
282
 
@@ -2,7 +2,7 @@
2
2
  "provenance_version": "1.0",
3
3
  "language_version": "1.0",
4
4
  "compiler_version": "0.1.0-dev",
5
- "compiled_at": "2026-06-05T20:34:57.556Z",
5
+ "compiled_at": "2026-06-05T20:35:05.809Z",
6
6
  "source_skill": {
7
7
  "name": "hello-world"
8
8
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "skillscript-runtime",
3
- "version": "0.18.2",
3
+ "version": "0.18.3",
4
4
  "description": "Runtime, compiler, lint, CLI, and dashboard for Skillscript — a small declarative language for authoring agent workflows.",
5
5
  "license": "MIT",
6
6
  "author": "Scott Shwarts <scotts@pobox.com>",