skillscript-runtime 0.18.1 → 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.
@@ -1 +1 @@
1
- {"version":3,"file":"agent-noop.d.ts","sourceRoot":"","sources":["../../src/connectors/agent-noop.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,cAAc,EACd,eAAe,EACf,WAAW,EACX,eAAe,EACf,eAAe,EACf,mBAAmB,EACnB,QAAQ,EACR,QAAQ,EACR,WAAW,EACZ,MAAM,YAAY,CAAC;AACpB,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,YAAY,CAAC;AAE7D;;;;;;;;;;;;;;;GAeG;AACH,qBAAa,kBAAmB,YAAW,cAAc;IACvD,MAAM,CAAC,kBAAkB,IAAI,0BAA0B;IASjD,WAAW,IAAI,OAAO,CAAC,eAAe,EAAE,CAAC;IAIzC,OAAO,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,eAAe,GAAG,OAAO,CAAC,eAAe,CAAC;IAO7E,IAAI,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,QAAQ,GAAG,OAAO,CAAC,WAAW,CAAC;IAO9D,YAAY,IAAI,OAAO,CAAC,OAAO,CAAC;IAQhC,gBAAgB,CACpB,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,eAAe,EACzB,KAAK,EAAE,mBAAmB,GACzB,OAAO,CAAC,QAAQ,CAAC;IAad,YAAY,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,CAAC;CAO5D"}
1
+ {"version":3,"file":"agent-noop.d.ts","sourceRoot":"","sources":["../../src/connectors/agent-noop.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,cAAc,EACd,eAAe,EACf,WAAW,EACX,eAAe,EACf,eAAe,EACf,mBAAmB,EACnB,QAAQ,EACR,QAAQ,EACR,WAAW,EACZ,MAAM,YAAY,CAAC;AACpB,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,YAAY,CAAC;AAE7D;;;;;;;;;;;;;;;GAeG;AACH,qBAAa,kBAAmB,YAAW,cAAc;IACvD,MAAM,CAAC,kBAAkB,IAAI,0BAA0B;IASjD,WAAW,IAAI,OAAO,CAAC,eAAe,EAAE,CAAC;IAIzC,OAAO,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,eAAe,GAAG,OAAO,CAAC,eAAe,CAAC;IAO7E,IAAI,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,QAAQ,GAAG,OAAO,CAAC,WAAW,CAAC;IAY9D,YAAY,IAAI,OAAO,CAAC,OAAO,CAAC;IAQhC,gBAAgB,CACpB,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,eAAe,EACzB,KAAK,EAAE,mBAAmB,GACzB,OAAO,CAAC,QAAQ,CAAC;IAad,YAAY,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,CAAC;CAO5D"}
@@ -32,7 +32,12 @@ export class NoOpAgentConnector {
32
32
  }
33
33
  async wake(agent_id, _opts) {
34
34
  process.stderr.write(`[NoOpAgentConnector] wake(${agent_id}) — no substrate wired; wake skipped.\n`);
35
- return { woken_at: Date.now() };
35
+ // v0.18.2 — graceful degradation: NoOp can't interrupt anything, so
36
+ // it reports `woken: false`. Callers reading the receipt distinguish
37
+ // delivered-only from actually-woke without needing per-substrate
38
+ // knowledge. Per Perry's degradation requirement: conform by
39
+ // degrading, never by erroring.
40
+ return { woken_at: Date.now(), woken: false };
36
41
  }
37
42
  async health_check() {
38
43
  // NoOp is always "healthy" — there's no real substrate to fail. Adopters
@@ -1 +1 @@
1
- {"version":3,"file":"agent-noop.js","sourceRoot":"","sources":["../../src/connectors/agent-noop.ts"],"names":[],"mappings":"AAaA;;;;;;;;;;;;;;;GAeG;AACH,MAAM,OAAO,kBAAkB;IAC7B,MAAM,CAAC,kBAAkB;QACvB,OAAO;YACL,cAAc,EAAE,iBAAiB;YACjC,cAAc,EAAE,oBAAoB;YACpC,gBAAgB,EAAE,OAAO;YACzB,QAAQ,EAAE,EAAE,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,WAAW,EAAE,IAAI,EAAE,YAAY,EAAE,IAAI,EAAE,YAAY,EAAE,IAAI,EAAE;SACnG,CAAC;IACJ,CAAC;IAED,KAAK,CAAC,WAAW;QACf,OAAO,EAAE,CAAC;IACZ,CAAC;IAED,KAAK,CAAC,OAAO,CAAC,QAAgB,EAAE,OAAwB;QACtD,OAAO,CAAC,MAAM,CAAC,KAAK,CAClB,gCAAgC,QAAQ,UAAU,OAAO,CAAC,IAAI,iBAAiB,OAAO,CAAC,IAAI,CAAC,WAAW,8CAA8C,CACtJ,CAAC;QACF,OAAO,EAAE,YAAY,EAAE,IAAI,CAAC,GAAG,EAAE,EAAE,CAAC;IACtC,CAAC;IAED,KAAK,CAAC,IAAI,CAAC,QAAgB,EAAE,KAAgB;QAC3C,OAAO,CAAC,MAAM,CAAC,KAAK,CAClB,6BAA6B,QAAQ,yCAAyC,CAC/E,CAAC;QACF,OAAO,EAAE,QAAQ,EAAE,IAAI,CAAC,GAAG,EAAE,EAAE,CAAC;IAClC,CAAC;IAED,KAAK,CAAC,YAAY;QAChB,yEAAyE;QACzE,0EAA0E;QAC1E,yEAAyE;QACzE,sEAAsE;QACtE,OAAO,IAAI,CAAC;IACd,CAAC;IAED,KAAK,CAAC,gBAAgB,CACpB,QAAgB,EAChB,QAAyB,EACzB,KAA0B;QAE1B,qEAAqE;QACrE,2EAA2E;QAC3E,uEAAuE;QACvE,wDAAwD;QACxD,MAAM,IAAI,KAAK,CACb,yCAAyC,QAAQ,uBAAuB;YACxE,yEAAyE;YACzE,yEAAyE;YACzE,8DAA8D,CAC/D,CAAC;IACJ,CAAC;IAED,KAAK,CAAC,YAAY,CAAC,SAAiB;QAClC,kEAAkE;QAClE,0EAA0E;QAC1E,sEAAsE;QACtE,2BAA2B;QAC3B,OAAO,SAAS,CAAC;IACnB,CAAC;CACF"}
1
+ {"version":3,"file":"agent-noop.js","sourceRoot":"","sources":["../../src/connectors/agent-noop.ts"],"names":[],"mappings":"AAaA;;;;;;;;;;;;;;;GAeG;AACH,MAAM,OAAO,kBAAkB;IAC7B,MAAM,CAAC,kBAAkB;QACvB,OAAO;YACL,cAAc,EAAE,iBAAiB;YACjC,cAAc,EAAE,oBAAoB;YACpC,gBAAgB,EAAE,OAAO;YACzB,QAAQ,EAAE,EAAE,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,WAAW,EAAE,IAAI,EAAE,YAAY,EAAE,IAAI,EAAE,YAAY,EAAE,IAAI,EAAE;SACnG,CAAC;IACJ,CAAC;IAED,KAAK,CAAC,WAAW;QACf,OAAO,EAAE,CAAC;IACZ,CAAC;IAED,KAAK,CAAC,OAAO,CAAC,QAAgB,EAAE,OAAwB;QACtD,OAAO,CAAC,MAAM,CAAC,KAAK,CAClB,gCAAgC,QAAQ,UAAU,OAAO,CAAC,IAAI,iBAAiB,OAAO,CAAC,IAAI,CAAC,WAAW,8CAA8C,CACtJ,CAAC;QACF,OAAO,EAAE,YAAY,EAAE,IAAI,CAAC,GAAG,EAAE,EAAE,CAAC;IACtC,CAAC;IAED,KAAK,CAAC,IAAI,CAAC,QAAgB,EAAE,KAAgB;QAC3C,OAAO,CAAC,MAAM,CAAC,KAAK,CAClB,6BAA6B,QAAQ,yCAAyC,CAC/E,CAAC;QACF,oEAAoE;QACpE,qEAAqE;QACrE,kEAAkE;QAClE,6DAA6D;QAC7D,gCAAgC;QAChC,OAAO,EAAE,QAAQ,EAAE,IAAI,CAAC,GAAG,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC;IAChD,CAAC;IAED,KAAK,CAAC,YAAY;QAChB,yEAAyE;QACzE,0EAA0E;QAC1E,yEAAyE;QACzE,sEAAsE;QACtE,OAAO,IAAI,CAAC;IACd,CAAC;IAED,KAAK,CAAC,gBAAgB,CACpB,QAAgB,EAChB,QAAyB,EACzB,KAA0B;QAE1B,qEAAqE;QACrE,2EAA2E;QAC3E,uEAAuE;QACvE,wDAAwD;QACxD,MAAM,IAAI,KAAK,CACb,yCAAyC,QAAQ,uBAAuB;YACxE,yEAAyE;YACzE,yEAAyE;YACzE,8DAA8D,CAC/D,CAAC;IACJ,CAAC;IAED,KAAK,CAAC,YAAY,CAAC,SAAiB;QAClC,kEAAkE;QAClE,0EAA0E;QAC1E,sEAAsE;QACtE,2BAA2B;QAC3B,OAAO,SAAS,CAAC;IACnB,CAAC;CACF"}
@@ -104,6 +104,14 @@ export interface DeliveryReceipt {
104
104
  delivered_at: number;
105
105
  /** Substrate-specific id for callers to correlate later. */
106
106
  delivery_id?: string;
107
+ /**
108
+ * v0.18.2 — the session that received the delivery. Set when the
109
+ * substrate routes to a specific session (mailbox-pull per terminal,
110
+ * webhook per running-tab, etc.). Omit if the substrate is
111
+ * agent-level only (Slack DM, email — no session concept) or if the
112
+ * substrate elected to fan out / accept without specific session.
113
+ */
114
+ session_id?: string;
107
115
  /**
108
116
  * Adopter signals "accepted but not pushed to the agent" — agent offline,
109
117
  * rate-limit drop, tmux session exists but agent hasn't read, etc.
@@ -141,9 +149,39 @@ export interface WakeOpts {
141
149
  context?: string;
142
150
  /** `"immediate"` (default) or a unix-ms timestamp for scheduled wake. */
143
151
  when?: "immediate" | number;
152
+ /**
153
+ * v0.18.2 — optional structured session targeting. Alternative to
154
+ * embedding `agent@session` in the `agent_id` opaque string. Callers
155
+ * with the session already separated (e.g., dashboard's per-session
156
+ * "wake this terminal" action) pass it here. Substrates that don't
157
+ * track sessions ignore the field; substrates that do route the wake
158
+ * to the named session. Per Perry's `e1ef003`-thread session-granular
159
+ * targeting requirement.
160
+ */
161
+ session_id?: string;
144
162
  }
163
+ /**
164
+ * v0.18.2 — `woken` is the required honesty signal. Wake-capable
165
+ * substrates (live tmux session, browser-tab open, push-channel
166
+ * connected) set `woken: true` after delivering an attention signal.
167
+ * Passive substrates (webhook, file-drop, store-only) can't interrupt
168
+ * and degrade gracefully: deliver the content, set `woken: false`. Per
169
+ * Perry's graceful-degradation requirement — "conform by degrading,
170
+ * never by erroring." Callers reading the receipt distinguish
171
+ * interrupted-them from delivered-only without needing per-substrate
172
+ * knowledge.
173
+ */
145
174
  export interface WakeReceipt {
146
175
  woken_at: number;
176
+ /** `true` if substrate actually woke the session; `false` if degraded to deliver-only. */
177
+ woken: boolean;
178
+ /**
179
+ * The session that received the wake (or delivery, if degraded). Set
180
+ * when the substrate knows; omit if the substrate doesn't track
181
+ * sessions. With session-targeted wake input, this typically echoes
182
+ * the requested session; with bare `agent_id`, the substrate's
183
+ * choice if it picks one.
184
+ */
147
185
  session_id?: string;
148
186
  }
149
187
  export interface AgentDescriptor {
@@ -176,7 +214,35 @@ export type AgentStatus = "active" | "idle" | "asleep" | "unknown";
176
214
  */
177
215
  export interface AgentConnector {
178
216
  list_agents(): Promise<AgentDescriptor[]>;
217
+ /**
218
+ * Durable put — "it's in their inbox, no interrupt."
219
+ *
220
+ * `agent_id` is an opaque string. The substrate may treat it as:
221
+ * - a bare agent identifier (Slack `@user`, email address, Discord user ID)
222
+ * - a composite `agent@session` (e.g., `"perry@kitchen-terminal"`) when the
223
+ * substrate tracks multiple live sessions per identity — the substrate
224
+ * decomposes the composite if it cares; non-session substrates ignore
225
+ * the suffix or treat the whole string as the address.
226
+ *
227
+ * Per Perry's v0.18.2 session-granular-targeting requirement: every
228
+ * messaging substrate needs to address either a bare identity or a
229
+ * specific live session. The opaque-composite form keeps the contract
230
+ * substrate-neutral while preserving the routing capability.
231
+ */
179
232
  deliver(agent_id: string, payload: DeliveryPayload): Promise<DeliveryReceipt>;
233
+ /**
234
+ * Attention signal — "go look at this now."
235
+ *
236
+ * `agent_id` accepts the same opaque/composite form as `deliver` (bare
237
+ * identity or `agent@session`). `opts.session_id` is the structured
238
+ * alternative for callers with the session already separated.
239
+ *
240
+ * Passive substrates that can't interrupt (webhook, file-drop, store-only)
241
+ * degrade gracefully — deliver the payload as if it were a `deliver()`
242
+ * call and set `woken: false` on the receipt. The contract's graceful-
243
+ * degradation rule (never throw for missing wake-capability) keeps
244
+ * non-wake-capable substrates conformant.
245
+ */
180
246
  wake(agent_id: string, opts?: WakeOpts): Promise<WakeReceipt>;
181
247
  /**
182
248
  * Bootstrap-time health probe. Adopter returns `true` if the substrate
@@ -1 +1 @@
1
- {"version":3,"file":"agent.d.ts","sourceRoot":"","sources":["../../src/connectors/agent.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"agent.d.ts","sourceRoot":"","sources":["../../src/connectors/agent.ts"],"names":[],"mappings":"AAuBA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAErD;;;;;;;;GAQG;AACH,MAAM,WAAW,YAAY;IAC3B;;;;;;;;;OASG;IACH,WAAW,EAAE,MAAM,CAAC;IAEpB;;;;;OAKG;IACH,OAAO,EAAE,MAAM,CAAC;IAEhB;;;OAGG;IACH,MAAM,EAAE;QACN,8DAA8D;QAC9D,UAAU,EAAE,MAAM,CAAC;QACnB;;;;;;WAMG;QACH,gBAAgB,CAAC,EAAE,MAAM,CAAC;QAC1B;;;;WAIG;QACH,YAAY,EAAE,MAAM,GAAG,SAAS,GAAG,SAAS,GAAG,OAAO,GAAG,KAAK,GAAG,WAAW,GAAG,QAAQ,CAAC;QACxF;;;;;;WAMG;QACH,eAAe,CAAC,EAAE,MAAM,CAAC;KAC1B,CAAC;IAEF;;;;;;;;;OASG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;IAEpB;;;;;;;;;OASG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAED;;;;;;;;;GASG;AACH,MAAM,MAAM,eAAe,GACvB;IAAE,IAAI,EAAE,SAAS,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,YAAY,CAAA;CAAE,GACxD;IAAE,IAAI,EAAE,UAAU,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,YAAY,CAAA;CAAE,CAAC;AAE7D,MAAM,WAAW,eAAe;IAC9B,6DAA6D;IAC7D,YAAY,EAAE,MAAM,CAAC;IACrB,4DAA4D;IAC5D,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;;;;OAMG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB;;;;;;;;;OASG;IACH,gBAAgB,CAAC,EAAE,OAAO,CAAC;CAC5B;AAED;;;;;GAKG;AACH,MAAM,WAAW,QAAQ;IACvB,gEAAgE;IAChE,cAAc,EAAE,MAAM,CAAC;IACvB,yBAAyB;IACzB,OAAO,EAAE,MAAM,CAAC;IAChB,qDAAqD;IACrD,OAAO,EAAE,MAAM,CAAC;IAChB,6BAA6B;IAC7B,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,mBAAmB;IAClC,mDAAmD;IACnD,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,QAAQ;IACvB,wDAAwD;IACxD,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,yEAAyE;IACzE,IAAI,CAAC,EAAE,WAAW,GAAG,MAAM,CAAC;IAC5B;;;;;;;;OAQG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED;;;;;;;;;;GAUG;AACH,MAAM,WAAW,WAAW;IAC1B,QAAQ,EAAE,MAAM,CAAC;IACjB,0FAA0F;IAC1F,KAAK,EAAE,OAAO,CAAC;IACf;;;;;;OAMG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,eAAe;IAC9B,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,YAAY,CAAC,EAAE,aAAa,CAAC,SAAS,GAAG,MAAM,GAAG,SAAS,GAAG,UAAU,CAAC,CAAC;CAC3E;AAED;;;;;;;;;;GAUG;AACH,MAAM,MAAM,WAAW,GAAG,QAAQ,GAAG,MAAM,GAAG,QAAQ,GAAG,SAAS,CAAC;AAEnE;;;;;;;;;;GAUG;AACH,MAAM,WAAW,cAAc;IAC7B,WAAW,IAAI,OAAO,CAAC,eAAe,EAAE,CAAC,CAAC;IAC1C;;;;;;;;;;;;;;OAcG;IACH,OAAO,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,eAAe,GAAG,OAAO,CAAC,eAAe,CAAC,CAAC;IAC9E;;;;;;;;;;;;OAYG;IACH,IAAI,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,QAAQ,GAAG,OAAO,CAAC,WAAW,CAAC,CAAC;IAC9D;;;;OAIG;IACH,YAAY,IAAI,OAAO,CAAC,OAAO,CAAC,CAAC;IACjC;;;;;OAKG;IACH,gBAAgB,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,eAAe,EAAE,IAAI,EAAE,mBAAmB,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAC;IAC3G,YAAY,CAAC,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,CAAC,CAAC;CACvD;AAED,MAAM,WAAW,mBAAmB;IAClC,KAAK,GAAG,IAAI,EAAE,KAAK,EAAE,GAAG,cAAc,CAAC;IACvC,kBAAkB,IAAI,kBAAkB,CAAC;CAC1C"}
@@ -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.*
@@ -233,13 +233,17 @@ export class HttpWebhookAgentConnector implements AgentConnector {
233
233
  async wake(agent_id: string, _opts?: WakeOpts): Promise<WakeReceipt> {
234
234
  const cfg = this.agents[agent_id];
235
235
  if (cfg === undefined) {
236
+ // Caller misconfiguration (agent not in HTTP_WEBHOOK_AGENTS) — still
237
+ // throws. Distinct from the substrate-can't-wake case below.
236
238
  throw new Error(`wake('${agent_id}'): agent not configured`);
237
239
  }
238
240
  if (cfg.wake_url === undefined) {
239
- throw new Error(
240
- `wake('${agent_id}'): no wake_url configured for this agent. ` +
241
- `Add "wake_url" to the agent's config entry in HTTP_WEBHOOK_AGENTS if your substrate supports wake.`,
242
- );
241
+ // v0.18.2 — graceful degradation. No wake URL configured for this
242
+ // agent means this connector is "passive" for them webhook can
243
+ // deliver content but can't interrupt. Per Perry's degradation
244
+ // requirement: conform by degrading, never by erroring. Caller
245
+ // reads `woken: false` and knows the wake was downgraded.
246
+ return { woken_at: Date.now(), woken: false };
243
247
  }
244
248
  // Adopter-specific wake protocol; bundled example just POSTs to the URL.
245
249
  const response = await fetch(cfg.wake_url, {
@@ -249,9 +253,11 @@ export class HttpWebhookAgentConnector implements AgentConnector {
249
253
  signal: AbortSignal.timeout(this.timeout_ms),
250
254
  });
251
255
  if (response.status >= 400) {
256
+ // Real substrate-side failure — still throws. Distinct from the
257
+ // graceful-degradation case above (no wake URL configured at all).
252
258
  throw new Error(`wake('${agent_id}'): HTTP ${response.status}`);
253
259
  }
254
- return { woken_at: Date.now() };
260
+ return { woken_at: Date.now(), woken: true };
255
261
  }
256
262
 
257
263
  async health_check(): Promise<boolean> {
@@ -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
 
@@ -278,17 +278,20 @@ describe("HttpWebhookAgentConnector — list_agents / health_check / wake / requ
278
278
  expect(await conn.health_check()).toBe(true);
279
279
  });
280
280
 
281
- it("wake throws when wake_url not configured for the agent", async () => {
281
+ it("wake degrades gracefully when wake_url not configured (v0.18.2 returns woken: false instead of throwing)", async () => {
282
282
  const conn = new HttpWebhookAgentConnector({ agents: { "agent-x": { url: mock.url } } });
283
- await expect(conn.wake("agent-x")).rejects.toThrow(/no wake_url configured/);
283
+ const receipt = await conn.wake("agent-x");
284
+ expect(receipt.woken).toBe(false);
285
+ expect(typeof receipt.woken_at).toBe("number");
284
286
  });
285
287
 
286
- it("wake POSTs to wake_url when configured", async () => {
288
+ it("wake POSTs to wake_url when configured + reports woken: true", async () => {
287
289
  const conn = new HttpWebhookAgentConnector({
288
290
  agents: { "agent-x": { url: mock.url, wake_url: `${mock.url}/wake` } },
289
291
  });
290
292
  const receipt = await conn.wake("agent-x");
291
293
  expect(typeof receipt.woken_at).toBe("number");
294
+ expect(receipt.woken).toBe(true);
292
295
  expect(mock.received).toHaveLength(1);
293
296
  expect(mock.received[0]!.url).toBe("/wake");
294
297
  });
@@ -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.611Z",
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.1",
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>",