skillscript-runtime 0.19.7 → 0.19.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +31 -27
- package/dist/connectors/config.d.ts.map +1 -1
- package/dist/connectors/config.js +17 -0
- package/dist/connectors/config.js.map +1 -1
- package/dist/connectors/mcp-remote.d.ts.map +1 -1
- package/dist/connectors/mcp-remote.js +10 -1
- package/dist/connectors/mcp-remote.js.map +1 -1
- package/dist/parser.d.ts.map +1 -1
- package/dist/parser.js +99 -57
- package/dist/parser.js.map +1 -1
- package/docs/adopter-playbook.md +17 -5
- package/docs/configuration.md +13 -2
- package/docs/language-reference.md +181 -159
- package/examples/skillscripts/hello-world.skill.provenance.json +1 -1
- package/package.json +1 -1
- package/scaffold/connectors.json +5 -1
|
@@ -18,12 +18,9 @@ These are features designed or anticipated but not yet implemented in the curren
|
|
|
18
18
|
|
|
19
19
|
- **Array aggregation primitives** — `|max`, `|min`, `|sum`, `|reduce` over arrays. Today the language tops out at "shape one record" — aggregating across an array requires `foreach` accumulator ceremony. Planned as a design question: is `foreach` the deliberate ceiling for aggregation, or do we add primitives?
|
|
20
20
|
|
|
21
|
-
## Triggers
|
|
21
|
+
## Triggers — note
|
|
22
22
|
|
|
23
|
-
|
|
24
|
-
- `agent-event:` — agent-emitted events
|
|
25
|
-
- `file-watch:` — filesystem change events
|
|
26
|
-
- `sensor:` — continuous sensor channels
|
|
23
|
+
Trigger sources are `cron` + `event` only (see the Triggers section). There is no separate `agent-event:` / `file-watch:` / `sensor:` trigger source — anything that isn't time-based is an external adapter that POSTs to the `/event` ingress. So there's nothing pending here on the trigger axis; the entry remains only to point readers who expect those sources at the adapter-POST pattern.
|
|
27
24
|
|
|
28
25
|
## Synchronous agent exchange
|
|
29
26
|
|
|
@@ -48,7 +45,9 @@ $set NAME = value scope=session
|
|
|
48
45
|
|
|
49
46
|
Scopes: skill-local (persists across fires of this skill, not visible to other skills), agent-global (visible to all skills of the same agent), session (alive for the duration of the current session, cleared at session end). Backed by a configured data-records connector.
|
|
50
47
|
|
|
51
|
-
## Sensors as a
|
|
48
|
+
## Sensors as a read-channel category
|
|
49
|
+
|
|
50
|
+
> `sensor:` is not a trigger source — to fire a skill on a sensor signal, a sensor adapter POSTs to `/event` (see Triggers). The concept below is the separate *read-channel* idea — continuous values an agent reads, distinct from triggering — which remains a future possibility if demand surfaces.
|
|
52
51
|
|
|
53
52
|
Distinct from triggers. Sensors are continuous channels the agent reads but doesn't emit on. Planned syntax:
|
|
54
53
|
|
|
@@ -72,7 +71,7 @@ Most "right time" reasoning is relative, not wall-clock.
|
|
|
72
71
|
|
|
73
72
|
## Other planned
|
|
74
73
|
|
|
75
|
-
- **Absence-as-trigger** — `# Triggers: idle: 5m` fire-on-quiet primitive
|
|
74
|
+
- **Absence-as-trigger** — `# Triggers: idle: 5m` fire-on-quiet primitive (would be a new trigger source beyond `cron`|`event`, or an adapter that POSTs on a timer)
|
|
76
75
|
- **Time-windowed aggregation** — filter-like primitives across firings (e.g., "user has shown frustration in 3 of 5 recent turns")
|
|
77
76
|
- **Debounce / rate-limit / coalesce** — declarative queueing policy headers
|
|
78
77
|
- **Suppression as valid output** — explicit "fire-and-suppress" (different from `# Output: none`)
|
|
@@ -95,35 +94,37 @@ Skillscript is a constrained domain-specific language for authoring agent workfl
|
|
|
95
94
|
|
|
96
95
|
Every skill follows the same shape:
|
|
97
96
|
|
|
98
|
-
1. **Trigger** — what fires the skill: cron
|
|
97
|
+
1. **Trigger** — what fires the skill autonomously: `cron` (time-based) or `event` (an external HTTP POST to the runtime's `/event` ingress). (A skill can also be invoked explicitly — by an agent mid-conversation or via the execute API — with no trigger at all.)
|
|
99
98
|
2. **Process** — pull data (MCP / data store / file), classify or compose via sub-LLM + iteration, build the deliverable.
|
|
100
|
-
3. **Deliver** —
|
|
99
|
+
3. **Deliver** — produce the result via the **body-text output template** (declarative — the skill body *is* the output) or via `emit()` (imperative, for dynamic output), then route it through one or more delivery channels (see below; production detail in Output).
|
|
101
100
|
|
|
102
101
|
Skillscript's job is to express this pipeline declaratively. When there is an agent above the skill, the agent's job is to act on the delivered artifact. When there isn't (autonomous fires), the delivery channel IS the outcome.
|
|
103
102
|
|
|
104
103
|
**Declarative DAG, not imperative script.** A skillscript declares targets and their dependencies (`needs:` keyword); the interpreter topologically sorts and executes them in dependency order. Write blocks in any order — the runtime walks the graph.
|
|
105
104
|
|
|
106
|
-
**Goal-directed, not entry-point-directed.** The `default:` declaration names the *goal target* — the terminal node whose result is the skill's output. The runtime walks dependencies backward from the goal through the topo-sort. A skill with a single target obscures this (goal == entry trivially); skills with multi-target DAGs make the shape visible.
|
|
105
|
+
**Goal-directed, not entry-point-directed.** The `default:` declaration names the *goal target* — the terminal node whose result is the skill's output. The runtime walks dependencies backward from the goal through the topo-sort. A skill with a single target obscures this (goal == entry trivially); skills with multi-target DAGs make the shape visible. (A skill with *no* target is also valid — a body-template-only skill; see Output.)
|
|
107
106
|
|
|
108
107
|
## Two execution paths
|
|
109
108
|
|
|
110
|
-
**Runtime-mediated** — the interpreter walks ops and dispatches them directly through configured connectors. Used for autonomous fires (cron
|
|
109
|
+
**Runtime-mediated** — the interpreter walks ops and dispatches them directly through configured connectors. Used for autonomous fires (cron- or event-triggered). Safety boundary is the connector config + per-op gating (see Ops Reference).
|
|
111
110
|
|
|
112
111
|
**Agent-mediated** — the compiler renders the skill as a prompt; an agent reads the prompt and executes ops through its own tools. Used when an agent invokes a skill mid-conversation. Safety boundary is the agent's harness tool permissions.
|
|
113
112
|
|
|
114
113
|
The language is identical in both paths. The execution model is a deployment-time + invocation-time decision.
|
|
115
114
|
|
|
116
|
-
##
|
|
115
|
+
## Output production and delivery channels
|
|
116
|
+
|
|
117
|
+
**Production:** a skill's output is produced either declaratively by the **body-text template** (the skill body, with `${var}` interpolation — the clean default for fixed-shape output) or imperatively by **`emit()`** (for variable-cardinality / conditional-shape / transcript output). See Output for the when-which rule. The template, when present, is the canonical output; `emit` feeds the transcript.
|
|
117
118
|
|
|
118
|
-
|
|
119
|
+
**Delivery:** the produced output is routed by the `# Output:` kind to one or more channels. Delivery channel is not a property of skill type — it's just the `# Output:` declaration.
|
|
119
120
|
|
|
120
|
-
| Channel |
|
|
121
|
+
| Channel | Declaration | When you'd use it |
|
|
121
122
|
|---|---|---|
|
|
122
|
-
| **Augmenting (context to agent)** | `emit(
|
|
123
|
-
| **Template (playbook to agent)** | `emit(
|
|
123
|
+
| **Augmenting (context to agent)** | body template or `emit()` + `# Output: agent: <name>` | Output is augment-kind payload for the receiving agent's next turn. Pattern: agent-augmenting skills (briefings, session-start prepared context). |
|
|
124
|
+
| **Template (playbook to agent)** | body template or `emit()` + `# Output: template: <name>` | Output is a template-kind payload (recipe/playbook) the receiving agent executes. Pattern: instructional skills, reusable recipes. |
|
|
124
125
|
| **Data handoff** | `$ data_write content="..." recipients=[<agent>] -> R` | Skill writes data the target agent picks up via mailbox at next session. Pattern: async carrier skills, autonomous fires that hand off to a future session. |
|
|
125
126
|
|
|
126
|
-
A single skill can use any combination. An autonomous cron-fired sweep might write data to one agent AND
|
|
127
|
+
A single skill can use any combination. An autonomous cron-fired sweep might write data to one agent AND deliver augment-kind context to another. The combinations are unconstrained — the per-op gating model governs which mutating ops are authorized, not which channels a skill uses.
|
|
127
128
|
|
|
128
129
|
## Three op classes
|
|
129
130
|
|
|
@@ -160,12 +161,12 @@ Substrate config syntax + the three-form configuration shape lives in the adopte
|
|
|
160
161
|
|
|
161
162
|
## Anatomy of a skill
|
|
162
163
|
|
|
163
|
-
A canonical example exercising trigger → process → deliver against the augmenting channel
|
|
164
|
+
A canonical example exercising trigger → process → deliver against the augmenting channel. (Output here is built with `emit()` because the per-issue count is variable — the right case for `emit`; a fixed-shape skill would use a body template instead.)
|
|
164
165
|
|
|
165
166
|
```
|
|
166
167
|
# Skill: morning-showstopper-sweep
|
|
167
168
|
# Description: Pre-triage open showstoppers before the human arrives; deliver as augmenting context to the on-call agent.
|
|
168
|
-
# Triggers: cron:
|
|
169
|
+
# Triggers: cron: 0 8 * * MON-FRI
|
|
169
170
|
# Output: agent: oncall
|
|
170
171
|
# Vars: PROJECT = "INFRA"
|
|
171
172
|
|
|
@@ -187,7 +188,7 @@ sweep:
|
|
|
187
188
|
default: sweep
|
|
188
189
|
```
|
|
189
190
|
|
|
190
|
-
The joined `emit()` stream becomes the augment-kind payload delivered to the on-call agent (per `# Output: agent: oncall`
|
|
191
|
+
The joined `emit()` stream becomes the augment-kind payload delivered to the on-call agent (per `# Output: agent: oncall`). The agent sees the briefing inline at next-turn dispatch.
|
|
191
192
|
|
|
192
193
|
**Three layers of declaration:**
|
|
193
194
|
1. **Header metadata** (`# Key: value` lines) — name, description, declared variables (`# Vars:`), declared returns (`# Returns:` — the export surface, output-side mirror of `# Vars:`; see Composition), triggers, `# Output:` routing, optional `# Autonomous:` flag, error fallbacks
|
|
@@ -276,7 +277,7 @@ The conditional grammar (`==` / `<` / `>` / `in` / `and` / `or` / `not`) lives i
|
|
|
276
277
|
|
|
277
278
|
When you reach for a primitive that isn't in the language (`|max` over an array, modular arithmetic, regex substitution, date math), the stopping rule isn't "feature shelved." It's: *can a tool do this work, and can the skill body invoke that tool via `$` or `shell`?* Almost always yes.
|
|
278
279
|
|
|
279
|
-
**The universal computation escape is `shell + standard CLI tools.**
|
|
280
|
+
**The universal computation escape is `shell` + standard CLI tools.**
|
|
280
281
|
|
|
281
282
|
```
|
|
282
283
|
shell(command="echo ${RAW|shell} | jq -r '[.weather[0].hourly[] | .chanceofrain | tonumber] | max'", unsafe=true) -> MAX_RAIN
|
|
@@ -418,20 +419,17 @@ Per-output-kind consumption semantics: presentation surfaces (`# Output: agent:
|
|
|
418
419
|
```
|
|
419
420
|
notify(agent="oncall", message="Threshold breached at ${COUNT}")
|
|
420
421
|
notify(agent="ops", message="ticket TR-1234 is a showstopper", event_type="ticket-911", correlation_id="${INCIDENT_ID}")
|
|
421
|
-
notify(agent="cc@kitchen-terminal", message="look here now")
|
|
422
422
|
```
|
|
423
423
|
|
|
424
424
|
Synchronous alert to a named agent via wired AgentConnector(s). **Contrast with `emit`:** `emit` accumulates into end-of-skill bulk delivery via the `# Output: agent: <name>` lifecycle hook; `notify` fires mid-execution to interrupt or page an agent before the skill completes.
|
|
425
425
|
|
|
426
|
-
- `agent` — target agent id (required)
|
|
427
|
-
- `message` — alert body (optional; defaults to accumulated emissions so far)
|
|
428
|
-
- `event_type` — adopter-defined routing label (optional; flows to `DeliveryMeta.event_type
|
|
429
|
-
- `correlation_id` — reply-correlation id (optional; required for future `exchange()` / `request_response()` paths)
|
|
430
|
-
- `connectors` — JSON array restricting which wired AgentConnector(s) receive the dispatch (optional)
|
|
431
|
-
|
|
432
|
-
Returns ACK `{agent, dispatched: [{connector, ok, route?, error?}]}` — `route` is `"deliver"` or `"wake"` per address-routing. Fire-and-forget callers ignore the binding; check-delivery callers inspect ACK.
|
|
426
|
+
- `agent` — target agent id (required)
|
|
427
|
+
- `message` — alert body (optional; defaults to accumulated emissions so far)
|
|
428
|
+
- `event_type` — adopter-defined routing label (optional; flows to `DeliveryMeta.event_type`; overrides `# Event-type:` frontmatter)
|
|
429
|
+
- `correlation_id` — reply-correlation id (optional; required for future `exchange()` / `request_response()` paths)
|
|
430
|
+
- `connectors` — JSON array restricting which wired AgentConnector(s) receive the dispatch (optional)
|
|
433
431
|
|
|
434
|
-
|
|
432
|
+
Returns ACK `{agent, dispatched: [{connector, ok, error?}]}` — fire-and-forget callers ignore the binding; check-delivery callers inspect ACK.
|
|
435
433
|
|
|
436
434
|
### `shell` — sandboxed or unsafe shell exec
|
|
437
435
|
|
|
@@ -1224,36 +1222,32 @@ Skills are orchestration, not computation. When the conditional logic feels Turi
|
|
|
1224
1222
|
|
|
1225
1223
|
## Triggers — # Triggers: header, declarative + imperative registration, source types
|
|
1226
1224
|
|
|
1227
|
-
Triggers declare what
|
|
1225
|
+
Triggers declare what fires a skill autonomously. A skill without triggers must be invoked explicitly (the compile/execute API); a skill with triggers fires automatically when its trigger condition occurs.
|
|
1228
1226
|
|
|
1229
|
-
|
|
1227
|
+
The trigger surface is **two sources: `cron` (time) and `event` (external signal)**. `session`, `agent-event`, `file-watch`, and `sensor` are not trigger sources — anything that isn't time-based is an external adapter that POSTs to the `/event` ingress. The runtime owns time (`cron`) and the event endpoint; source-specific watching (a file-watcher, a sensor stream, an agent-event bridge) is the adapter's job.
|
|
1230
1228
|
|
|
1231
|
-
|
|
1229
|
+
## Declarative registration via `# Triggers:` header
|
|
1232
1230
|
|
|
1233
1231
|
```
|
|
1234
|
-
# Triggers: cron: 0 8 * *
|
|
1232
|
+
# Triggers: cron: 0 8 * * *
|
|
1233
|
+
# Triggers: event: deploy-finished
|
|
1235
1234
|
```
|
|
1236
1235
|
|
|
1237
|
-
|
|
1236
|
+
The trigger registry parses the header and registers each trigger. A skill re-registering **its own** event_name is a silent upsert — the declarative re-save path stays friction-free.
|
|
1238
1237
|
|
|
1239
1238
|
## Imperative registration
|
|
1240
1239
|
|
|
1241
|
-
For dynamic, one-shot, or runtime-decided triggers, use the imperative `registerTrigger` API:
|
|
1242
|
-
|
|
1243
1240
|
```
|
|
1244
1241
|
registerTrigger({
|
|
1245
|
-
skill_name: "
|
|
1246
|
-
source: "cron"
|
|
1247
|
-
name: "
|
|
1248
|
-
expires_at: 1779107400 // optional auto-cleanup
|
|
1242
|
+
skill_name: "notify-deploy",
|
|
1243
|
+
source: "event", // "cron" | "event"
|
|
1244
|
+
name: "deploy-finished" // cron expression for cron; the event_name for event
|
|
1249
1245
|
})
|
|
1250
1246
|
```
|
|
1251
1247
|
|
|
1252
|
-
Imperative triggers default to
|
|
1248
|
+
Imperative triggers default to 30-day expiry (`expires_at`); pass `null` for indefinite and clean up via `unregisterTrigger`.
|
|
1253
1249
|
|
|
1254
|
-
##
|
|
1255
|
-
|
|
1256
|
-
### `cron: <expression>` — time-based
|
|
1250
|
+
## Source: `cron: <expr>` — time-based
|
|
1257
1251
|
|
|
1258
1252
|
Standard 5-field cron. Sliding-window evaluation by a 30s poll loop. No catch-up replay if the runtime was down at fire time.
|
|
1259
1253
|
|
|
@@ -1261,184 +1255,212 @@ Standard 5-field cron. Sliding-window evaluation by a 30s poll loop. No catch-up
|
|
|
1261
1255
|
# Triggers: cron: 0 3 * * *
|
|
1262
1256
|
```
|
|
1263
1257
|
|
|
1264
|
-
|
|
1258
|
+
## Source: `event: <event_name>` — external HTTP signal
|
|
1265
1259
|
|
|
1266
|
-
|
|
1260
|
+
A skill fires when an external caller POSTs its `event_name` to the runtime's `/event` ingress.
|
|
1267
1261
|
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1262
|
+
**`event_name` is the public contract.**
|
|
1263
|
+
- Case-insensitive (normalized at register + lookup).
|
|
1264
|
+
- **1:1** — one `event_name` → exactly one skill, unique per deployment.
|
|
1265
|
+
- Posters address the **event_name**, never the skill — so you can swap the skill behind an event without breaking callers, and no POST can reach an arbitrary skill.
|
|
1266
|
+
- **Rebind:** re-registering an already-bound `event_name` is allowed (last-write-wins). Same skill → silent upsert. A repoint to a **different** skill is **audit-logged** (`event_name 'X' rebound: skill A → skill B`), so a cross-skill takeover is never silent.
|
|
1272
1267
|
|
|
1273
|
-
|
|
1268
|
+
**Params auto-derive from the skill's `# Vars:`.** You do NOT declare params separately on the trigger — the event's accepted parameter set *is* the skill's declared `# Vars:`. A POST supplies values for those names. (Both the declarative and imperative registration paths derive params from `# Vars:`.)
|
|
1274
1269
|
|
|
1275
|
-
|
|
1270
|
+
```
|
|
1271
|
+
# Skill: notify-deploy
|
|
1272
|
+
# Vars: SERVICE="", VERSION=""
|
|
1273
|
+
# Triggers: event: deploy-finished
|
|
1274
|
+
```
|
|
1275
|
+
→ the `deploy-finished` event accepts params `SERVICE`, `VERSION`.
|
|
1276
1276
|
|
|
1277
|
-
|
|
1278
|
-
- `event: thread.replied` — a thread receives a new reply
|
|
1279
|
-
- `event: mailbox.dangle` — an addressed item expires unprocessed
|
|
1280
|
-
- `event: classifier.flagged` — a background classifier surfaces an urgent finding
|
|
1281
|
-
- (extensible via runtime-host event registration)
|
|
1277
|
+
## The `/event` HTTP ingress
|
|
1282
1278
|
|
|
1283
|
-
|
|
1279
|
+
- **Off by default.** Enable with `SKILLSCRIPT_EVENT_INGRESS_ENABLED=true`.
|
|
1280
|
+
- Runs on the **DashboardServer** (reuses the dashboard port — no second server).
|
|
1281
|
+
- **Binds localhost by default.** Reach beyond localhost only via explicit bind config.
|
|
1282
|
+
- **Optional bearer auth:** set `SKILLSCRIPT_EVENT_INGRESS_AUTH_TOKEN`; when set, every POST must carry `Authorization: Bearer <token>`.
|
|
1284
1283
|
|
|
1285
|
-
|
|
1284
|
+
**Request:** `POST /event`, JSON body `{ "event_name": "...", "params": { ... } }`.
|
|
1286
1285
|
|
|
1287
|
-
|
|
1288
|
-
# Triggers: agent-event: builder.task.completed
|
|
1289
|
-
```
|
|
1286
|
+
**Param validation (strict):** all declared params present, no unknown params, no defaults, no type coercion (JSON types pass through; a type mismatch surfaces at the consuming op, not at the ingress).
|
|
1290
1287
|
|
|
1291
|
-
|
|
1288
|
+
**Responses:**
|
|
1289
|
+
- `200` — accepted + queued. Body: `{ "run_id": "<trace_id>", "durability": "in-process" }`
|
|
1290
|
+
- `400` — missing or unknown params (body names the offending set)
|
|
1291
|
+
- `401` — missing/wrong bearer token (when auth is configured)
|
|
1292
|
+
- `404` — unknown `event_name`
|
|
1293
|
+
- `405` — wrong method
|
|
1292
1294
|
|
|
1293
|
-
|
|
1295
|
+
**Async semantics (important):** `200` means **accepted into the in-process queue, NOT skill-completed.** The skill runs asynchronously; the response never reflects skill success/failure. `durability: "in-process"` is self-describing: the queue is **best-effort, not durable across a restart** — the same property `cron` has (no catch-up replay). For at-least-once delivery the caller retries; durable queuing is not provided.
|
|
1294
1296
|
|
|
1295
|
-
|
|
1297
|
+
**`run_id` = `trace_id`.** The `run_id` in the 200 is the fire's `trace_id` — it round-trips to the fire record (the `fires` query / dashboard `/fires` view / the trace file on disk), so a caller can later check whether its fire completed.
|
|
1296
1298
|
|
|
1297
|
-
|
|
1299
|
+
## Canonical example — calling `/event`
|
|
1298
1300
|
|
|
1299
|
-
|
|
1301
|
+
The `notify-deploy` skill above, fired by an external deploy pipeline:
|
|
1300
1302
|
|
|
1301
1303
|
```
|
|
1302
|
-
|
|
1304
|
+
curl -X POST http://localhost:7878/event \
|
|
1305
|
+
-H "Authorization: Bearer $SKILLSCRIPT_EVENT_INGRESS_AUTH_TOKEN" \
|
|
1306
|
+
-H "Content-Type: application/json" \
|
|
1307
|
+
-d '{"event_name":"deploy-finished","params":{"SERVICE":"api","VERSION":"1.2.3"}}'
|
|
1308
|
+
# → 200 {"run_id":"7f3c...","durability":"in-process"}
|
|
1303
1309
|
```
|
|
1304
1310
|
|
|
1305
|
-
|
|
1311
|
+
**This is the pattern for every external trigger source.** A file-watcher, a sensor stream, an agent-event bridge, a webhook relay — each is just an external adapter that watches its source and POSTs to `/event`. (A file-watch adapter, e.g.: watch a path; on change, `POST {event_name, params:{path, kind}}`.)
|
|
1306
1312
|
|
|
1307
|
-
|
|
1313
|
+
## Trigger context
|
|
1308
1314
|
|
|
1309
|
-
|
|
1315
|
+
When a skill fires from a trigger, the runtime populates ambient refs:
|
|
1316
|
+
- `${TRIGGER_TYPE}` — `cron` | `event`
|
|
1310
1317
|
- `${TRIGGER_PAYLOAD}` — source-specific data
|
|
1311
|
-
-
|
|
1318
|
+
- For event fires, the POSTed params land in the skill's vars (per `# Vars:`).
|
|
1312
1319
|
|
|
1313
1320
|
## Trigger lifecycle
|
|
1314
1321
|
|
|
1315
|
-
- **
|
|
1316
|
-
- **
|
|
1317
|
-
- **
|
|
1318
|
-
- **Archival:** `unregisterTrigger(trigger_id)` archives the trigger (audit trail preserved); declarative triggers are removed by editing the skill body to drop the declaration
|
|
1322
|
+
- **Register:** declarative (auto on skill write) or imperative (`registerTrigger`).
|
|
1323
|
+
- **Inspect:** `listTriggers({ skill?, source? })`.
|
|
1324
|
+
- **Remove:** `unregisterTrigger(trigger_id)` (archives; audit trail preserved). Declarative triggers are removed by editing the `# Triggers:` line out of the skill body.
|
|
1319
1325
|
|
|
1320
1326
|
## Multiple triggers
|
|
1321
1327
|
|
|
1322
|
-
A skill may declare multiple triggers; each fires an independent execution. The compiled output is identical regardless of trigger; the runtime distinguishes via `${TRIGGER_TYPE}`.
|
|
1323
|
-
|
|
1324
|
-
Open spec question: dedup on near-simultaneous fires. If `cron: 0 8 * * *` and `event: user.present` both fire within seconds, the runtime currently runs the skill twice (one per trigger). Author dedups via state if needed.
|
|
1328
|
+
A skill may declare multiple triggers; each fires an independent execution. The compiled output is identical regardless of trigger; the runtime distinguishes via `${TRIGGER_TYPE}`. Near-simultaneous dedup is the author's responsibility (via state).
|
|
1325
1329
|
|
|
1326
1330
|
## Output targets — # Output: header, delivery kinds
|
|
1327
1331
|
|
|
1328
|
-
|
|
1332
|
+
A skill's **body text is its output template**, and the `# Output:` header declares where that output is delivered. Default delivery (no `# Output:` header) is `text` — returned to the caller.
|
|
1329
1333
|
|
|
1330
|
-
##
|
|
1334
|
+
## Two ways to produce output: the template vs `emit` — and when to use each
|
|
1331
1335
|
|
|
1332
|
-
|
|
1336
|
+
A skill produces output two ways. Choosing the right one is the difference between a clean skill and a confusing one:
|
|
1337
|
+
|
|
1338
|
+
- **Body-text template (declarative) — for fixed-shape output.** Write the output literally in the body, interpolating `${vars}`. Use it when you know the output's shape and are just filling in values.
|
|
1339
|
+
- **`emit(...)` (imperative) — for output whose shape or count is decided at runtime.** `emit` is NOT deprecated; it's the right tool for:
|
|
1340
|
+
1. **Variable-cardinality output** — `foreach IT in ${LIST}: emit(...)` produces a number of lines decided at runtime, which a single template can't express.
|
|
1341
|
+
2. **Conditional whole-output** — branches that produce genuinely different output *shapes* (not merely different values).
|
|
1342
|
+
3. **Reasoning / transcript trace** — `emit` always writes to the transcript channel for debugging.
|
|
1343
|
+
|
|
1344
|
+
**Rule of thumb:** if you can write the output as one block of text with `${vars}` filled in, use the template. If the output's *shape or count* varies at runtime, use `emit`.
|
|
1345
|
+
|
|
1346
|
+
**They are complementary channels, not competitors.** When a body template is present, it owns the *canonical output*; `emit` always feeds the *transcript* channel. A skill may use both — a clean declarative output plus a rich transcript — with no conflict.
|
|
1333
1347
|
|
|
1334
|
-
|
|
1348
|
+
## The body-text output template
|
|
1349
|
+
|
|
1350
|
+
Body text — at the top, the bottom, or between targets — is the output template (it may appear **anywhere a target doesn't**; see Grammar). It interpolates, at runtime:
|
|
1351
|
+
- `# Vars:` declared inputs, and
|
|
1352
|
+
- any variable bound by the body (`$set`, `-> VAR`).
|
|
1335
1353
|
|
|
1336
1354
|
```
|
|
1337
|
-
#
|
|
1355
|
+
# Skill: get-weather
|
|
1356
|
+
# Vars: LOCATION="", UNITS="imperial"
|
|
1357
|
+
|
|
1358
|
+
${AREA}: ${TEMP}${UNIT} and ${DESC}. High ${HIGH}${UNIT}, low ${LOW}${UNIT}.
|
|
1359
|
+
|
|
1360
|
+
fetch:
|
|
1361
|
+
shell(command="curl -s --max-time 10 https://wttr.in/${LOCATION|url}?format=j1") -> RAW
|
|
1362
|
+
$ json_parse ${RAW} -> W
|
|
1363
|
+
$set AREA = "${W.nearest_area.0.areaName.0.value}"
|
|
1364
|
+
$set DESC = "${W.current_condition.0.weatherDesc.0.value|trim}"
|
|
1365
|
+
if ${UNITS} == "metric":
|
|
1366
|
+
$set TEMP = "${W.current_condition.0.temp_C}" $set UNIT = "°C" # + HIGH/LOW
|
|
1367
|
+
else:
|
|
1368
|
+
$set TEMP = "${W.current_condition.0.temp_F}" $set UNIT = "°F" # + HIGH/LOW
|
|
1369
|
+
default: fetch
|
|
1338
1370
|
```
|
|
1371
|
+
Renders to the canonical output: `Valdese: 81°F and Sunny. High 93°F, low 64°F.` — no `emit`.
|
|
1339
1372
|
|
|
1340
|
-
|
|
1373
|
+
**Lint:** every `${var}` in the template must be a `# Vars:` input or bound by the body. An unbound reference is a tier-1 error (`unset-template-var`). The coupling is mechanical (variable names), so it is checkable.
|
|
1341
1374
|
|
|
1342
|
-
|
|
1375
|
+
**Branching stays single-shape:** resolve a fork *inside the block* into vars (metric/imperial → `TEMP`/`UNIT`) so the template is one shape. Whole-output conditionals are an `emit` job, not a template job.
|
|
1343
1376
|
|
|
1377
|
+
**A body-template-only skill is valid.** A skill that is just a template + inputs, with **no compute block / no target**, compiles and runs — the template alone is the skill. `hello-world` is exactly this:
|
|
1344
1378
|
```
|
|
1345
|
-
#
|
|
1346
|
-
#
|
|
1379
|
+
# Skill: hello-world
|
|
1380
|
+
# Status: Approved
|
|
1381
|
+
# Vars: WHO=world
|
|
1382
|
+
|
|
1383
|
+
Hello, ${WHO}!
|
|
1347
1384
|
```
|
|
1385
|
+
No target required. (A tier-3 `body-template-detected` advisory fires on a static template with no `${...}` interpolations and no text-consuming `# Output:` — a gentle "is this output, or did you mean a `#` comment?" intent check; harmless on a genuinely-static line.)
|
|
1348
1386
|
|
|
1349
|
-
|
|
1387
|
+
## `# Output:` delivery kinds
|
|
1350
1388
|
|
|
1351
|
-
|
|
1389
|
+
The body template (or, absent one, the emit/`-> VAR` result) is delivered according to the `# Output:` kind:
|
|
1352
1390
|
|
|
1353
|
-
### `
|
|
1391
|
+
### `text` (default, bare-only)
|
|
1392
|
+
Returns the result as a string to whatever invoked the skill via API or read the compiled artifact. Bare-only — a target is a parse error.
|
|
1393
|
+
```
|
|
1394
|
+
# Output: text
|
|
1395
|
+
```
|
|
1354
1396
|
|
|
1355
|
-
|
|
1397
|
+
### `agent: <agent-name>` — augmenting context to a named agent
|
|
1398
|
+
The output prepends to the named agent's next-turn prompt context as augment-kind payload — bringing an agent into the next turn pre-shaped. Wired via the runtime host's prompt-prepend surface + a synchronous trigger-fire endpoint with timeout-fallback so next-turn dispatch isn't blocked on slow execution.
|
|
1399
|
+
```
|
|
1400
|
+
# Output: agent: <agent-name>
|
|
1401
|
+
```
|
|
1356
1402
|
|
|
1403
|
+
### `template: <agent-name>` — playbook delivered to a named agent
|
|
1404
|
+
The output renders as a playbook the named agent executes itself — the runtime hands over a recipe rather than dispatching the ops.
|
|
1357
1405
|
```
|
|
1358
1406
|
# Output: template: <agent-name>
|
|
1359
|
-
# Output: template: cc@browser-tab-3
|
|
1360
1407
|
```
|
|
1361
1408
|
|
|
1362
|
-
Used for reusable recipes: a skill that, when compiled, produces instructions another agent follows.
|
|
1363
|
-
|
|
1364
|
-
**Address-routing**: same as `agent:` above. Bare → deliver(); `@session` → wake().
|
|
1365
|
-
|
|
1366
1409
|
### `file: <path>` — write to file
|
|
1367
|
-
|
|
1368
|
-
Header parses; file router not yet implemented. See "Not yet implemented, but planned" at top.
|
|
1410
|
+
Header parses; file router not yet implemented. See "Not yet implemented, but planned."
|
|
1369
1411
|
|
|
1370
1412
|
### `none` (bare-only)
|
|
1371
|
-
|
|
1372
|
-
Side-effects only — the skill's purpose is the writes / shell ops it performs, not the returned value. Bare-only; parse error if a target is supplied.
|
|
1373
|
-
|
|
1413
|
+
Side-effects only — the skill's purpose is its writes / shell ops, not a returned value. Bare-only.
|
|
1374
1414
|
```
|
|
1375
1415
|
# Output: none
|
|
1376
1416
|
```
|
|
1377
1417
|
|
|
1378
1418
|
## Multiple output targets
|
|
1379
|
-
|
|
1380
|
-
A skill may declare multiple output targets, one per line. Each target receives the same content.
|
|
1381
|
-
|
|
1419
|
+
A skill may declare multiple output targets, one per line; each receives the same content.
|
|
1382
1420
|
```
|
|
1383
1421
|
# Output: agent: ops-channel
|
|
1384
1422
|
# Output: agent: assistant
|
|
1385
1423
|
```
|
|
1386
1424
|
|
|
1387
|
-
A morning-brief skill, for example, can deliver to a team-channel agent and to an assistant agent's session-start prompt context simultaneously.
|
|
1388
|
-
|
|
1389
|
-
## Skill categories — Augmenting / Template / Headless
|
|
1390
|
-
|
|
1391
|
-
The output kind declaration determines the skill's category for discovery purposes (see SkillStore `skill_list` discovery surface):
|
|
1392
|
-
|
|
1393
|
-
| Category | Determined by | Discovery group |
|
|
1394
|
-
|---|---|---|
|
|
1395
|
-
| **Augmenting** | Has `# Output: agent: <name>` declared | `receives` |
|
|
1396
|
-
| **Template** | Has `# Output: template: <name>` declared OR no agent/template output but agent-invokable (no triggers) | `skills` |
|
|
1397
|
-
| **Headless** | Output is `text` / `file:` / `none` AND has autonomous triggers | `headless` (filtered out of default agent discovery) |
|
|
1398
|
-
|
|
1399
|
-
The derivation: ANY `output.kind === "agent"` → Augmenting; else ANY `output.kind === "template"` → Template; else if no autonomous triggers → Template (agent-invokable inference); else → Headless.
|
|
1400
|
-
|
|
1401
|
-
Agent discovery via `skill_list()` defaults to `receives` + `skills` groups. Headless skills are filtered out of the default view (admin views can opt in via `filter: { audience: "all" }`).
|
|
1402
|
-
|
|
1403
1425
|
## Per-kind output value semantics
|
|
1404
1426
|
|
|
1405
|
-
|
|
1427
|
+
What each kind consumes as the output, in precedence order:
|
|
1428
|
+
- **Presentation surfaces** (`agent:`, `template:`): the body template if present; otherwise joined emissions (all `emit()` ops concatenated in execution order).
|
|
1429
|
+
- **Programmatic surfaces** (`text`, `file:`): the body template if present; otherwise the `lastBoundVar` (most recent `-> VAR`).
|
|
1406
1430
|
|
|
1407
|
-
|
|
1408
|
-
- **Programmatic surfaces** (`text`, `file:`) consume the `lastBoundVar` — the most recently bound `-> VAR` value from any op
|
|
1431
|
+
A present body template is the canonical output for every kind; `emit` output continues to populate the transcript independently. Single source of truth in the executor's `perKindOutput()`; routers stay dumb. (Note: the `lastBoundVar` fallback is forgiving — a skill that binds a value but writes no template still outputs that last value — but it *masks* a forgotten template; prefer an explicit template when the output is fixed-shape.)
|
|
1409
1432
|
|
|
1410
|
-
|
|
1433
|
+
## Skill categories — Augmenting / Template / Headless
|
|
1411
1434
|
|
|
1412
|
-
|
|
1435
|
+
The output kind determines the skill's discovery category (see `skill_list`):
|
|
1413
1436
|
|
|
1414
|
-
|
|
1437
|
+
| Category | Determined by | Discovery group |
|
|
1438
|
+
|---|---|---|
|
|
1439
|
+
| **Augmenting** | Has `# Output: agent: <name>` | `receives` |
|
|
1440
|
+
| **Template** | Has `# Output: template: <name>`, OR no agent/template output but agent-invokable (no triggers) | `skills` |
|
|
1441
|
+
| **Headless** | Output is `text` / `file:` / `none` AND has autonomous triggers | `headless` (filtered from default agent discovery) |
|
|
1415
1442
|
|
|
1416
|
-
|
|
1443
|
+
Derivation: ANY `output.kind === "agent"` → Augmenting; else ANY `template` → Template; else no autonomous triggers → Template; else → Headless. `skill_list()` defaults to `receives` + `skills`; headless is opt-in via `filter:{audience:"all"}`.
|
|
1417
1444
|
|
|
1418
|
-
|
|
1445
|
+
## Augmenting / Template companion header — `# Event-type:`
|
|
1419
1446
|
|
|
1447
|
+
Skills with `agent:`/`template:` output may declare a routing tag that rides with the delivery payload (no effect on Headless):
|
|
1420
1448
|
```
|
|
1421
1449
|
# Output: agent: perry
|
|
1422
1450
|
# Event-type: ticket-911
|
|
1423
1451
|
```
|
|
1424
|
-
|
|
1425
|
-
The receiving agent reads `event_type` for routing decisions ("this is a 911 — surface immediately" vs "this is a routine check — fold into next brief").
|
|
1426
|
-
|
|
1427
|
-
### Lint coverage
|
|
1428
|
-
|
|
1429
|
-
A tier-2 lint rule `unused-augmenting-header` fires when `# Event-type:` appears on a Headless skill (no `agent:` or `template:` output declared). Headless skills have no AgentConnector dispatch path, so the header would silently no-op — the lint warns the author to either change the output kind or remove the header.
|
|
1430
|
-
|
|
1431
|
-
Legacy `# Delivery-context:` header was renamed to `# Event-type:` for vocab consistency. A tier-2 advisory `legacy-frontmatter-header` fires on the legacy form with a rename suggestion.
|
|
1452
|
+
The receiving agent reads `event_type` for routing ("911 — surface now" vs "routine — fold into next brief"). Flows to `DeliveryMeta.event_type`; `notify(event_type=...)` takes precedence per-emit. A tier-2 lint `unused-augmenting-header` fires when `# Event-type:` appears on a Headless skill. (Legacy `# Delivery-context:` → renamed `# Event-type:`; tier-2 `legacy-frontmatter-header` advisory on the old form.)
|
|
1432
1453
|
|
|
1433
1454
|
## Grammar
|
|
1434
1455
|
|
|
1435
|
-
-
|
|
1436
|
-
-
|
|
1437
|
-
-
|
|
1456
|
+
- Bare-only kinds (`text`, `none`): `# Output: text` valid; `# Output: text: anything` is a parse error.
|
|
1457
|
+
- Targeted kinds (`agent`, `template`, `file`): require `<kind>: <target>`; bare form is a parse error.
|
|
1458
|
+
- **Template/target boundary (position-free):** a **target** is a `<name>:` line followed by an indented op-block. **Any other line** — including a `word: value` line with no op-block beneath it (`Summary: hot today`) — is **template text**, wherever it sits (top, bottom, or between targets). So the output template can go where the author's instinct puts it. Declaring **two separate template regions** in one skill is an error — pick one location.
|
|
1459
|
+
- Parse errors on bare-only kinds suggest the corrected shape inline.
|
|
1438
1460
|
|
|
1439
1461
|
## Output routing failures
|
|
1440
1462
|
|
|
1441
|
-
If `# Output: agent: <name>` fires and the wired AgentConnector throws,
|
|
1463
|
+
If `# Output: agent: <name>` fires and the wired AgentConnector throws, delivery routes through `else:` / `# OnError:`; the failure is recorded on `agent_delivery_receipts[]` for the scheduler to log.
|
|
1442
1464
|
|
|
1443
1465
|
## Lifecycle and status — # Status: header, six canonical states, compile + runtime enforcement
|
|
1444
1466
|
|
|
@@ -1966,16 +1988,18 @@ Skill discovery via `skill_list()` (see SkillStore docs) closes the related visi
|
|
|
1966
1988
|
|
|
1967
1989
|
Design rationale for planned features. The user-facing list of "what's coming" lives in the "Not yet implemented, but planned" section at top; this section documents the *why* behind each planned addition. When the language extends, the relevant grammar moves into its canonical section (Ops reference, Variables, Triggers, etc.) and the design-rationale entry here is replaced with a cross-reference.
|
|
1968
1990
|
|
|
1969
|
-
## Sensors as a
|
|
1991
|
+
## Sensors as a read-channel category
|
|
1970
1992
|
|
|
1971
|
-
|
|
1993
|
+
> `sensor:` is **not a trigger source** — to *fire* a skill on a sensor signal, a sensor adapter POSTs to the `/event` ingress like any other external source (see Triggers). What remains a future idea is the separate concept below: sensors as a continuous **read channel**, decoupled from triggering.
|
|
1994
|
+
|
|
1995
|
+
The read-channel idea: a skill declares sensors it reads, distinct from what fires it:
|
|
1972
1996
|
|
|
1973
1997
|
```
|
|
1974
1998
|
# Sensors: presence, screen-state, voice-prosody
|
|
1975
1999
|
# Triggers: cron: 0 8 * * *
|
|
1976
2000
|
```
|
|
1977
2001
|
|
|
1978
|
-
**Distinction:** Sensors are continuous
|
|
2002
|
+
**Distinction:** Sensors (read-channel) are continuous values the agent reads but doesn't emit on. Triggers are discrete signals that fire the skill. The two are different semantics — sensors need continuous read, ambient-ref access, and privacy gating; triggers need discrete fire + dispatch. (Conflating them in one `sensor:` trigger source was the old design; the trigger half folds into `event`, the read-channel half stays future.)
|
|
1979
2003
|
|
|
1980
2004
|
Pending: ambient refs for sensor values (`${SENSOR.presence}`, `${SENSOR.voice-prosody.affect}`) and the privacy-gating discipline that determines when a sensor is readable.
|
|
1981
2005
|
|
|
@@ -1999,7 +2023,7 @@ Different shape from event triggers — "fire if user hasn't messaged in N minut
|
|
|
1999
2023
|
# Triggers: idle: 5m
|
|
2000
2024
|
```
|
|
2001
2025
|
|
|
2002
|
-
Runtime tracks the relevant idleness counter and fires when the threshold crosses. Separate dispatch mechanism from event triggers.
|
|
2026
|
+
Runtime tracks the relevant idleness counter and fires when the threshold crosses. Would be a **new trigger source beyond the current `cron`|`event`** — or, absent a new source, an adapter that POSTs to `/event` on a timer. Separate dispatch mechanism from event triggers either way.
|
|
2003
2027
|
|
|
2004
2028
|
## Time-windowed aggregation
|
|
2005
2029
|
|
|
@@ -2132,7 +2156,7 @@ Runtime fails-fast on missing capabilities. Trust precondition for sensor work
|
|
|
2132
2156
|
## Build order rationale
|
|
2133
2157
|
|
|
2134
2158
|
Some features depend on others:
|
|
2135
|
-
- Suppression + persistent state should land before
|
|
2159
|
+
- Suppression + persistent state should land before the sensor read-channel (sensor work would compound problems without them)
|
|
2136
2160
|
- Pub-sub needs sensors producing traffic before it has anything to route
|
|
2137
2161
|
- Introspection is ergonomic, not foundational — useful but skippable
|
|
2138
2162
|
- Capability declarations are the trust gate that makes sensor + privacy work socially defensible
|
|
@@ -2158,33 +2182,31 @@ Should `${ERROR}` be an ambient ref inside `else:` blocks, populated with the er
|
|
|
2158
2182
|
|
|
2159
2183
|
## 3. Multiple triggers — concurrency
|
|
2160
2184
|
|
|
2161
|
-
If `cron: 0 8 * * *` and `event: user
|
|
2185
|
+
If `cron: 0 8 * * *` and `event: user-present` both fire within seconds, does the skill run twice (independent) or get deduped? Lean: independent. Author dedups via state if needed. Affects dispatch layer.
|
|
2162
2186
|
|
|
2163
2187
|
## 4. `execute_skill()` invocation vs trigger firing
|
|
2164
2188
|
|
|
2165
2189
|
When skill A invokes skill B via `execute_skill()`, do skill B's `# Triggers:` fire? Almost certainly no — `execute_skill()` is direct invocation, distinct from the trigger event surface. Worth saying explicitly.
|
|
2166
2190
|
|
|
2167
|
-
## 5.
|
|
2168
|
-
|
|
2169
|
-
Recursive or directory-only by default? Inotify supports both. Lean: directory-only default; offer recursive via `file-watch-recursive:` or `file-watch: <path> (recursive)`. Affects dispatch layer when the file-watch trigger source actually fires (currently parse-only).
|
|
2170
|
-
|
|
2171
|
-
## 6. Output target delivery failures
|
|
2191
|
+
## 5. Output target delivery failures
|
|
2172
2192
|
|
|
2173
2193
|
If a delivery target is unreachable when the skill fires, what happens? Lean: delivery failure is its own retryable error; queue if possible, else error to caller. Worth a separate small spec section. Affects dispatch layer.
|
|
2174
2194
|
|
|
2175
|
-
##
|
|
2195
|
+
## 6. Skill versioning rollback UX
|
|
2176
2196
|
|
|
2177
2197
|
Edits via upsert preserve history through substrate versioning (SqliteSkillStore's `skill_versions` table, FilesystemSkillStore's git history), but no first-class "rollback" affordance. Probably needs a `--version <N>` flag on the compile API or a sister tool.
|
|
2178
2198
|
|
|
2179
|
-
##
|
|
2199
|
+
## 7. Connector capability declarations
|
|
2180
2200
|
|
|
2181
2201
|
Skills can declare required connector capabilities via `# Requires:` for var resolution. Extending this to "needs semantic search" / "needs structured-extraction model with 32K context" capabilities would be useful for the substrate-portable story. Pending design.
|
|
2182
2202
|
|
|
2183
|
-
##
|
|
2203
|
+
## 8. Per-op timeouts
|
|
2204
|
+
|
|
2205
|
+
Hung dispatches hang the skill without explicit timeout configuration. Lean: skill-level `# Timeout:` header + per-op `timeout=N` kwarg + runtime defaults. Pending implementation; see "Not yet implemented, but planned" at top.
|
|
2184
2206
|
|
|
2185
|
-
|
|
2207
|
+
*(The former "file-watch path semantics" question was removed — `file-watch` is no longer a trigger source; filesystem watching is now an external adapter that POSTs to the `/event` ingress, so there are no in-language file-watch path semantics to settle.)*
|
|
2186
2208
|
|
|
2187
2209
|
---
|
|
2188
2210
|
|
|
2189
|
-
*Rendered from `skillscript/skillscript-language-reference` — 2026-06-
|
|
2190
|
-
*Source of truth: AMP (`amp_render_document("skillscript/skillscript-language-reference")`)*
|
|
2211
|
+
*Rendered from `skillscript/skillscript-language-reference` — 2026-06-08 17:41 EDT*
|
|
2212
|
+
*Source of truth: AMP (`amp_render_document("skillscript/skillscript-language-reference")`)*
|