skillscript-runtime 0.27.1 → 0.27.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +21 -0
- package/README.md +5 -1
- package/dist/cli.js +6 -2
- package/dist/cli.js.map +1 -1
- package/dist/dashboard/spa/app.js +8 -2
- package/dist/errors.js +2 -2
- package/dist/errors.js.map +1 -1
- package/dist/help-content.js +4 -4
- package/docs/adopter-playbook.md +9 -7
- package/docs/connector-contract-reference.md +28 -26
- package/docs/language-reference.md +61 -93
- package/examples/skillscripts/classify-support-ticket.skill.provenance.json +1 -1
- package/examples/skillscripts/data-store-roundtrip.skill.provenance.json +1 -1
- package/examples/skillscripts/doc-qa-with-citations.skill.provenance.json +1 -1
- package/examples/skillscripts/feedback-sentiment-scan.skill.provenance.json +1 -1
- package/examples/skillscripts/hello-world.skill.provenance.json +1 -1
- package/examples/skillscripts/morning-brief.skill.provenance.json +1 -1
- package/examples/skillscripts/queue-length-monitor.skill.provenance.json +1 -1
- package/examples/skillscripts/service-health-watch.skill.provenance.json +1 -1
- package/examples/skillscripts/skill-store-roundtrip.skill.provenance.json +1 -1
- package/examples/skillscripts/youtrack-morning-sweep.skill.provenance.json +1 -1
- package/package.json +1 -1
|
@@ -301,7 +301,7 @@ Closed list of language-intrinsic ops the runtime knows directly. Each is a func
|
|
|
301
301
|
| `emit` | `emit(text="...")` | none | Append to the skill's emission stream; consumed by the configured `# Output:` delivery channel. |
|
|
302
302
|
| `notify` | `notify(agent="...", message="...", [event_type=...], [correlation_id=...]) -> ACK` | optional | Mid-skill agent alert; synchronous send via configured AgentConnector. |
|
|
303
303
|
| `inline` | `inline(skill="<data-skill-name>")` | none | Compile-time inline of an Approved `# Type: data` skill. Resolves at compile, records `content_hash` in provenance. |
|
|
304
|
-
| `execute_skill` | `execute_skill(
|
|
304
|
+
| `execute_skill` | `execute_skill(name="...", inputs=\{...}) -> R` | optional | Composition primitive. Runtime-resolved. `skill_name=` accepted as back-compat alias. See Composition section. |
|
|
305
305
|
| `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. |
|
|
306
306
|
| `file_read` | `file_read(path="...") -> R` | required | Read a file at `path`; binds string contents. Optional `encoding="utf8"\|"base64"` kwarg (default `utf8`). |
|
|
307
307
|
| `file_write` | `file_write(path="...", content="...")` | none | Write `content` to `path`. `mkdir -p` semantics for parent directories. Mutation-classified. |
|
|
@@ -358,7 +358,7 @@ Returns ACK `{agent, dispatched: [{connector, ok, error?}]}` — fire-and-forget
|
|
|
358
358
|
|
|
359
359
|
Three forms.
|
|
360
360
|
|
|
361
|
-
**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:`
|
|
361
|
+
**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 the target's `else:` handler.
|
|
362
362
|
|
|
363
363
|
```
|
|
364
364
|
shell(command="curl -s 'wttr.in/${LOCATION|url}?format=j1'") -> RAW
|
|
@@ -437,10 +437,10 @@ See Composition section for the distinction between `inline` (compile-time), `ex
|
|
|
437
437
|
|
|
438
438
|
```
|
|
439
439
|
classify:
|
|
440
|
-
execute_skill(
|
|
440
|
+
execute_skill(name="classifier", inputs={"text": "${INPUT}"}) -> VERDICT
|
|
441
441
|
```
|
|
442
442
|
|
|
443
|
-
Runtime-resolved against the SkillStore. Recursion-depth-guarded (default 10).
|
|
443
|
+
Runtime-resolved against the SkillStore. Recursion-depth-guarded (default 10). Canonical kwarg is `name=`; `skill_name=` is an accepted back-compat alias.
|
|
444
444
|
|
|
445
445
|
**Returns.** The caller's `-> R` binding receives `outputs` + `transcript` + execution metadata always, plus the child's declared `# Returns:` surface (each declared var accessible as `${R.X}`). Undeclared scratch is filtered — a child with no `# Returns:` yields `final_vars: {}`. This is what keeps composition from compounding (a child's internal state never propagates up). Full contract in Composition § Return contract.
|
|
446
446
|
|
|
@@ -517,7 +517,7 @@ $ amp.amp_olsen_task task_type="scan" timeout=120 -> DISPATCH
|
|
|
517
517
|
$ data_write content="${REPORT}" tags=["oncall"] approved="morning roundup" -> ACK
|
|
518
518
|
```
|
|
519
519
|
|
|
520
|
-
**`(fallback: "value")` trailer** —
|
|
520
|
+
**`(fallback: "value")` trailer** — Uniform as of v0.27.0: fires on ANY op failure — a dispatch throw (including a `timeout=N` expiry), an empty bound value (empty string after trim, empty array, null/undefined), OR a raised throw from an `execute_skill` child or a `$ json_parse` off-shape input. 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[]` (with its `.reason`).
|
|
521
521
|
|
|
522
522
|
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:[]}`).
|
|
523
523
|
|
|
@@ -599,7 +599,7 @@ $ data_write content="${SUMMARY}" tags=["oncall"] approved="morning roundup, 202
|
|
|
599
599
|
$ ticketing.search query="project:INFRA state:Open" limit=20 -> ISSUES
|
|
600
600
|
```
|
|
601
601
|
|
|
602
|
-
Tool args are unconstrained `key=value` pairs — the connector forwards them to the underlying MCP tool. If a dispatched call returns `isError: true`, the executor throws via `makeOpError`, which routes through `else:`
|
|
602
|
+
Tool args are unconstrained `key=value` pairs — the connector forwards them to the underlying MCP tool. If a dispatched call returns `isError: true`, the executor throws via `makeOpError`, which routes through the target's `else:` handler. The inner tool's error text is preserved in `result.errors[]`.
|
|
603
603
|
|
|
604
604
|
**Substrate-neutrality.** Typed-contract bare ops (`$ llm`, `$ data_read`, `$ data_write`, `$ skill_*`, `$ json_parse`) are not reserved built-ins — they're typed-contract bridges the adopter wires through `substrate.local_model` / `substrate.data_store` / `substrate.skill_store` in `skillscript.config.json`. Substrate-specific tools (`$ amp.*`, `$ github.*`, etc.) are whatever the adopter declares in `connectors.json`. For external MCP servers, three bundled wiring paths cover the common cases:
|
|
605
605
|
|
|
@@ -669,23 +669,13 @@ deliver:
|
|
|
669
669
|
| Runtime-intrinsic | `emit` | `emit(text="...")` or `emit(text="""...""")` for multi-line | none |
|
|
670
670
|
| Runtime-intrinsic | `notify` | `notify(agent="...", [message=...], [event_type=...], [correlation_id=...]) -> ACK` | optional |
|
|
671
671
|
| Runtime-intrinsic | `inline` | `inline(skill="<name>")` | none (compile-time) |
|
|
672
|
-
| Runtime-intrinsic | `execute_skill` | `execute_skill(
|
|
672
|
+
| Runtime-intrinsic | `execute_skill` | `execute_skill(name="...", inputs=\{...}) -> R` (`skill_name=` back-compat alias) | optional |
|
|
673
673
|
| 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 |
|
|
674
674
|
| Runtime-intrinsic | `file_read` | `file_read(path="...", [encoding="utf8"\|"base64"]) -> R` | required |
|
|
675
675
|
| Runtime-intrinsic | `file_write` | `file_write(path="...", content="...", [approved="..."])` | none |
|
|
676
676
|
| External MCP — substrate-specific | `$ <connector>.<tool>` | `$ <connector>.<tool> kwarg=value, ... [timeout=N] [approved="..."] [-> R] [(fallback: "...")]` | optional |
|
|
677
677
|
| External MCP — typed-contract | `$ <tool>` | `$ <tool> kwarg=value, ... [timeout=N] [approved="..."] [-> R] [(fallback: "...")]` (typed-contract ops only: `data_*`, `skill_*`, `json_parse`, `llm`) | optional |
|
|
678
678
|
|
|
679
|
-
---
|
|
680
|
-
|
|
681
|
-
## Current-runtime corrections (v0.27.0)
|
|
682
|
-
|
|
683
|
-
A few points above predate the 0.26/0.27 error-handling changes — read them with these corrections:
|
|
684
|
-
|
|
685
|
-
- **`(fallback:)` is now UNIFORM (v0.27.0).** Beyond `$` dispatch and `shell()`, the op-level `(fallback:)` trailer now also contains a raised throw from `execute_skill` (a child skill that throws) and `$ json_parse` (malformed input). It contains ANY failure of the op — a raised throw OR an empty/missing result — on every fallible op. A fired fallback is recorded in `result.fallbacks[].reason`.
|
|
686
|
-
- **`# OnError:` is parsed but NOT wired** in the current runtime — the named fallback skill never fires. Wherever the text above says an op-error "routes through `else:` / `# OnError:`", read it as `else:` only. Use `else:` (or an op-level `(fallback:)`, or a structural guard) for containment. See the Error handling and Robustness sections.
|
|
687
|
-
- **`execute_skill` canonical kwarg is `name=`** (`skill_name=` is an accepted silent back-compat alias). Prefer `execute_skill(name="...", inputs={...}) -> R` in new skills; the `skill_name=` examples above still work but aren't canonical.
|
|
688
|
-
|
|
689
679
|
## Variable resolution — ${VAR} canonical, substitution + ambient refs + # Requires: cascade
|
|
690
680
|
|
|
691
681
|
Skillscript supports four tiers of variables, each with distinct resolution timing and scope. Substitution uses **`${VAR}` as the canonical form**.
|
|
@@ -714,7 +704,7 @@ Injected automatically at runtime; never declared by the author.
|
|
|
714
704
|
| `$\{TRIGGER_TYPE}` | What event fired this skill |
|
|
715
705
|
| `$\{TRIGGER_PAYLOAD}` | Event-specific data |
|
|
716
706
|
| `$\{EVENT.*}` | Event-payload fields populated by the trigger source |
|
|
717
|
-
| `$\{ERROR_CONTEXT}` |
|
|
707
|
+
| `$\{ERROR_CONTEXT}` | Inside a target's `else:` error-handler: kind + message + target of the failure. |
|
|
718
708
|
|
|
719
709
|
Iterator vars from `foreach` and output bindings from runtime-intrinsic / MCP-dispatch ops also pass through ambient at compile time; the runtime substitutes them per iteration / per op completion.
|
|
720
710
|
|
|
@@ -811,10 +801,6 @@ Missing-ref in the RHS produces a tier-1 runtime error.
|
|
|
811
801
|
- `foreach IDENT in EXPR:` iterator vars are loop-local — `$set` bindings inside the loop don't persist after the loop ends
|
|
812
802
|
- Target outputs (`${target.output}`) are accessible after the target completes
|
|
813
803
|
|
|
814
|
-
## Current-runtime correction (v0.27.0)
|
|
815
|
-
|
|
816
|
-
**`${ERROR_CONTEXT}` is surfaced in `else:` error-handler blocks, not `# OnError:`.** The Tier-1 ambient table above describes `${ERROR_CONTEXT}` as available "In `# OnError:` fallback skills" — that is stale. `# OnError:` is parsed but inert (never fires). The working error-handler is the target-level `else:` block (runs when any op in the target body throws), and `${ERROR_CONTEXT}` (failure type + target) is populated inside that `else:` block. Read the table row as: "In an `else:` error-handler: type + target where failure occurred." See the Error handling and Conditionals sections for `else:`-handler semantics.
|
|
817
|
-
|
|
818
804
|
## Secrets — secret.NAME references, {{secret.NAME}} sink markers, SKILLSCRIPT_SECRET_ provisioning, use-only enforcement
|
|
819
805
|
|
|
820
806
|
## Secrets
|
|
@@ -968,6 +954,8 @@ emit:
|
|
|
968
954
|
emit(text="nested: ${ISSUE.customFields.Assignee|fallback:\"unassigned\"}")
|
|
969
955
|
```
|
|
970
956
|
|
|
957
|
+
**Order-independent in a filter chain (v0.26.2+).** A `|fallback` anywhere in the chain rescues an unresolved base — `${x|trim|fallback:"d"}` and `${x|fallback:"d"|trim}` both degrade an unresolved `x` to `"d"`. Nuance: it rescues a genuinely-unresolved/missing base; a **present-but-empty value still flows through the other filters**, so `${x|length|fallback:"0"}` on `x=" "` returns `"2"`, not `"0"`. See the Robustness & error containment section for the chain semantics.
|
|
958
|
+
|
|
971
959
|
**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.
|
|
972
960
|
|
|
973
961
|
**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.
|
|
@@ -1032,7 +1020,7 @@ elif ${BODY|length} > 1000:
|
|
|
1032
1020
|
|
|
1033
1021
|
## Error handling
|
|
1034
1022
|
|
|
1035
|
-
Unknown filter on a resolved variable produces a tier-1 `unknown-filter` compile error. Catches both bare (`|unknown`) and colon-positional (`|unknown:"arg"`) shapes. Filter chains that fail at runtime (e.g., `|json` on a non-serializable value, `|length` on a number, `|isodate` on a non-numeric value) produce op errors that route through `else:`
|
|
1023
|
+
Unknown filter on a resolved variable produces a tier-1 `unknown-filter` compile error. Catches both bare (`|unknown`) and colon-positional (`|unknown:"arg"`) shapes. Filter chains that fail at runtime (e.g., `|json` on a non-serializable value, `|length` on a number, `|isodate` on a non-numeric value) produce op errors that route through the target's `else:` handler.
|
|
1036
1024
|
|
|
1037
1025
|
Bare `${NAME}` without a filter is unchanged.
|
|
1038
1026
|
|
|
@@ -1061,13 +1049,6 @@ Filters are pure functions (input → output, no side effects). Stay small and o
|
|
|
1061
1049
|
|
|
1062
1050
|
`length`, `fallback:`, `isodate`, and `contains:` were all added in response to cold-author harness signal — authored skills demonstrated the gap was load-bearing before each filter shipped. `contains:` is also notable as the first filter to operate on structured types; filter-as-conditional-primitive is the design line that warranted the cross.
|
|
1063
1051
|
|
|
1064
|
-
---
|
|
1065
|
-
|
|
1066
|
-
## Current-runtime corrections
|
|
1067
|
-
|
|
1068
|
-
- **`|fallback` is order-independent in a filter chain (v0.26.2+).** A `|fallback` anywhere in the chain rescues an unresolved base — `${x|trim|fallback:"d"}` and `${x|fallback:"d"|trim}` both degrade an unresolved `x` to `"d"`. Nuance: it rescues a genuinely-unresolved/missing base; a present-but-empty value still flows through the other filters, so `${x|length|fallback:"0"}` on `x=" "` returns `"2"`, not `"0"`. See the Robustness & error containment section for the chain semantics.
|
|
1069
|
-
- The "Error handling" note above ("Filter chains that fail at runtime … route through `else:` / `# OnError:` machinery") should read **`else:`** — `# OnError:` is parsed but not wired in the current runtime.
|
|
1070
|
-
|
|
1071
1052
|
## Conditionals & iteration — if/elif/else, foreach, supported operators
|
|
1072
1053
|
|
|
1073
1054
|
Skillscript supports narrow conditionals and bounded iteration. Both are deliberately constrained — composition over expressiveness.
|
|
@@ -1531,11 +1512,7 @@ The receiving agent reads `event_type` for routing ("911 — surface now" vs "ro
|
|
|
1531
1512
|
|
|
1532
1513
|
## Output routing failures
|
|
1533
1514
|
|
|
1534
|
-
If `# Output: agent: <name>` fires and the wired AgentConnector throws, delivery routes through `else:`
|
|
1535
|
-
|
|
1536
|
-
## Current-runtime correction (v0.27.0)
|
|
1537
|
-
|
|
1538
|
-
**Output routing failures land in `else:`, not `# OnError:`.** The "Output routing failures" section says a throwing AgentConnector "routes through `else:` / `# OnError:`." `# OnError:` is parsed but inert (never fires) — read that as `else:` only. The target-level `else:` handler catches the delivery throw; the failure is still recorded on `agent_delivery_receipts[]` for the scheduler regardless.
|
|
1515
|
+
If `# Output: agent: <name>` fires and the wired AgentConnector throws, delivery routes through the target's `else:` handler; the failure is recorded on `agent_delivery_receipts[]` for the scheduler to log.
|
|
1539
1516
|
|
|
1540
1517
|
## Connectors — substrate routing, the five connector types, agent_id resolution
|
|
1541
1518
|
|
|
@@ -1776,7 +1753,7 @@ interface PortableMemory {
|
|
|
1776
1753
|
score?: number;
|
|
1777
1754
|
|
|
1778
1755
|
// Curated substrate subset — concept-portable, value-substrate-specific.
|
|
1779
|
-
// Top-level access via $
|
|
1756
|
+
// Top-level access via ${MEMORY.field}. Connectors populate when the
|
|
1780
1757
|
// concept applies. MUST NOT also be duplicated into metadata.
|
|
1781
1758
|
thread_status?: string;
|
|
1782
1759
|
pinned?: boolean;
|
|
@@ -1790,7 +1767,7 @@ interface PortableMemory {
|
|
|
1790
1767
|
agent_id?: string;
|
|
1791
1768
|
vault?: string;
|
|
1792
1769
|
|
|
1793
|
-
// Substrate-specific bag. Accessed via $
|
|
1770
|
+
// Substrate-specific bag. Accessed via ${MEMORY.metadata.X}.
|
|
1794
1771
|
metadata?: Record<string, unknown>;
|
|
1795
1772
|
}
|
|
1796
1773
|
|
|
@@ -1809,22 +1786,18 @@ interface McpDispatchCtx {
|
|
|
1809
1786
|
|
|
1810
1787
|
## Field access semantics
|
|
1811
1788
|
|
|
1812
|
-
`$
|
|
1789
|
+
`${MEMORY.field}` resolves in tiers:
|
|
1813
1790
|
1. Core fields (id, summary, detail, score)
|
|
1814
1791
|
2. Curated substrate subset (thread_status, pinned, etc.)
|
|
1815
1792
|
3. `metadata.X` for everything else
|
|
1816
|
-
4. Ambient passthrough as literal `$
|
|
1793
|
+
4. Ambient passthrough as literal `${MEMORY.field}` if unresolved
|
|
1817
1794
|
|
|
1818
|
-
**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 `$
|
|
1795
|
+
**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.
|
|
1819
1796
|
|
|
1820
1797
|
## Why connector abstraction matters
|
|
1821
1798
|
|
|
1822
1799
|
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.
|
|
1823
1800
|
|
|
1824
|
-
## Notation correction
|
|
1825
|
-
|
|
1826
|
-
The "Portable shapes" and "Field access semantics" sections write memory-field access as `$(MEMORY.field)` / `$(M.thread_status)` / `$(MEMORY.metadata.X)`. That is the wrong sigil. Skillscript's canonical substitution form is `${...}` (brace), not `$(...)` (paren) — the paren form is deliberately reserved to avoid collision with bash `$(command)` inside `shell(command=..., unsafe=true)` (see the Variable resolution section). Read every `$(MEMORY.field)` here as `${MEMORY.field}`. The tiered field-access resolution (core → curated subset → `metadata.X` → ambient passthrough) is otherwise accurate; only the sigil is wrong.
|
|
1827
|
-
|
|
1828
1801
|
## Lifecycle and status — # Status: header, three canonical states (Draft / Approved / Disabled), compile + runtime enforcement
|
|
1829
1802
|
|
|
1830
1803
|
Skillscripts carry an explicit lifecycle state via the `# Status:` header. The compiler and runtime enforce it — a Disabled skill cannot fire under any path, and (in secured mode) an unapproved skill cannot perform effectful ops under any path.
|
|
@@ -1888,11 +1861,11 @@ Test, Deployed, and Deprecated were considered and deferred — today's Draft/Ap
|
|
|
1888
1861
|
|
|
1889
1862
|
Lifecycle states are the language's operational-safety answer. A Disabled skill can't fire even if every author forgets it's broken; in secured mode, an Approved skill can't fire if its body was tampered post-signature, and an agent can't approve its own work. The constraint IS the safety story, here as elsewhere.
|
|
1890
1863
|
|
|
1891
|
-
## Error handling — else: blocks
|
|
1864
|
+
## Error handling — else: blocks and op-level (fallback:) values
|
|
1892
1865
|
|
|
1893
|
-
Skillscript has no try/catch — error handling is authored.
|
|
1866
|
+
Skillscript has no try/catch — error handling is authored. Two runtime mechanisms (local to global) plus a structural discipline that prevents failure outright.
|
|
1894
1867
|
|
|
1895
|
-
## Layer 1: Target-level `else:` block — the
|
|
1868
|
+
## Layer 1: Target-level `else:` block — the throw-container
|
|
1896
1869
|
|
|
1897
1870
|
Runs if any op in the target's primary body throws. Local to the failing target; downstream targets that depend on it can still proceed with whatever the `else:` branch produced. `${ERROR_CONTEXT}` (`.kind` / `.message` / `.target`) is available inside it. This is the way to contain a raised throw — including an `execute_skill` child-throw and a `$ json_parse` on malformed input (both verified v0.27.0).
|
|
1898
1871
|
|
|
@@ -1906,11 +1879,7 @@ else:
|
|
|
1906
1879
|
|
|
1907
1880
|
Distinguished from conditional `else:` (which appears after an `if:`/`elif:` chain inside a body) by the parser's scope-stack; both can coexist in a target. An `else:` block may not declare its own error handler.
|
|
1908
1881
|
|
|
1909
|
-
## Layer 2:
|
|
1910
|
-
|
|
1911
|
-
`# OnError: <fallback-skill>` is accepted and existence-checked at compile time, but it is **NOT wired in the current runtime — the named fallback skill never fires.** Do not rely on it for containment; use `else:` (or a structural guard) instead. (Tracked: wire it or remove the header in a future release.)
|
|
1912
|
-
|
|
1913
|
-
## Layer 3: Op-level `(fallback:)` value — uniform (v0.27.0+)
|
|
1882
|
+
## Layer 2: Op-level `(fallback:)` value — uniform (v0.27.0+)
|
|
1914
1883
|
|
|
1915
1884
|
Inline fallback on the op line. As of **v0.27.0 it is uniform: it contains ANY failure of that op** — a raised throw OR an empty/missing result. It works on `$` (MCP dispatch), `shell()`, `file_read`, and — since v0.27.0 — `execute_skill` (child throw) and `$ json_parse` (malformed input). On failure the fallback value binds to the output var via the same path as a success (coerce-on-bind), so downstream sees it transparently and needs no "did this fail?" check. A fired fallback is recorded in `result.fallbacks[].reason` (not `result.errors[]`) — degrade-loud.
|
|
1916
1885
|
|
|
@@ -1929,18 +1898,33 @@ $ llm prompt="..." -> VERDICT (fallback: unknown) # bare identifi
|
|
|
1929
1898
|
$ slack.post text="..." -> ACK (fallback: "post failed") # quoted string
|
|
1930
1899
|
```
|
|
1931
1900
|
|
|
1901
|
+
## Structural guard — prevent the failure
|
|
1902
|
+
|
|
1903
|
+
Pre-bind defaults (`$set`) for every var the template/downstream reads, then gate the risky op behind a `contains`/shape check (`if ${RAW|contains:"expected_key"}:`) so the throw never happens. Most robust of all — no error to contain because none is raised. See *Robustness & error containment* for the full discipline.
|
|
1904
|
+
|
|
1932
1905
|
## Choosing a mechanism
|
|
1933
1906
|
|
|
1934
|
-
- **`(fallback:)`** — "any failure of this op → this default value." Uniform; the per-op opt-out.
|
|
1907
|
+
- **`(fallback:)`** — "any failure of this op → this default value." Uniform; the per-op opt-out. Reach for it first.
|
|
1935
1908
|
- **`else:`** — target-level; when you need the error's details (`${ERROR_CONTEXT}`) to branch, or to contain a throw spanning several ops.
|
|
1936
|
-
- **Structural guard**
|
|
1937
|
-
|
|
1909
|
+
- **Structural guard** — when you can cheaply validate the shape before the risky op; prevents the failure entirely.
|
|
1910
|
+
|
|
1911
|
+
## Deprecated: `# OnError:` — parsed but not wired
|
|
1912
|
+
|
|
1913
|
+
A skill-level `# OnError: <fallback-skill>` header parses, but it is **not wired in the current runtime: the named fallback never fires.** A skill that relies on it has, in effect, no error handling — so don't use it. Use a target-level `else:` handler (or an op-level `(fallback:)`) instead. Removal of the header is planned for a post-launch release (after 0.30); until then it is inert, not a compile error. To recover at the whole-skill level, wrap the body in a target with an `else:` that dispatches your recovery skill:
|
|
1914
|
+
|
|
1915
|
+
```
|
|
1916
|
+
run:
|
|
1917
|
+
execute_skill(name="do-the-work") -> R
|
|
1918
|
+
else:
|
|
1919
|
+
execute_skill(name="recovery", inputs={"REASON": "${ERROR_CONTEXT.message}"}) -> R
|
|
1920
|
+
```
|
|
1938
1921
|
|
|
1939
1922
|
## Propagation
|
|
1940
1923
|
|
|
1941
1924
|
- Op throw → contained by a `(fallback:)` on that op if present; else by the target's `else:`; else propagates to the caller (surfaces in `result.errors[]`).
|
|
1942
1925
|
- A fired op-level `(fallback:)` is recorded in `result.fallbacks[]`, not `result.errors[]`.
|
|
1943
1926
|
- Under the hood: when a `$` op returns `isError: true` the executor throws via `makeOpError` rather than binding the error text — that throw is what routes to `(fallback:)` / `else:` and surfaces in `result.errors[]`. Op-level `(fallback:)` intercepts it and binds the fallback value instead.
|
|
1927
|
+
- **Composition caveat:** a child skill's failures do NOT bubble to the parent's top-level `result.errors[]` — they nest in the bound `${R.errors}`. A top-level `errors: []` does not mean nothing failed downstream. Inspect `${R.errors}` or rely on `(fallback:)`/`else:`, which fire regardless of nesting depth. (See Composition.)
|
|
1944
1928
|
|
|
1945
1929
|
## Robustness & error containment — best practices (no try/catch by design)
|
|
1946
1930
|
|
|
@@ -1986,23 +1970,23 @@ Bad input skips the `if`, the default stands, the child never throws — so a pa
|
|
|
1986
1970
|
|
|
1987
1971
|
Skillscript supports skill-to-skill composition via the runtime's public composition primitive. A parent skill invokes a child skill, optionally passes inputs, optionally binds the child's result. The runtime threads variable state, propagates errors, and enforces a recursion-depth guard.
|
|
1988
1972
|
|
|
1989
|
-
Composition is exposed as a runtime-intrinsic op: `execute_skill(
|
|
1973
|
+
Composition is exposed as a runtime-intrinsic op: `execute_skill(name="...", inputs={...}) -> R`. Symmetric with `compile_skill` and `lint_skill` — same surface, same naming convention, no external-namespace dependency. (`skill_name=` is an accepted back-compat alias for `name=`; prefer `name=` in new skills.)
|
|
1990
1974
|
|
|
1991
1975
|
## Surface
|
|
1992
1976
|
|
|
1993
1977
|
```
|
|
1994
1978
|
parent:
|
|
1995
|
-
execute_skill(
|
|
1979
|
+
execute_skill(name="child") -> RESULT
|
|
1996
1980
|
emit(text="Child returned: ${RESULT}")
|
|
1997
1981
|
```
|
|
1998
1982
|
|
|
1999
|
-
The runtime resolves `
|
|
1983
|
+
The runtime resolves `name` against the configured SkillStore at dispatch time, runs the child to completion against the runtime's wired connectors, and binds the result.
|
|
2000
1984
|
|
|
2001
1985
|
## Tool signature
|
|
2002
1986
|
|
|
2003
1987
|
```
|
|
2004
1988
|
execute_skill({
|
|
2005
|
-
|
|
1989
|
+
name: string, // required — resolves via SkillStore (skill_name= accepted as back-compat alias)
|
|
2006
1990
|
inputs?: Record<string,string>, // optional — Vars override map
|
|
2007
1991
|
mechanical?: boolean // optional — dry-run mode (default false)
|
|
2008
1992
|
})
|
|
@@ -2049,7 +2033,7 @@ default: fetch
|
|
|
2049
2033
|
|
|
2050
2034
|
## Semantics
|
|
2051
2035
|
|
|
2052
|
-
**Skill resolution.** Missing skills produce a clean structured error (`MissingSkillReferenceError extends OpError`) — the parent's `(fallback: ...)` discipline applies if specified, otherwise
|
|
2036
|
+
**Skill resolution.** Missing skills produce a clean structured error (`MissingSkillReferenceError extends OpError`) — the parent's `(fallback: ...)` discipline applies if specified, otherwise a target-level `else:` handler catches it if declared, otherwise the parent fails with the error propagated through.
|
|
2053
2037
|
|
|
2054
2038
|
**Input override.** `inputs` map keys must match the child's `# Vars:` declarations. Undeclared keys are ignored. Required vars without defaults must be supplied or dispatch fails before the child starts.
|
|
2055
2039
|
|
|
@@ -2063,28 +2047,27 @@ default: fetch
|
|
|
2063
2047
|
|
|
2064
2048
|
Two non-obvious behaviors when reading a child's failures from the parent (both confirmed by the v1.0 runtime-semantics test battery):
|
|
2065
2049
|
|
|
2066
|
-
- **Nested errors do NOT surface at the top level.** When a child op fails — e.g. the recursion-depth guard fires — the structured error nests inside the child's `R.errors`, which nests inside *its* parent's `R.errors`, and so on up the chain. It does **not** bubble to the caller's top-level `result.errors`. So a top-level `errors: []` does **not** mean nothing failed downstream. To detect a child failure, inspect the bound `${R.errors}` (and deeper), or rely on `(fallback: ...)` /
|
|
2050
|
+
- **Nested errors do NOT surface at the top level.** When a child op fails — e.g. the recursion-depth guard fires — the structured error nests inside the child's `R.errors`, which nests inside *its* parent's `R.errors`, and so on up the chain. It does **not** bubble to the caller's top-level `result.errors`. So a top-level `errors: []` does **not** mean nothing failed downstream. To detect a child failure, inspect the bound `${R.errors}` (and deeper), or rely on `(fallback: ...)` / `else:` — those fire on the structured error regardless of nesting depth. As of v0.27.0 the op-level `(fallback: ...)` trailer is uniform: it contains a raised `execute_skill` child-throw (recursion-guard fire, missing-skill, or any error nested in the child), so `execute_skill(...) -> R (fallback: "...")` reliably degrades on a child failure regardless of nesting depth, and the fired fallback lands in `result.fallbacks[].reason`.
|
|
2067
2051
|
- **`# Returns: R` is load-bearing for observability.** If the parent binds a child via `-> R` but does not declare `# Returns: R`, the `# Returns:` filter strips `R` from the parent's `final_vars` — and any nested child errors disappear from the parent's MCP-wire response along with it. A composition skill that wants its child's failures observable from *its own* caller must declare the binding (`# Returns: R`) explicitly. Observability of child failure is opt-in, the same way value export is.
|
|
2068
2052
|
|
|
2069
2053
|
## Forward-reference resolution
|
|
2070
2054
|
|
|
2071
|
-
Skill references (`inline(skill=...)`, `execute_skill(
|
|
2055
|
+
Skill references (`inline(skill=...)`, `execute_skill(name=...)`) are validated at compile time but allow forward references with tier-2 advisories — making it possible to author sibling skills together (chicken-and-egg).
|
|
2072
2056
|
|
|
2073
2057
|
**Lint behavior:**
|
|
2074
2058
|
- `unknown-skill-reference` (tier-2) — covers `inline()` and `execute_skill()` with missing targets
|
|
2075
2059
|
- `deferred-skill-reference` (tier-3 advisory) — teaching message: *"Skill 'X' referenced via `<op>` is not currently in the SkillStore. Will resolve at execute time if the skill exists by then, or throw `SkillNotFoundError` if not. If this is a typo, fix it now; if it's a forward reference, this advisory will clear once you store 'X'."*
|
|
2076
2060
|
|
|
2077
|
-
**Runtime behavior:** when a deferred reference still can't resolve at execute time, the runtime throws `MissingSkillReferenceError extends OpError` with structured fields (`missingSkillName`, `viaOp` for the op kind, inherited `target` and `opKind`). The error
|
|
2061
|
+
**Runtime behavior:** when a deferred reference still can't resolve at execute time, the runtime throws `MissingSkillReferenceError extends OpError` with structured fields (`missingSkillName`, `viaOp` for the op kind, inherited `target` and `opKind`). The error is containable by a target-level `else:` handler (or an op-level `(fallback: ...)`) naturally.
|
|
2078
2062
|
|
|
2079
2063
|
**Stronger contracts kept tier-1:**
|
|
2080
|
-
- `# OnError: <missing>` — error-handler missing-at-runtime is the worst possible UX moment to discover a missing reference; explicit at compile is the right call.
|
|
2081
2064
|
- `disabled-skill-reference` — pointing at a Disabled skill is a stronger contract than "missing yet to be authored"; explicit at compile.
|
|
2082
2065
|
|
|
2083
2066
|
## When to use composition vs other primitives
|
|
2084
2067
|
|
|
2085
2068
|
Three distinct cases that look similar but have different intents:
|
|
2086
2069
|
|
|
2087
|
-
1. **Get a value back from another skill.** Use `execute_skill(
|
|
2070
|
+
1. **Get a value back from another skill.** Use `execute_skill(name="...") -> RESULT` and use `${RESULT}` locally — reaching declared returns (`${RESULT.X}`) or the output stream (`${RESULT.outputs.text}`). This is the composition primitive case.
|
|
2088
2071
|
|
|
2089
2072
|
2. **Delegate work to an agent as a task.** Use `# Output: template: <agent>` to route a compiled artifact through AgentConnector. The receiving agent acts on the prompt. *This is the Template-skill story* — uses compile-as-delivery, not execute-and-bind.
|
|
2090
2073
|
|
|
@@ -2112,7 +2095,7 @@ default: greet
|
|
|
2112
2095
|
# Status: Approved
|
|
2113
2096
|
|
|
2114
2097
|
call_greeting:
|
|
2115
|
-
execute_skill(
|
|
2098
|
+
execute_skill(name="greeting") -> GREETING_RESULT
|
|
2116
2099
|
emit(text="Greeting skill said: ${GREETING_RESULT.outputs.text}")
|
|
2117
2100
|
|
|
2118
2101
|
default: call_greeting
|
|
@@ -2141,7 +2124,7 @@ default: classify
|
|
|
2141
2124
|
# Vars: INPUT="some text"
|
|
2142
2125
|
|
|
2143
2126
|
call_with_inputs:
|
|
2144
|
-
execute_skill(
|
|
2127
|
+
execute_skill(name="classifier", inputs={"TEXT": "${INPUT}"}) -> R
|
|
2145
2128
|
emit(text="Classified as: ${R.VERDICT}")
|
|
2146
2129
|
|
|
2147
2130
|
default: call_with_inputs
|
|
@@ -2154,17 +2137,19 @@ default: call_with_inputs
|
|
|
2154
2137
|
# Status: Approved
|
|
2155
2138
|
|
|
2156
2139
|
call_maybe_missing:
|
|
2157
|
-
execute_skill(
|
|
2140
|
+
execute_skill(name="might-not-exist") -> RESULT (fallback: "child unavailable")
|
|
2158
2141
|
emit(text="Result: ${RESULT.outputs.text}")
|
|
2159
2142
|
|
|
2160
2143
|
default: call_maybe_missing
|
|
2161
2144
|
```
|
|
2162
2145
|
|
|
2146
|
+
As of v0.27.0 the `(fallback:)` trailer is uniform, so it contains an `execute_skill` child-throw (missing-skill, recursion-guard fire, or any error raised inside the child) as well as a missing/empty bind — `RESULT` degrades to `"child unavailable"` on any of those, and the fired fallback is recorded in `result.fallbacks[].reason`.
|
|
2147
|
+
|
|
2163
2148
|
**TestFlight preview (from the runtime caller, not from inside a skill):**
|
|
2164
2149
|
|
|
2165
2150
|
```
|
|
2166
2151
|
execute_skill({
|
|
2167
|
-
|
|
2152
|
+
name: "parent",
|
|
2168
2153
|
mechanical: true
|
|
2169
2154
|
})
|
|
2170
2155
|
```
|
|
@@ -2182,7 +2167,7 @@ For *data skills* (skills marked `# Type: data`), the compile-time inline primit
|
|
|
2182
2167
|
- Treat composition as a real cost. Each `execute_skill()` dispatch incurs the child's full execution time + side effects. Don't compose for trivial cases that could be inlined.
|
|
2183
2168
|
- Declare `# Returns:` when a caller needs structured access to a child's variables. Leave it off for emit-only skills whose consumers read `.outputs.text` — the default-empty filter keeps scratch from propagating.
|
|
2184
2169
|
- Want a child's failures visible to your caller? Declare the child binding in `# Returns:` — nested errors are filtered out with the binding otherwise (see Error surfacing in composition), and don't trust a top-level `errors: []`.
|
|
2185
|
-
- Pair composition with `(fallback: ...)` when the child skill might fail and the parent has a sensible degraded path.
|
|
2170
|
+
- Pair composition with `(fallback: ...)` when the child skill might fail and the parent has a sensible degraded path. As of v0.27.0 the `(fallback:)` catches a raised child throw, not just a missing bind.
|
|
2186
2171
|
- Use mechanical mode to TestFlight any multi-skill chain before shipping it as a Headless skill on a cron trigger.
|
|
2187
2172
|
- Forward references work — author sibling skills in any order, validate independently. The tier-2 warning surfaces the deferred-resolution path; runtime catches genuine misses.
|
|
2188
2173
|
- Recursion is legal but bounded. If your design requires deeper recursion than the configured limit, reshape the workflow — almost always a sign of an iteration that should be expressed as `foreach` rather than recursion.
|
|
@@ -2190,18 +2175,6 @@ For *data skills* (skills marked `# Type: data`), the compile-time inline primit
|
|
|
2190
2175
|
## Session isolation — a skill can't mutate the calling agent's session
|
|
2191
2176
|
A skill executes in its OWN session; agent session state (context, persona) is session-local. Calling `amp_set_session_context` (or similar) inside a skill sets it for the SKILL's session, not the caller's — the caller sees no change. The contract: **skills assemble and RETURN data; the agent owns its SESSION STATE.** To "enter a project," a skill returns the instruction bodies and the AGENT applies its own session context.
|
|
2192
2177
|
|
|
2193
|
-
## Current-runtime corrections (v0.27.0)
|
|
2194
|
-
|
|
2195
|
-
This section predates the v0.27.0 error-containment model. Corrections to how it describes failure handling:
|
|
2196
|
-
|
|
2197
|
-
1. **`# OnError:` is parsed but INERT — it never fires.** Everywhere this section says a "`# OnError:` fallback fires if declared" (skill resolution, `MissingSkillReferenceError` handling, the deferred-reference runtime path, the fallback-chain in Error surfacing), that is aspirational, not current behavior. The `fallbackSkillExecutor` is declared but never wired. For reliable throw-containment of a child failure, use the target-level `else:` handler (`${ERROR_CONTEXT}` available) or the op-level `(fallback: ...)` trailer. Read every "`# OnError:`" in this section as "`else:`".
|
|
2198
|
-
|
|
2199
|
-
2. **`(fallback: ...)` is uniform (v0.27.0+).** The op-trailer now contains ANY op failure — a missing/unresolved value OR a raised throw — including an `execute_skill` child-throw (recursion-guard fire, missing-skill, or any error nested in the child). So `execute_skill(...) -> R (fallback: "child unavailable")` reliably degrades on a child failure regardless of nesting depth, and the fired fallback lands in `result.fallbacks[].reason`. This strengthens the "Pair composition with `(fallback: ...)`" guidance: it now works against raised child errors, not just a missing bind.
|
|
2200
|
-
|
|
2201
|
-
3. **`execute_skill` canonical kwarg is `name=`.** Examples here use `skill_name=`, which remains a back-compat alias and still works. New authoring should prefer `execute_skill(name="child", inputs={...})`.
|
|
2202
|
-
|
|
2203
|
-
The "Nested errors do NOT surface at the top level" observation remains accurate and important — inspect the bound `${R.errors}` or rely on `(fallback:)`/`else:`; a top-level `errors: []` does not mean nothing failed downstream.
|
|
2204
|
-
|
|
2205
2178
|
## Static vs Dynamic — skill execution model
|
|
2206
2179
|
|
|
2207
2180
|
Orthogonal to the three skill categories (Headless / Augmenting / Template, which describe the skill's relationship to the frontier agent), every skill has an *execution model* that describes its relationship to the Skillscript runtime.
|
|
@@ -2323,7 +2296,6 @@ Normal `prompt` / `prose` compilation ignores the `# Tests:` section entirely
|
|
|
2323
2296
|
### Runtime assertions (for `format: "test"` execution)
|
|
2324
2297
|
|
|
2325
2298
|
- `target_else_executed: "<target_name>"` — verifies the `else:` branch ran
|
|
2326
|
-
- `onerror_invoked: "<fallback_skill>"` — verifies the `# OnError:` skill was called
|
|
2327
2299
|
- `op_fallback_used: "<target.op_index>"` — verifies an op-level fallback value was substituted
|
|
2328
2300
|
- `result_value: "<expected_string>"` — the skill's final output value
|
|
2329
2301
|
|
|
@@ -2566,12 +2538,12 @@ Scopes: skill-local (persists across fires of this skill, not visible to other s
|
|
|
2566
2538
|
|
|
2567
2539
|
## Time as first-class primitives
|
|
2568
2540
|
|
|
2569
|
-
Currently `$
|
|
2541
|
+
Currently `${NOW}` (wall-clock). Planned relative-time primitives:
|
|
2570
2542
|
|
|
2571
2543
|
```
|
|
2572
|
-
$
|
|
2573
|
-
$
|
|
2574
|
-
$
|
|
2544
|
+
${SECONDS_SINCE_LAST_USER_MESSAGE}
|
|
2545
|
+
${MINUTES_SINCE_SESSION_START}
|
|
2546
|
+
${SECONDS_SINCE_LAST_FIRE_OF.<skill-name>}
|
|
2575
2547
|
```
|
|
2576
2548
|
|
|
2577
2549
|
Most "right time" reasoning is relative, not wall-clock.
|
|
@@ -2585,17 +2557,13 @@ Most "right time" reasoning is relative, not wall-clock.
|
|
|
2585
2557
|
- **Cross-skill pub-sub** — `# Publishes: signal.X` / `# Subscribes: signal.Y` decoupling
|
|
2586
2558
|
- **Confidence/threshold gating** — `# RequiresConfidence: classifier >= 0.8` / `# RequiresThreshold:`
|
|
2587
2559
|
- **Invocation-control axis** — `# Invocable-By: user | agent | trigger` (sensitive ops shouldn't leak across invocation boundaries)
|
|
2588
|
-
- **Introspection primitives** — `$
|
|
2560
|
+
- **Introspection primitives** — `${PROMPT_CONTEXT.size}`, `${SKILLS_FIRED_RECENTLY.last-1h}`, `${SELF.confidence-trend}`
|
|
2589
2561
|
|
|
2590
2562
|
## When the language extends, this section shrinks
|
|
2591
2563
|
|
|
2592
2564
|
When any of these primitives ship, the relevant grammar moves into its canonical section (Ops reference, Variables, Triggers, etc.) and the entry here is removed. This section stays alive as a continuous staging area for the next horizon of unshipped work.
|
|
2593
2565
|
|
|
2594
|
-
## Notation correction
|
|
2595
|
-
|
|
2596
|
-
The planned time-primitive and introspection examples in this section are written with the `$(NAME)` paren sigil (`$(NOW)`, `$(SECONDS_SINCE_LAST_USER_MESSAGE)`, `$(PROMPT_CONTEXT.size)`, etc.). Skillscript's substitution sigil is `${...}` (brace) — the paren form is reserved to avoid bash `$(command)` collision (see Variable resolution). When these primitives ship they will use the brace form (`${NOW}`, `${SECONDS_SINCE_LAST_USER_MESSAGE}`), consistent with the Future-grammar section which already writes them correctly. Read the paren examples here as brace form.
|
|
2597
|
-
|
|
2598
2566
|
---
|
|
2599
2567
|
|
|
2600
|
-
*Rendered from `skillscript/skillscript-language-reference` — 2026-07-
|
|
2568
|
+
*Rendered from `skillscript/skillscript-language-reference` — 2026-07-10 13:59 EDT*
|
|
2601
2569
|
*Source of truth: AMP (`amp_render_document("skillscript/skillscript-language-reference")`)*
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "skillscript-runtime",
|
|
3
|
-
"version": "0.27.
|
|
3
|
+
"version": "0.27.3",
|
|
4
4
|
"description": "Runtime, compiler, lint, CLI, and dashboard for Skillscript — a small declarative language for authoring agent workflows.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Scott Shwarts <scotts@pobox.com>",
|