skillscript-runtime 0.19.12 → 0.19.14
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/dist/help-content.d.ts.map +1 -1
- package/dist/help-content.js +9 -0
- package/dist/help-content.js.map +1 -1
- package/dist/parser.d.ts +0 -1
- package/dist/parser.d.ts.map +1 -1
- package/dist/parser.js +0 -4
- package/dist/parser.js.map +1 -1
- package/docs/adopter-playbook.md +100 -45
- package/docs/language-reference.md +341 -23
- package/examples/skillscripts/classify-support-ticket.skill.md +4 -4
- package/examples/skillscripts/feedback-sentiment-scan.skill.md +9 -20
- package/examples/skillscripts/hello-world.skill.provenance.json +1 -1
- package/examples/skillscripts/morning-brief.skill.md +4 -4
- package/examples/skillscripts/queue-length-monitor.skill.md +2 -2
- package/examples/skillscripts/service-health-watch.skill.md +2 -2
- package/package.json +1 -1
|
@@ -379,7 +379,7 @@ Closed list of language-intrinsic ops the runtime knows directly. Each is a func
|
|
|
379
379
|
| `notify` | `notify(agent="...", message="...", [event_type=...], [correlation_id=...]) -> ACK` | optional | Mid-skill agent alert; synchronous send via configured AgentConnector. |
|
|
380
380
|
| `inline` | `inline(skill="<data-skill-name>")` | none | Compile-time inline of an Approved `# Type: data` skill. Resolves at compile, records `content_hash` in provenance. |
|
|
381
381
|
| `execute_skill` | `execute_skill(skill_name="...", inputs={...}) -> R` | optional | Composition primitive. Runtime-resolved. See Composition section. |
|
|
382
|
-
| `shell` | `shell(command="...") -> R` / `shell(command="...", unsafe=true) -> R` | optional |
|
|
382
|
+
| `shell` | `shell(command="...") -> R` / `shell(argv=[...]) -> R` / `shell(command="...", unsafe=true) -> R` | optional | Structural spawn (default), explicit-argv spawn (`argv=[...]`, no tokenizer), or full-shell exec (`unsafe=true`, gated by `runtime.enable_unsafe_shell`). Binary gated by the operator allowlist (see below). stdout binds. |
|
|
383
383
|
| `file_read` | `file_read(path="...") -> R` | required | Read a file at `path`; binds string contents. |
|
|
384
384
|
| `file_write` | `file_write(path="...", content="...")` | none | Write `content` to `path`. `mkdir -p` semantics for parent directories. Mutation-classified. |
|
|
385
385
|
|
|
@@ -431,18 +431,25 @@ Synchronous alert to a named agent via wired AgentConnector(s). **Contrast with
|
|
|
431
431
|
|
|
432
432
|
Returns ACK `{agent, dispatched: [{connector, ok, error?}]}` — fire-and-forget callers ignore the binding; check-delivery callers inspect ACK.
|
|
433
433
|
|
|
434
|
-
### `shell` —
|
|
434
|
+
### `shell` — structural, explicit-argv, or unsafe exec
|
|
435
435
|
|
|
436
|
-
|
|
436
|
+
Three forms.
|
|
437
|
+
|
|
438
|
+
**1. `shell(command="...")` — structural spawn (default).** The command string is whitespace-tokenized and quote-stripped, then one binary is spawned with the resulting tokens. No shell, no metacharacters, no pipes/redirects. The tokenizer is **quote-aware**: quotes are respected during the whitespace split, so a literal `'hello world'` stays one token (the surrounding quotes are then stripped). stdout binds; non-zero exit → op-error routed through `else:` / `# OnError:`.
|
|
437
439
|
|
|
438
440
|
```
|
|
439
441
|
shell(command="curl -s 'wttr.in/${LOCATION|url}?format=j1'") -> RAW
|
|
440
442
|
shell(command="git status") -> STATUS
|
|
441
443
|
```
|
|
442
444
|
|
|
443
|
-
|
|
445
|
+
**2. `shell(argv=["bin","arg1","${VAR}",...])` — explicit-argv spawn.** Each list element is exactly one argv token; `${VAR}` substitutes per element and the result is **not re-split**, so an arg containing whitespace, quote characters, JSON, or any dynamic content stays one intact arg. No tokenizer, no quote-matching, no shell — strictly safer than `unsafe=true` (injection-surface zero). This is the right form whenever an arg may contain dynamic or whitespace-bearing content. **Mutex:** `argv=` does not compose with `command=` or `unsafe=true` — it's an execv-class spawn, there is no shell to opt into.
|
|
446
|
+
|
|
447
|
+
```
|
|
448
|
+
shell(argv=["say","-v","${VOICE}","-f","${PATH}"]) -> OUT
|
|
449
|
+
shell(argv=["jq","-c","${FILTER}","/tmp/data.json"]) -> RESULT
|
|
450
|
+
```
|
|
444
451
|
|
|
445
|
-
**
|
|
452
|
+
**3. `shell(command="...", unsafe=true)` — full bash.** Required for pipes, redirects, shell built-ins.
|
|
446
453
|
|
|
447
454
|
```
|
|
448
455
|
shell(command="for i in $(seq 1 10); do echo $i; done", unsafe=true) -> R
|
|
@@ -452,11 +459,26 @@ shell(command="curl -s example.com | jq '.field' > /tmp/out", unsafe=true)
|
|
|
452
459
|
- Lint flags every `unsafe=true` call as tier-2.
|
|
453
460
|
- Runtime refuses with `UnsafeShellDisabledError` unless deployment sets `runtime.enable_unsafe_shell = true` (default `false`). Compile-time `unsafe-shell-disabled` tier-1 catches at authoring.
|
|
454
461
|
- Audit-visible at every fire.
|
|
462
|
+
- Bash's `$(command)` and arithmetic `$((expr))` pass through to bash without escape because skillscript's substitution is braced (`${VAR}`).
|
|
455
463
|
|
|
456
|
-
|
|
464
|
+
**TWO security gates, not one — structural shape AND a binary allowlist.** The structural constraint (above) governs *how* a command runs (no shell/metacharacters on the safe path). It is HALF the model. The other half is an **operator-owned binary allowlist (default-deny)** governing *which* binary may run at all:
|
|
465
|
+
|
|
466
|
+
- Every shell op's `argv[0]` (or the first token of `command=`) is checked against the allowlist; a non-allowlisted binary is refused with `ShellBinaryNotAllowedError`, regardless of safe-vs-`unsafe`.
|
|
467
|
+
- **Default is deny-all** — if no allowlist is wired, *every* shell op is refused.
|
|
468
|
+
- Configure via `SKILLSCRIPT_SHELL_ALLOWLIST` env (comma-separated), the `shellAllowlist` field in `skillscript.config.json`, or `bootstrap({ shellAllowlist: [...] })`. The runtime must **restart** to pick up changes. `skillfile shell-audit` scans the corpus and prints the binary union ready to paste.
|
|
469
|
+
- For `unsafe=true`, `bash` itself must be on the allowlist (binary-scope is independent of unsafe-vs-safe).
|
|
470
|
+
- The current allowlist is reported by `runtime_capabilities.shellExecution.allowlist`.
|
|
471
|
+
- **This is an operator boundary the skill author cannot escape** — not via `unsafe`, not via any in-skill mechanism. It scopes *which binary*, not *what the binary does*: allowlisting a powerful authenticated CLI (e.g. `gh`) grants its whole surface, so wrap powerful binaries to least-privilege (a read-only wrapper script on the allowlist instead of the raw binary) before allowlisting.
|
|
472
|
+
|
|
473
|
+
**All three forms honor the universal op-level kwargs below**, including `(fallback:)` — the fallback fires on op throw OR empty stdout.
|
|
457
474
|
|
|
458
475
|
**Pipes need unsafe; sandboxed multi-call + temp file is the unsafe-free alternative.** A pipe (`curl ... | jq ...`) is a shell metacharacter and requires `unsafe=true`. To compute-in-tools without unsafe, split into sequential sandboxed calls sharing a temp file: `shell(command="curl -s -o /tmp/x.json ...")` then `shell(command="jq -c '<filter>' /tmp/x.json") -> R`. Sequential sandboxed calls share the runtime's filesystem within an execution. Caveat: a fixed temp path races under concurrent invocation (no built-in uniquifier short of a `mktemp` call) — for a large fetched intermediate, keeping it in an execution-scoped var + filtering exports via `# Returns:` is usually cleaner than either shell form.
|
|
459
476
|
|
|
477
|
+
**The `'${VAR}'` quote trap — works in test, breaks on edge input.** Because the structural tokenizer is quote-aware, `shell(command="say -v Jamie '${TEXT}'")` DOES pass a simple multi-word value as one argument — it works for `TEXT="Perry here now"`. That is the trap: it's fragile on the *content* of the substituted value. If `TEXT` contains a quote character (`Jamie's turn`) the quote-matching drifts and the arg breaks — and it fails silently, only on certain inputs, after the pattern already "worked" in testing. Tier-2 lint `shell-quoted-var-in-command` flags the `'${VAR}'`-in-`command=` pattern for exactly this reason and points at `argv=[...]`. Three safe ways to pass an arg that may contain whitespace or quote characters:
|
|
478
|
+
- **`shell(argv=[...])` (preferred):** explicit-token list (form 2 above) — no tokenizer touches the value; the safest because there is no shell and no quote-matching at all.
|
|
479
|
+
- **File-roundtrip:** `file_write(path="/tmp/x.txt", content="${TEXT}")` then `shell(command="say -f /tmp/x.txt")` — the binary reads the value off disk via its own file-input flag; no tokenizer involved. Good when the binary has a file-input mode.
|
|
480
|
+
- **`unsafe=true` + `${TEXT|shell}`:** bash quoting via the POSIX-escape filter; tier-2 lint flags it; injection surface remains if `TEXT` is untrusted.
|
|
481
|
+
|
|
460
482
|
### `file_read` / `file_write` — file I/O
|
|
461
483
|
|
|
462
484
|
```
|
|
@@ -546,7 +568,7 @@ Typed-contract ops (`data_*`, `skill_*`, `json_parse`, `llm`) auto-wire from the
|
|
|
546
568
|
|
|
547
569
|
### Universal op-level kwargs
|
|
548
570
|
|
|
549
|
-
Four surfaces apply to every `$` dispatch regardless of tool. The runtime intercepts these before forwarding the remaining kwargs to the connector.
|
|
571
|
+
Four surfaces apply to every `$` dispatch and to `shell()` (notably `(fallback:)`), regardless of tool. The runtime intercepts these before forwarding the remaining kwargs to the connector.
|
|
550
572
|
|
|
551
573
|
**`timeout=N`** — Per-op timeout in **seconds**. Integer literal or `${VAR}` ref. Resolution chain (most-specific wins):
|
|
552
574
|
|
|
@@ -566,13 +588,14 @@ $ amp.amp_olsen_task task_type="scan" timeout=120 -> DISPATCH
|
|
|
566
588
|
$ data_write content="${REPORT}" tags=["oncall"] approved="morning roundup" -> ACK
|
|
567
589
|
```
|
|
568
590
|
|
|
569
|
-
**`(fallback: "value")` trailer** — Fires on dispatch throw OR empty bound value (empty string after trim, empty array, null/undefined). Coerce-on-bind: the fallback value binds to the output var transparently, downstream targets need no conditional. Permissive value parsing — bare identifiers, quoted strings, bracketed array literals all accepted.
|
|
591
|
+
**`(fallback: "value")` trailer** — Fires on dispatch throw OR empty bound value (empty string after trim, empty array, null/undefined). Honored on `$` dispatch and on `shell()` (fires on shell op throw or empty stdout). Coerce-on-bind: the fallback value binds to the output var transparently, downstream targets need no conditional. Permissive value parsing — bare identifiers, quoted strings, bracketed array literals all accepted. A fired fallback is recorded in `result.fallbacks[]`.
|
|
570
592
|
|
|
571
593
|
Note: an envelope object like `{items: []}` is a non-empty object and does NOT trigger the fallback even though its contained array is empty. To handle envelope-empty downstream, test the contained collection (`if ${R.items|length} == "0":`) or apply a filter (`${R.items|fallback:[]}`).
|
|
572
594
|
|
|
573
595
|
```
|
|
574
596
|
$ llm prompt="Classify: ${INPUT}" -> VERDICT (fallback: "unknown")
|
|
575
597
|
$ amp.amp_query_memories query="${TOPIC}" -> RESULTS (fallback: [])
|
|
598
|
+
shell(argv=["gh","pr","list","--repo","acme/foo"]) -> PRS (fallback: "No open PRs")
|
|
576
599
|
$ ticketing.search query="..." -> ISSUES (fallback: "search-unavailable")
|
|
577
600
|
```
|
|
578
601
|
|
|
@@ -657,7 +680,7 @@ Tool args are unconstrained `key=value` pairs — the connector forwards them to
|
|
|
657
680
|
|
|
658
681
|
See the adopter playbook for the substrate config reference + the full Case 2 tradeoff.
|
|
659
682
|
|
|
660
|
-
**Discovery surface.** `runtime_capabilities` (MCP tool) exposes the registered substrate state. Every entry across all four substrate slots (SkillStore / DataStore / LocalModel / McpConnector) carries its instance `manifest()` payload alongside the static features. Three observable states per entry: working `manifest:{...}`, runtime failure `manifest:null, manifest_error:"..."`, structural absence `manifest:null, manifest_unsupported:true` (AgentConnector only — the contract has no `manifest()` method). The bridge `wraps` convention re-exposes the underlying substrate's full manifest, so adopters reading the discovery surface see the full bound state without traversing multiple entries.
|
|
683
|
+
**Discovery surface.** `runtime_capabilities` (MCP tool) exposes the registered substrate state. Every entry across all four substrate slots (SkillStore / DataStore / LocalModel / McpConnector) carries its instance `manifest()` payload alongside the static features. Three observable states per entry: working `manifest:{...}`, runtime failure `manifest:null, manifest_error:"..."`, structural absence `manifest:null, manifest_unsupported:true` (AgentConnector only — the contract has no `manifest()` method). The bridge `wraps` convention re-exposes the underlying substrate's full manifest, so adopters reading the discovery surface see the full bound state without traversing multiple entries. The `shellExecution` entry reports the operator shell allowlist + mode.
|
|
661
684
|
|
|
662
685
|
Connector entries also surface `features` declarations (`supports_identity_propagation`, `supports_streaming_responses`, `supports_batch`). Capability flags that span multiple layers — `supports_identity_propagation` requires both the connector's ctx-honoring AND the substrate's per-identity scope honoring — gate via `RuntimeCapabilitiesConformance` auto-coverage: declaring a feature flag true requires the adopter to wire both Level-1 (substrate-independent: ctx reaches transport) and Level-2 (substrate-coupled: distinct identities yield distinct observable scopes) probes via `flagProbes`. Missing probes fail the gate before runtime accepts the flag as true. This is the structural close that prevents the discipline-only-contract pattern (capability claim without honoring impl) from recurring at the capability-flag surface.
|
|
663
686
|
|
|
@@ -667,7 +690,7 @@ Connector entries also surface `features` declarations (`supports_identity_propa
|
|
|
667
690
|
|
|
668
691
|
## Per-op gating
|
|
669
692
|
|
|
670
|
-
|
|
693
|
+
Two independent authorization layers govern a shell op: **(0) the binary allowlist** — can this binary run at all (operator-owned, default-deny; see the shell section above; refusal is `ShellBinaryNotAllowedError`) — and **(1) the mutation gate** below. The allowlist is checked first and the author cannot bypass it. Beyond shell, mutation ops require an authorization signal. The signal is per-op, not a mode binary.
|
|
671
694
|
|
|
672
695
|
**Mutation-classified ops:**
|
|
673
696
|
- `file_write(...)` (runtime-intrinsic)
|
|
@@ -678,7 +701,7 @@ Mutation ops require an authorization signal. The signal is per-op, not a mode b
|
|
|
678
701
|
|
|
679
702
|
**Read-only ops (always allowed, no authorization needed):**
|
|
680
703
|
- `file_read`, `emit`, `notify`, `inline`, `execute_skill`
|
|
681
|
-
- `shell(command=...)` with read-only verb
|
|
704
|
+
- `shell(command=...)` with read-only verb (still subject to the binary allowlist)
|
|
682
705
|
- `$ <connector>.<tool>` against tools declared `mutating: false` (or unspecified, default false for query-shaped tools)
|
|
683
706
|
- `$set`, `$append`
|
|
684
707
|
|
|
@@ -718,7 +741,7 @@ deliver:
|
|
|
718
741
|
| Runtime-intrinsic | `notify` | `notify(agent="...", [message=...], [event_type=...], [correlation_id=...]) -> ACK` | optional |
|
|
719
742
|
| Runtime-intrinsic | `inline` | `inline(skill="<name>")` | none (compile-time) |
|
|
720
743
|
| Runtime-intrinsic | `execute_skill` | `execute_skill(skill_name="...", inputs={...}) -> R` | optional |
|
|
721
|
-
| Runtime-intrinsic | `shell` | `shell(command="...", [unsafe=true], [approved="..."]) -> R` | optional |
|
|
744
|
+
| Runtime-intrinsic | `shell` | `shell(command="...", [unsafe=true], [approved="..."]) [-> R] [(fallback: "...")]` or `shell(argv=[...], [approved="..."]) [-> R] [(fallback: "...")]` (mutually exclusive forms; binary allowlist applies to both) | optional |
|
|
722
745
|
| Runtime-intrinsic | `file_read` | `file_read(path="...") -> R` | required |
|
|
723
746
|
| Runtime-intrinsic | `file_write` | `file_write(path="...", content="...", [approved="..."])` | none |
|
|
724
747
|
| External MCP — substrate-specific | `$ <connector>.<tool>` | `$ <connector>.<tool> kwarg=value, ... [timeout=N] [approved="..."] [-> R] [(fallback: "...")]` | optional |
|
|
@@ -863,7 +886,7 @@ Pipe filters apply transforms to resolved variables before substitution. Syntax:
|
|
|
863
886
|
| `trim` | Whitespace trim | `${VERDICT|trim}` for `"urgent\n"` | `urgent` |
|
|
864
887
|
| `length` | Count of items (array) or characters (string) | `${ITEMS|length}` for `["a","b","c"]` | `3` |
|
|
865
888
|
| `contains:"X"` | Boolean: type-aware substring / element membership | `${MSG|contains:"urgent"}` for `"Yes, urgent"` | `true` |
|
|
866
|
-
| `fallback:"X"` | Coalesce on missing/undefined ref | `${VAR.missing|fallback:"-"}` | `-` |
|
|
889
|
+
| `fallback:"X"` | Coalesce on missing/undefined/empty ref | `${VAR.missing|fallback:"-"}` | `-` |
|
|
867
890
|
| `isodate` | Epoch seconds → ISO-8601 timestamp | `${EPOCH|isodate}` for `1779660000` | `2026-05-24T22:00:00.000Z` |
|
|
868
891
|
|
|
869
892
|
### `length` semantics
|
|
@@ -915,16 +938,17 @@ Empty-string match: `${VAR|contains:""}` returns `true` if VAR is bound. Documen
|
|
|
915
938
|
|
|
916
939
|
### `fallback:"X"` semantics
|
|
917
940
|
|
|
918
|
-
Coalesce-on-missing. Emits the literal string `X` when the ref resolves to missing/null/undefined. Strict-by-default semantics preserved everywhere else; `|fallback:` is the explicit opt-out at the call site.
|
|
941
|
+
Coalesce-on-empty-or-missing. Emits the literal string `X` when the ref resolves to missing/null/undefined OR to an empty string. Strict-by-default semantics preserved everywhere else; `|fallback:` is the explicit opt-out at the call site.
|
|
919
942
|
|
|
920
943
|
```
|
|
921
944
|
emit:
|
|
922
|
-
emit(text="present: ${PRESENT|fallback:\"missing\"}") # → "hello" (PRESENT is bound)
|
|
923
|
-
emit(text="missing: ${NOT_DECLARED|fallback:\"-\"}") # → "-" (NOT_DECLARED isn't)
|
|
945
|
+
emit(text="present: ${PRESENT|fallback:\"missing\"}") # → "hello" (PRESENT is bound and non-empty)
|
|
946
|
+
emit(text="missing: ${NOT_DECLARED|fallback:\"-\"}") # → "-" (NOT_DECLARED isn't declared)
|
|
947
|
+
emit(text="empty: ${BOUND_EMPTY|fallback:\"-\"}") # → "-" (bound to "")
|
|
924
948
|
emit(text="nested: ${ISSUE.customFields.Assignee|fallback:\"unassigned\"}")
|
|
925
949
|
```
|
|
926
950
|
|
|
927
|
-
**Why filter-shape, not ref-level `(fallback:)`.** Op-level `(fallback: ...)` exists on `$` dispatch for **error recovery** (
|
|
951
|
+
**Why filter-shape, not ref-level `(fallback:)`.** Op-level `(fallback: ...)` exists on `$` dispatch and `shell()` for **error/empty recovery** (the op ran, then failed or returned empty). Ref-level `|fallback:` is **coalesce** (the lookup itself found nothing — missing, null, or empty). They rhyme but are adjacent concepts. The filter-chain attachment keeps composition clean (`${VAR|json_parse|fallback:"-"}` works as a chain step) and the vocabulary alignment with op-level `(fallback:)` lets cold authors learn "fallback" as the universal concept while the syntax disambiguates the attachment site.
|
|
928
952
|
|
|
929
953
|
**Closes the missing-field strict-error trap**: `${ISSUE.customFields.Assignee}` against an object without that key threw `UnresolvedVariableError` and aborted whole-render. The filter is the per-ref opt-out.
|
|
930
954
|
|
|
@@ -1422,6 +1446,15 @@ A skill may declare multiple output targets, one per line; each receives the sam
|
|
|
1422
1446
|
# Output: agent: assistant
|
|
1423
1447
|
```
|
|
1424
1448
|
|
|
1449
|
+
## Output target resolution
|
|
1450
|
+
|
|
1451
|
+
For the `agent:` and `template:` kinds, the runtime resolves the target agent_id via a **2-level chain** (first match wins):
|
|
1452
|
+
|
|
1453
|
+
1. **Explicit name** — `# Output: agent: perry` delivers to agent_id `perry`.
|
|
1454
|
+
2. **`${VAR}` compile-time substitution** — `# Output: agent: ${RECIPIENT}` resolves against the resolved inputs map (`# Vars:` defaults, `# Requires:` cascade, caller-supplied `inputs`) at compile time. Only compile-time inputs resolve here — a runtime-bound ref (an op's `-> VAR` output, an ambient ref) passes through verbatim and fails at delivery if still unresolved.
|
|
1455
|
+
|
|
1456
|
+
There is **no** invocation-context inheritance and **no** runtime `default_agent_id` fallback: a skill must name its target explicitly or pass it as an input var. See the Connectors section (AgentConnector) for the full delivery contract behind the resolved id.
|
|
1457
|
+
|
|
1425
1458
|
## Per-kind output value semantics
|
|
1426
1459
|
|
|
1427
1460
|
What each kind consumes as the output, in precedence order:
|
|
@@ -1462,6 +1495,290 @@ The receiving agent reads `event_type` for routing ("911 — surface now" vs "ro
|
|
|
1462
1495
|
|
|
1463
1496
|
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.
|
|
1464
1497
|
|
|
1498
|
+
## Connectors — substrate routing, the five connector types, agent_id resolution
|
|
1499
|
+
|
|
1500
|
+
The substrate-routing ops (`$ connector.tool`, `$ data_read`, `$ llm`) and the agent-bound `# Output:` kinds (`agent:`, `template:`) don't call any specific backend directly. They route through thin connector interfaces. Skill source persistence follows the same pattern via a dedicated contract. This is the programmable surface through which authors compose information topology per skill and per moment. Skills are portable across substrates because the language doesn't bake substrate identity into the source.
|
|
1501
|
+
|
|
1502
|
+
## Five connector types
|
|
1503
|
+
|
|
1504
|
+
### MemoryStore
|
|
1505
|
+
|
|
1506
|
+
Routes `$ data_read` retrieval ops. Interface: `MemoryStore.query(filters) → PortableMemory[]`.
|
|
1507
|
+
|
|
1508
|
+
Implementations vary by deployment — a knowledge-substrate-backed store, a SQLite-backed store, a vector-DB-backed store, an in-memory test store. All conform to the `MemoryStore.query` contract and return `PortableMemory[]`.
|
|
1509
|
+
|
|
1510
|
+
### LocalModel
|
|
1511
|
+
|
|
1512
|
+
Routes `$ llm` local-model ops. Interface: `LocalModel.run(prompt, opts) → string`.
|
|
1513
|
+
|
|
1514
|
+
Default impl wraps a local-model HTTP service (e.g., Ollama). Constructor takes `{ model: string }` (required) — no class-level implicit default. Multiple instances by name in the registry; each backed by a distinct model tag.
|
|
1515
|
+
|
|
1516
|
+
### McpConnector
|
|
1517
|
+
|
|
1518
|
+
Routes `$ connector.tool` MCP-tool ops. Interface: `McpConnector.call(toolName, args, ctxOverrides?) → unknown`.
|
|
1519
|
+
|
|
1520
|
+
Implementations include adapters wrapping in-process tool dispatch (when the runtime is embedded in a host that already has MCP tools) and HTTP-based MCP clients (when calling out to remote MCP servers). All conform to the `McpConnector.call` contract.
|
|
1521
|
+
|
|
1522
|
+
**Bare-name dispatch.** `$ TOOL` (no connector prefix) routes through the `primary` McpConnector entry in the registry. If no `primary` is wired, the runtime throws `ConnectorNotFoundError` at runtime AND fires tier-1 `unwired-primary-connector` lint at compile time. The remediation diagnostic includes both fix paths: add `primary` to connectors.json, or qualify the op as `$ named.TOOL`. Failing loud is the correct mode for autonomous-pattern skills — a silent stub would let an autonomous skill appear to succeed while doing nothing.
|
|
1523
|
+
|
|
1524
|
+
### AgentConnector
|
|
1525
|
+
|
|
1526
|
+
Routes the agent-bound `# Output:` kinds — `agent:` (Augmenting) and `template:` (Template) per the skill-kind taxonomy in Section 1. Interface:
|
|
1527
|
+
|
|
1528
|
+
```typescript
|
|
1529
|
+
interface AgentConnector {
|
|
1530
|
+
list_agents(): Promise<AgentDescriptor[]>;
|
|
1531
|
+
deliver(agent_id: string, payload: DeliveryPayload): Promise<DeliveryReceipt>;
|
|
1532
|
+
wake(agent_id: string, opts?: WakeOpts): Promise<WakeReceipt>;
|
|
1533
|
+
agent_status?(agent_id: string): Promise<AgentStatus>;
|
|
1534
|
+
}
|
|
1535
|
+
|
|
1536
|
+
type DeliveryPayload =
|
|
1537
|
+
| { kind: "augment"; content: string; format?: "text" | "markdown"; source_skill?: string; triggered_by?: TriggerProvenance; delivery_context?: string; templates?: string[] }
|
|
1538
|
+
| { kind: "template"; prompt: string; source_skill?: string; triggered_by?: TriggerProvenance; delivery_context?: string; templates?: string[] };
|
|
1539
|
+
|
|
1540
|
+
type TriggerProvenance = {
|
|
1541
|
+
source: "cron" | "session" | "event" | "agent-event" | "file-watch" | "sensor" | "manual";
|
|
1542
|
+
name: string; // e.g. "0 8 * * *", "session:start", "manual"
|
|
1543
|
+
fired_at_ms: number; // unix ms timestamp
|
|
1544
|
+
};
|
|
1545
|
+
|
|
1546
|
+
type DeliveryReceipt = { delivered_at: number; delivery_id?: string };
|
|
1547
|
+
|
|
1548
|
+
type WakeOpts = {
|
|
1549
|
+
context?: string;
|
|
1550
|
+
when?: "immediate" | number;
|
|
1551
|
+
};
|
|
1552
|
+
|
|
1553
|
+
type WakeReceipt = { woken_at: number; session_id?: string };
|
|
1554
|
+
|
|
1555
|
+
type AgentDescriptor = {
|
|
1556
|
+
agent_id: string;
|
|
1557
|
+
agent_name?: string;
|
|
1558
|
+
capabilities?: ("deliver" | "wake" | "augment" | "template")[];
|
|
1559
|
+
};
|
|
1560
|
+
|
|
1561
|
+
type AgentStatus = "active" | "idle" | "asleep" | "unknown";
|
|
1562
|
+
```
|
|
1563
|
+
|
|
1564
|
+
The `agent:` Output kind produces a `DeliveryPayload` of kind `augment`; `template:` produces one of kind `template`.
|
|
1565
|
+
|
|
1566
|
+
Two primary verbs (`deliver` + `wake`), one mandatory discovery method (`list_agents`), one optional status method. The contract is substrate-neutral; adopters wire any delivery mechanism behind it:
|
|
1567
|
+
|
|
1568
|
+
| Substrate | `deliver` impl | `wake` impl |
|
|
1569
|
+
|---|---|---|
|
|
1570
|
+
| tmux session | `tmux send-keys` to a pane | `tmux send-keys` with wake prompt |
|
|
1571
|
+
| webhook | POST to `/augment` or `/template` endpoint | POST to `/wake` endpoint |
|
|
1572
|
+
| memory store | write a memory record with delivery tag | write addressed memory + push notification |
|
|
1573
|
+
| file-watch | write to `<path>/augment-<id>.txt` | write to `<path>/wake-<id>.txt` |
|
|
1574
|
+
| chat thread | post to monitored thread | post + @mention |
|
|
1575
|
+
| IPC named pipe | write to delivery pipe | write to wake pipe |
|
|
1576
|
+
|
|
1577
|
+
Default impl `NoOpAgentConnector` logs warnings and resolves; lets the runtime ship without an agent-delivery substrate wired. Adopter impls run the bundled `AgentConnectorConformance` suite to verify their substrate wiring.
|
|
1578
|
+
|
|
1579
|
+
#### DeliveryPayload provenance fields
|
|
1580
|
+
|
|
1581
|
+
Every `deliver` call carries optional provenance the receiving agent uses to disambiguate the source and context of the delivery:
|
|
1582
|
+
|
|
1583
|
+
- `source_skill?: string` — name of the skill that produced this delivery. Lets the receiver attribute the content to a specific authored skill, distinguishing "this is from the stock-monitor skill" from "this is from the news-brief skill."
|
|
1584
|
+
- `triggered_by?: TriggerProvenance` — why the skill fired. Receiver disambiguates cron tick (autonomous), session-start (lifecycle), event-driven (external signal), manual (user-requested), etc. Carries the trigger source + name + unix-ms timestamp.
|
|
1585
|
+
- `delivery_context?: string` — prose explanation of why the agent is being notified and what to do with the content. Populated from the `# Delivery-context:` header (see Section 7).
|
|
1586
|
+
- `templates?: string[]` — list of Template-kind skill names the receiver can fetch as follow-on actions. Populated from the `# Templates:` header (see Section 7).
|
|
1587
|
+
|
|
1588
|
+
The runtime threads these from `ExecuteContext.triggerCtx` (set at dispatch) and `ParsedSkill.deliveryContext` / `ParsedSkill.templates` (set at parse) through to the `deliver` call site.
|
|
1589
|
+
|
|
1590
|
+
#### `agent_id` resolution
|
|
1591
|
+
|
|
1592
|
+
When `# Output: agent:` or `# Output: template:` fires, the runtime resolves the target agent_id via a 2-level chain (first match wins):
|
|
1593
|
+
|
|
1594
|
+
1. **Explicit name in the `# Output:` line** — `# Output: agent: perry` dispatches to agent_id `perry`.
|
|
1595
|
+
2. **`${VAR}` compile-time substitution** — `# Output: agent: ${RECIPIENT}` resolves against the resolved inputs map (`# Vars:` defaults + `# Requires:` cascade + caller-supplied `inputs`) at compile time. Caveat: only compile-time inputs resolve here — runtime-bound refs (a target's output var, ambient refs) pass through verbatim and fail at delivery if still unresolved.
|
|
1596
|
+
|
|
1597
|
+
Invocation-context inheritance and a runtime-config `default_agent_id` fallback are NOT implemented: `# Output: agent:` does not auto-inherit the caller's identity, and there is no default-agent fallback. A skill must name its target explicitly or pass it as an input var.
|
|
1598
|
+
|
|
1599
|
+
#### Output-kind classification in the runtime
|
|
1600
|
+
|
|
1601
|
+
The runtime's `TEXT_COERCED_OUTPUT_KINDS` set classifies output kinds by payload shape (text vs structured), not by semantic destination. Membership controls payload coercion; it doesn't bake destination identity into the runtime.
|
|
1602
|
+
|
|
1603
|
+
### SkillStore
|
|
1604
|
+
|
|
1605
|
+
Routes skill source persistence. Interface:
|
|
1606
|
+
|
|
1607
|
+
```typescript
|
|
1608
|
+
interface SkillStore {
|
|
1609
|
+
get(name: string): Promise<SkillRecord | null>;
|
|
1610
|
+
write(name: string, body: string): Promise<void>;
|
|
1611
|
+
list(): Promise<SkillDescriptor[]>;
|
|
1612
|
+
delete(name: string): Promise<void>;
|
|
1613
|
+
}
|
|
1614
|
+
```
|
|
1615
|
+
|
|
1616
|
+
Bundled impls: `FilesystemSkillStore` reads and writes `.skill.md` source plus `.skill` compiled output and `.skill.provenance.json` sidecar in a configured directory; the standard for file-backed deployments. Substrate-specific impls live in adopter packages (memory-backed stores live in the substrate's adapter repo).
|
|
1617
|
+
|
|
1618
|
+
Skill records are infrastructure, not knowledge atoms — adopter impls should treat skills as first-class long-lived records, not as candidates for substrate-level garbage collection.
|
|
1619
|
+
|
|
1620
|
+
## Capabilities discovery
|
|
1621
|
+
|
|
1622
|
+
All connector types expose `capabilities()` for runtime discovery. Consumers:
|
|
1623
|
+
1. `# Requires:` matching against the registered set
|
|
1624
|
+
2. Dynamic queries via `listMemoryStores()` / `listLocalModels()` / `listMcpConnectors()` / `listAgentConnectors()` to pick a connector for the moment
|
|
1625
|
+
3. Authoring tools that surface the registered set
|
|
1626
|
+
|
|
1627
|
+
## Multi-instance by design
|
|
1628
|
+
|
|
1629
|
+
Multiple instances of the same connector type are the *normal case*, not the exception.
|
|
1630
|
+
|
|
1631
|
+
```
|
|
1632
|
+
{
|
|
1633
|
+
primary: MemoryStoreImplA,
|
|
1634
|
+
project: SqliteProjectStore,
|
|
1635
|
+
scratch: InMemoryStore
|
|
1636
|
+
}
|
|
1637
|
+
```
|
|
1638
|
+
|
|
1639
|
+
```
|
|
1640
|
+
{
|
|
1641
|
+
default: OllamaLocalModel({model: "gemma2:9b"}),
|
|
1642
|
+
gemma2: OllamaLocalModel({model: "gemma2:9b"}),
|
|
1643
|
+
qwen: OllamaLocalModel({model: "qwen2.5:7b"})
|
|
1644
|
+
}
|
|
1645
|
+
```
|
|
1646
|
+
|
|
1647
|
+
```
|
|
1648
|
+
{
|
|
1649
|
+
primary: PrimaryMcpConnector,
|
|
1650
|
+
personal: HttpMcpConnector,
|
|
1651
|
+
project: HttpMcpConnector
|
|
1652
|
+
}
|
|
1653
|
+
```
|
|
1654
|
+
|
|
1655
|
+
Per-skill resolution against named connectors is first-class; an unnamed lookup returns the configured default. Multiple keys pointing at the same underlying instance configuration are allowed and useful — see the `default`/`gemma2` alias below.
|
|
1656
|
+
|
|
1657
|
+
## Model selection — choosing among LocalModel instances
|
|
1658
|
+
|
|
1659
|
+
The LocalModel registry holds multiple instances by design. Skill authors choose which to dispatch to via `$ llm model="<name>"`. Two layers of indirection are involved, and the distinction matters for both authoring and adopter configuration:
|
|
1660
|
+
|
|
1661
|
+
1. **Skillscript name → registered instance.** `$ llm model="qwen"` references the instance keyed `qwen` in the registry. The registry resolves to the configured connector implementation.
|
|
1662
|
+
2. **Registered instance → underlying model.** Each `OllamaLocalModel` is constructed with the actual model tag (e.g. `qwen2.5:7b`). The skill never sees the tag directly.
|
|
1663
|
+
|
|
1664
|
+
### Example instance names
|
|
1665
|
+
|
|
1666
|
+
| Name | Underlying model | Notes |
|
|
1667
|
+
| --- | --- | --- |
|
|
1668
|
+
| `default` | `gemma2:9b` | Resolved when `model=` is omitted; alias of `gemma2` |
|
|
1669
|
+
| `gemma2` | `gemma2:9b` | Explicit name; matches the convention below |
|
|
1670
|
+
| `qwen` | `qwen2.5:7b` | Interactive, latency-sensitive |
|
|
1671
|
+
|
|
1672
|
+
`default` and `gemma2` can point at the same `OllamaLocalModel` configuration. The alias exists so skill syntax can match a tier convention ("use gemma2 for batch") rather than the back-compat name (`default`). Skills that write `model="default"` work unchanged; prefer the explicit name.
|
|
1673
|
+
|
|
1674
|
+
### Convention: model tier by use case
|
|
1675
|
+
|
|
1676
|
+
- **Small classification-class model** (e.g., `gemma2`) for *batch and scan work* — atomization, large-batch classification, anything async or background-scheduled.
|
|
1677
|
+
- **Longer-context dispatch-class model** (e.g., `qwen`) for *interactive verdicts in skills* — single-shot decisions inside an active dispatch where latency matters and queue contention with batch work would block forward progress.
|
|
1678
|
+
|
|
1679
|
+
When in doubt: small model if the call is asynchronous from a user/agent's perspective, larger model if a downstream op depends on the response.
|
|
1680
|
+
|
|
1681
|
+
### Contention property
|
|
1682
|
+
|
|
1683
|
+
Any skill that calls `$ llm` shares the underlying local-model service with every other process on the deployment that dispatches to the same model. Most local-model services serialize per-model dispatch. A skill that fires asynchronous batch work via `$` (e.g. invoking a batch-classification tool that dispatches N calls to model X) and then immediately calls `$ llm model="X"` will race itself — the synchronous call queues behind the dispatched batch.
|
|
1684
|
+
|
|
1685
|
+
The runtime does not promise concurrency-safe model dispatch. Skill authors and operators own model-tier allocation. The canonical mitigation: use distinct models for the synchronous and asynchronous paths (a smaller model for interactive verdicts, a larger model for batch).
|
|
1686
|
+
|
|
1687
|
+
### Adopter deployments
|
|
1688
|
+
|
|
1689
|
+
Adopters override the bundled set via `connectors.json`:
|
|
1690
|
+
|
|
1691
|
+
```jsonc
|
|
1692
|
+
{
|
|
1693
|
+
"localModels": {
|
|
1694
|
+
"default": { "type": "OllamaLocalModel", "model": "llama3.2:3b" },
|
|
1695
|
+
"fast": { "type": "OllamaLocalModel", "model": "phi3:mini" }
|
|
1696
|
+
}
|
|
1697
|
+
}
|
|
1698
|
+
```
|
|
1699
|
+
|
|
1700
|
+
Adopters with no local models register no LocalModel instances. Skills with `$ llm` ops fail at dispatch with `LocalModel '<name>' not registered`. A `# Requires:` capability declaration promotes this to a compile-time fail-fast — a skill that requires LocalModel won't compile if none is configured. Substrate-blind skills (no `$ llm` ops) work unchanged.
|
|
1701
|
+
|
|
1702
|
+
## Configuration: substrate selection vs operator config
|
|
1703
|
+
|
|
1704
|
+
Two distinct concerns, often conflated. Keep them separate.
|
|
1705
|
+
|
|
1706
|
+
**Substrate-class selection** — *which* connector implementation fills each slot. Configured via `connectors.json` `substrate.<slot>` entries, or programmatically via `bootstrap()` options. This is what makes a skill portable: the slot is named in config, never in the skill source. Connectors are runtime-resolved — the compiler stays pure read+transform, and compiled artifacts are generic, so any runtime can dispatch them through whatever substrates it has wired.
|
|
1707
|
+
|
|
1708
|
+
**Operator-runtime config** — the per-knob runtime settings: `SKILLSCRIPT_*` env vars, `skillscript.config.json`, and `bootstrap()` options. These tune runtime behavior; they do not select substrate classes.
|
|
1709
|
+
|
|
1710
|
+
Precedence (first wins): explicit `bootstrap()` option > env var > config file > bundled default.
|
|
1711
|
+
|
|
1712
|
+
Per-deployment naming lives in config, not the contract. A given deployment registers concrete instances under whatever names make sense locally; skill authors reference those names.
|
|
1713
|
+
|
|
1714
|
+
The `connectors.json` loader handles literal + `${ENV_VAR}` credential resolution, file-discovery via `$SKILLSCRIPT_HOME`, and a class registry recognizing the bundled connector classes (including `CallbackMcpConnector` and `RemoteMcpConnector`). The per-connector `allowed_tools` field constrains dispatch surface for safe defaults — a cold-author skill against the connector can only invoke allowlisted tools. See ERD §3 + §4 for the full credential-discipline contract.
|
|
1715
|
+
|
|
1716
|
+
## Per-call identity overrides (McpConnector)
|
|
1717
|
+
|
|
1718
|
+
A skill running under one identity can dispatch against a personal MCP server under a different identity without needing connector-internal state. The merge order at dispatch (top wins):
|
|
1719
|
+
|
|
1720
|
+
1. **Registry-configured per-connector identity** — set in `connectors.json` (`identity: { agentId: "<id>", isAdmin: false }`) at connector instantiation. Locks an identity to a connector.
|
|
1721
|
+
2. **Per-call `ctxOverrides`** — threaded by the runtime per the security boundary contract. A skill running as agent X passes `{ agentId: "X", isAdmin: false }` into every `$` op.
|
|
1722
|
+
3. **(no intrinsic identity)** — adapter forwards whatever the merge produces.
|
|
1723
|
+
|
|
1724
|
+
Configured identity is a *partial merge* — unmentioned keys (e.g., `isAdmin`) flow through from the per-call ctx. Lets a connector lock `agentId` without clobbering the runtime's admin-drop discipline. Default connectors should configure no intrinsic identity, so `ctxOverrides` always wins — preserving the runtime's authority-flow guarantees intact.
|
|
1725
|
+
|
|
1726
|
+
## Portable shapes
|
|
1727
|
+
|
|
1728
|
+
```typescript
|
|
1729
|
+
interface PortableMemory {
|
|
1730
|
+
// Core fields — mandatory on every connector return.
|
|
1731
|
+
id: string;
|
|
1732
|
+
summary: string;
|
|
1733
|
+
detail?: string;
|
|
1734
|
+
score?: number;
|
|
1735
|
+
|
|
1736
|
+
// Curated substrate subset — concept-portable, value-substrate-specific.
|
|
1737
|
+
// Top-level access via $(MEMORY.field). Connectors populate when the
|
|
1738
|
+
// concept applies. MUST NOT also be duplicated into metadata.
|
|
1739
|
+
thread_status?: string;
|
|
1740
|
+
pinned?: boolean;
|
|
1741
|
+
confidence?: number;
|
|
1742
|
+
domain_tags?: string[];
|
|
1743
|
+
payload_type?: string;
|
|
1744
|
+
knowledge_type?: string;
|
|
1745
|
+
recipients?: string[];
|
|
1746
|
+
expires_at?: number;
|
|
1747
|
+
created_at?: number;
|
|
1748
|
+
agent_id?: string;
|
|
1749
|
+
vault?: string;
|
|
1750
|
+
|
|
1751
|
+
// Substrate-specific bag. Accessed via $(MEMORY.metadata.X).
|
|
1752
|
+
metadata?: Record<string, unknown>;
|
|
1753
|
+
}
|
|
1754
|
+
|
|
1755
|
+
interface QueryFilters {
|
|
1756
|
+
query: string;
|
|
1757
|
+
limit: number;
|
|
1758
|
+
mode: "fts" | "semantic" | "rerank" | string;
|
|
1759
|
+
[key: string]: unknown;
|
|
1760
|
+
}
|
|
1761
|
+
|
|
1762
|
+
interface McpDispatchCtx {
|
|
1763
|
+
agentId?: string;
|
|
1764
|
+
isAdmin?: boolean;
|
|
1765
|
+
}
|
|
1766
|
+
```
|
|
1767
|
+
|
|
1768
|
+
## Field access semantics
|
|
1769
|
+
|
|
1770
|
+
`$(MEMORY.field)` resolves in tiers:
|
|
1771
|
+
1. Core fields (id, summary, detail, score)
|
|
1772
|
+
2. Curated substrate subset (thread_status, pinned, etc.)
|
|
1773
|
+
3. `metadata.X` for everything else
|
|
1774
|
+
4. Ambient passthrough as literal `$(MEMORY.field)` if unresolved
|
|
1775
|
+
|
|
1776
|
+
**Connector duplication is a contract violation.** If a field is in the curated subset, the connector populates it at top-level only — `metadata.<same_name>` MUST be absent. Otherwise `$(M.thread_status)` and `$(M.metadata.thread_status)` can return different values (silent data divergence). Connectors enforce.
|
|
1777
|
+
|
|
1778
|
+
## Why connector abstraction matters
|
|
1779
|
+
|
|
1780
|
+
Hard-coupling skills to specific substrates would make information-flow decisions infrastructural rather than skill-authored, defeating the point of skills as the agent's programming language. The connector layer is what lets the same skill body run against substrate A today and run against substrate B tomorrow without rewriting.
|
|
1781
|
+
|
|
1465
1782
|
## Lifecycle and status — # Status: header, six canonical states, compile + runtime enforcement
|
|
1466
1783
|
|
|
1467
1784
|
Skillscripts carry an explicit lifecycle state via the `# Status:` header. The compiler and runtime enforce status — a Disabled skillscript cannot fire under any path, regardless of who invokes it.
|
|
@@ -1595,16 +1912,17 @@ Nested `# OnError:` is *not* supported. If `# OnError: degraded-skill` fires and
|
|
|
1595
1912
|
|
|
1596
1913
|
## Layer 3: Op-level fallback values
|
|
1597
1914
|
|
|
1598
|
-
Inline fallback declared on the op line. Used when the call fails or returns empty. Supported on `$` (MCP dispatch) ops with coerce-on-bind semantics.
|
|
1915
|
+
Inline fallback declared on the op line. Used when the call fails or returns empty. Supported on `$` (MCP dispatch) ops and on `shell()` with coerce-on-bind semantics. For `shell()` the fallback fires on op throw OR empty stdout (e.g. `gh pr list` against a repo with no open PRs binds the fallback string).
|
|
1599
1916
|
|
|
1600
1917
|
```
|
|
1601
1918
|
weather:
|
|
1602
1919
|
$ data_read mode=fts query="weather ${LOCATION}" limit=1 -> CURRENT (fallback: "weather unavailable")
|
|
1603
1920
|
$ llm prompt="Summarize: ${CURRENT}" -> SUMMARY (fallback: "summary unavailable")
|
|
1604
1921
|
$ slack.post channel=${CHANNEL} text=${SUMMARY} (fallback: "post failed silently") -> ACK
|
|
1922
|
+
shell(argv=["gh","pr","list","--repo","acme/foo"]) -> PRS (fallback: "No open PRs")
|
|
1605
1923
|
```
|
|
1606
1924
|
|
|
1607
|
-
Same pattern as the `# Requires:` cascade's `(fallback: ...)` syntax — consistent across compile-time (`# Requires:`) and runtime (`$` dispatch).
|
|
1925
|
+
Same pattern as the `# Requires:` cascade's `(fallback: ...)` syntax — consistent across compile-time (`# Requires:`) and runtime (`$` dispatch and `shell()`).
|
|
1608
1926
|
|
|
1609
1927
|
**Fallback value parsing.** Permissive: bare identifiers, quoted strings, and bracketed array literals all accepted. Matches the `# Requires:` cascade convention.
|
|
1610
1928
|
|
|
@@ -1632,7 +1950,7 @@ Open spec question: should `${ERROR}` be ambient inside `else:` blocks (same sha
|
|
|
1632
1950
|
|
|
1633
1951
|
Same idea at every scope:
|
|
1634
1952
|
- Compile-time: `# Requires: ... (fallback: value)`
|
|
1635
|
-
- Runtime op: `$ dispatch ... (fallback: value)`
|
|
1953
|
+
- Runtime op: `$ dispatch ... (fallback: value)` and `shell(...) ... (fallback: value)`
|
|
1636
1954
|
- Runtime target: `else:` block
|
|
1637
1955
|
- Whole skill: `# OnError:` header
|
|
1638
1956
|
|
|
@@ -1640,7 +1958,7 @@ Authors composing complex skills use these in combination — op-level for trans
|
|
|
1640
1958
|
|
|
1641
1959
|
## Connection to runtime observability
|
|
1642
1960
|
|
|
1643
|
-
Per-op error contract is what makes cascading fallbacks work. When `$` returns `isError: true`, the executor throws via `makeOpError` rather than binding the error text to the output var. The throw routes through `else:` / `# OnError:` machinery and surfaces in `result.errors[]` for the scheduler to log. Without this discipline, op-level failures wouldn't propagate to the fallback layers and silent-fail would be the default.
|
|
1961
|
+
Per-op error contract is what makes cascading fallbacks work. When `$` returns `isError: true`, the executor throws via `makeOpError` rather than binding the error text to the output var. The throw routes through `else:` / `# OnError:` machinery and surfaces in `result.errors[]` for the scheduler to log. Without this discipline, op-level failures wouldn't propagate to the fallback layers and silent-fail would be the default. Op-level `(fallback:)` is distinct: it intercepts the throw/empty-result at the op and binds the fallback value rather than propagating — a fired fallback is recorded in `result.fallbacks[]`, not `result.errors[]`.
|
|
1644
1962
|
|
|
1645
1963
|
## Composition — skills calling skills
|
|
1646
1964
|
|
|
@@ -2208,5 +2526,5 @@ Hung dispatches hang the skill without explicit timeout configuration. Lean: ski
|
|
|
2208
2526
|
|
|
2209
2527
|
---
|
|
2210
2528
|
|
|
2211
|
-
*Rendered from `skillscript/skillscript-language-reference` — 2026-06-
|
|
2212
|
-
*Source of truth: AMP (`amp_render_document("skillscript/skillscript-language-reference")`)*
|
|
2529
|
+
*Rendered from `skillscript/skillscript-language-reference` — 2026-06-15 18:44 EDT*
|
|
2530
|
+
*Source of truth: AMP (`amp_render_document("skillscript/skillscript-language-reference")`)*
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
# Skill: classify-support-ticket
|
|
2
|
-
# Status: Approved v1:
|
|
2
|
+
# Status: Approved v1:ae27d3f9
|
|
3
3
|
# Autonomous: true
|
|
4
4
|
# Description: Read an incoming support ticket and route it: severity-1 tickets get paged to ops-channel, billing tickets get tagged for finance review, everything else gets a draft reply queued for human review
|
|
5
5
|
# Vars: TICKET_TEXT, TICKET_ID
|
|
@@ -14,13 +14,13 @@ severity_check:
|
|
|
14
14
|
|
|
15
15
|
route:
|
|
16
16
|
needs: severity_check
|
|
17
|
-
if ${CATEGORY|
|
|
17
|
+
if ${CATEGORY|contains:"sev-1"}:
|
|
18
18
|
emit(text="PAGE: sev-1 ticket ${TICKET_ID} - ${TICKET_TEXT}")
|
|
19
19
|
$ data_write content="sev-1 ticket ${TICKET_ID}: ${TICKET_TEXT}" tags=["support","sev-1","page"]
|
|
20
|
-
elif ${IS_SEV1|
|
|
20
|
+
elif ${IS_SEV1|contains:"yes"}:
|
|
21
21
|
emit(text="PAGE: classifier said ${CATEGORY|trim} but severity-check flagged this as sev-1: ${TICKET_ID}")
|
|
22
22
|
$ data_write content="sev-1 escalation ${TICKET_ID}: category=${CATEGORY|trim} but severity-check=yes" tags=["support","sev-1","disagreement"]
|
|
23
|
-
elif ${CATEGORY|
|
|
23
|
+
elif ${CATEGORY|contains:"billing"}:
|
|
24
24
|
$set TAG_FOR = "finance"
|
|
25
25
|
emit(text="Tagged for ${TAG_FOR} review: ${TICKET_ID}")
|
|
26
26
|
$ data_write content="billing ticket ${TICKET_ID}: ${TICKET_TEXT}" tags=["support","billing"]
|
|
@@ -1,29 +1,18 @@
|
|
|
1
1
|
# Skill: feedback-sentiment-scan
|
|
2
|
-
# Status: Approved v1:
|
|
3
|
-
#
|
|
4
|
-
# Description: Each night, scan the previous 24h of customer feedback records, classify sentiment via local model, surface entries where sentiment is "frustrated" or "blocking" so the team sees them at start-of-day; skip entries already seen on prior nights
|
|
2
|
+
# Status: Approved v1:2b4868c1
|
|
3
|
+
# Description: Each night, scan the previous 24h of customer feedback records, classify sentiment via the local model, and surface entries that read 'frustrated' or 'blocking' so the team sees them at start-of-day.
|
|
5
4
|
# Triggers: cron: 0 3 * * *
|
|
6
5
|
# Vars: SCAN_LIMIT=50
|
|
7
6
|
# Output: agent: support-lead
|
|
8
7
|
|
|
9
|
-
|
|
8
|
+
scan:
|
|
10
9
|
$ data_read mode=fts query="customer feedback" limit=${SCAN_LIMIT} -> FEEDBACK
|
|
11
|
-
|
|
12
|
-
fetch_seen:
|
|
13
|
-
$ data_read mode=fts query="sentiment-scan seen marker" limit=200 domain_tags=["sentiment-scan-seen"] -> SEEN_MARKERS
|
|
14
|
-
|
|
15
|
-
classify_and_emit:
|
|
16
|
-
needs: fetch_new, fetch_seen
|
|
17
10
|
emit(text="Sentiment scan results for ${NOW}:")
|
|
18
11
|
foreach F in ${FEEDBACK.items}:
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
emit(text="- FRUSTRATED [${F.id|trim}] ${F.summary}")
|
|
25
|
-
elif ${VERDICT|trim} == "blocking":
|
|
26
|
-
emit(text="- BLOCKING [${F.id|trim}] ${F.summary}")
|
|
27
|
-
$ data_write content="sentiment-scan seen ${F.id|trim} verdict=${VERDICT|trim} on ${EVENT.fired_at_unix}" tags=["sentiment-scan-seen"] expires_at=${EVENT.fired_at_plus_7d_unix} -> ACK
|
|
12
|
+
$ llm prompt="Classify the sentiment of this customer feedback. Respond with ONE word: 'frustrated', 'blocking', 'satisfied', 'neutral'. No explanation.\n\nFeedback: ${F.summary}\nDetail: ${F.detail}" maxTokens=10 -> VERDICT
|
|
13
|
+
if ${VERDICT|contains:"frustrated"}:
|
|
14
|
+
emit(text="- FRUSTRATED [${F.id|trim}] ${F.summary}")
|
|
15
|
+
elif ${VERDICT|contains:"blocking"}:
|
|
16
|
+
emit(text="- BLOCKING [${F.id|trim}] ${F.summary}")
|
|
28
17
|
|
|
29
|
-
default:
|
|
18
|
+
default: scan
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Skill: morning-brief
|
|
2
|
-
# Status: Approved v1:
|
|
3
|
-
# Description: Compose a daily morning brief from calendar, mailbox, and overnight
|
|
2
|
+
# Status: Approved v1:1530981d
|
|
3
|
+
# Description: Compose a daily morning brief from calendar, mailbox, and overnight notes when the cron trigger fires at 7am. Delivers via the agent: lifecycle hook to the receiving agent, who decides whether to surface to Slack / Discord / etc. Requires a `calendar` connector configured in connectors.json (the dotted `$ calendar.*` form). `model=qwen` is a representative LocalModel alias — adopters register one under whatever name fits (the bundled bootstrap registers `default`).
|
|
4
4
|
# Vars: AGENT, BRIEF_HORIZON_HOURS=24
|
|
5
5
|
# Triggers: cron: 0 7 * * *
|
|
6
6
|
# OnError: morning-brief-degraded
|
|
@@ -12,10 +12,10 @@ calendar:
|
|
|
12
12
|
$ calendar.list_events horizon_hours=${BRIEF_HORIZON_HOURS} -> EVENTS
|
|
13
13
|
|
|
14
14
|
mailbox:
|
|
15
|
-
$ data_read mode=fts query="
|
|
15
|
+
$ data_read mode=fts query="messages for ${AGENT}" limit=10 -> MAIL
|
|
16
16
|
|
|
17
17
|
overnight:
|
|
18
|
-
$ data_read mode=rerank query="overnight writes
|
|
18
|
+
$ data_read mode=rerank query="overnight notes and writes" limit=15 -> NOTES
|
|
19
19
|
|
|
20
20
|
compose: needs: calendar, mailbox, overnight
|
|
21
21
|
$ llm prompt="Compose a concise morning brief. Calendar: ${EVENTS|json}. Mailbox: ${MAIL|json}. Overnight notes: ${NOTES|json}. Three sections, six bullets max each." model=qwen maxTokens=1200 -> BRIEF
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Skill: queue-length-monitor
|
|
2
|
-
#
|
|
3
|
-
#
|
|
2
|
+
# Status: Approved v1:c6bc06df
|
|
3
|
+
# Description: Count pending items in a queue and alert when the count exceeds threshold. Requires `cat` on the operator's shell allowlist (default-deny: a non-allowlisted binary is refused).
|
|
4
4
|
# Vars: QUEUE_PATH=/var/queue/pending.json, THRESHOLD=10
|
|
5
5
|
# Triggers: cron: */5 * * * *
|
|
6
6
|
|