skillscript-runtime 0.33.0 → 0.34.0
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 +13 -0
- package/dist/connectors/index.d.ts +1 -1
- package/dist/connectors/index.d.ts.map +1 -1
- package/dist/connectors/index.js.map +1 -1
- package/dist/runtime.d.ts.map +1 -1
- package/dist/runtime.js +29 -3
- package/dist/runtime.js.map +1 -1
- package/docs/adopter-playbook.md +11 -0
- package/docs/connector-contract-reference.md +38 -0
- package/docs/language-reference.md +77 -47
- package/examples/connectors/README.md +2 -2
- package/examples/connectors/RestConnector/.env.example +19 -0
- package/examples/connectors/RestConnector/README.md +109 -0
- package/examples/connectors/RestConnector/RestConnector.ts +326 -0
- 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
|
@@ -6,7 +6,7 @@ mode: wide
|
|
|
6
6
|
|
|
7
7
|
Canonical language reference for skillscript. Audience: skill authors (human + agent). Specifies what is valid syntax, what behavior to expect at compile + runtime, and what is currently pending implementation.
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
It describes the current runtime; release history lives in the CHANGELOG. Items not yet implemented are called out in the relevant sections.
|
|
10
10
|
|
|
11
11
|
|
|
12
12
|
## Overview & language model
|
|
@@ -29,11 +29,23 @@ Skillscript's job is to express this pipeline declaratively. When there is an ag
|
|
|
29
29
|
|
|
30
30
|
## Two execution paths
|
|
31
31
|
|
|
32
|
-
**
|
|
32
|
+
The two paths differ by **who executes the ops** — the runtime, or an agent — *not* by who invokes the skill. Getting this backwards is the classic trap: an agent calling a skill mid-conversation still gets deterministic runtime execution. Determinism is a property of the execution path, not of the caller's context.
|
|
33
33
|
|
|
34
|
-
**
|
|
34
|
+
**Runtime-mediated** — the interpreter walks the ops and dispatches them through configured connectors, returning a completed, deterministic result. This is *every actual execution*, regardless of caller: autonomous cron/event fires, the CLI execute command, `execute_skill` over MCP (an agent invoking a stored skill mid-conversation — the common case), in-skill `$ execute_skill` composition, and `inline(skill=...)`. If a skill *runs*, it runs here. Safety boundary is the connector config + per-op gating (see Ops Reference).
|
|
35
35
|
|
|
36
|
-
|
|
36
|
+
**Agent-mediated** — *not* an execution path in the "runs the ops" sense. It's the **compile/preview** path: `compile_skill` renders the skill as a prompt (no side effects, nothing executed), and an agent may then read that prompt and carry out the steps through its *own* tools. Determinism is not guaranteed here, because the runtime never touches the ops. Safety boundary is the agent's harness tool permissions.
|
|
37
|
+
|
|
38
|
+
**Neither of those is `# Output: agent:`.** That's a *delivery* target — a runtime-executed (deterministic) skill handing its rendered output to an agent for its next turn. "Agent" there names who *receives* the result, not who executes the ops.
|
|
39
|
+
|
|
40
|
+
### Which call gets which path
|
|
41
|
+
|
|
42
|
+
| Call | Path | Result |
|
|
43
|
+
|---|---|---|
|
|
44
|
+
| `execute_skill({name})` (MCP), CLI execute, cron/event fire, `$ execute_skill`, `inline(skill=…)` | Runtime-mediated | The runtime walks the ops and returns the finished result. **Deterministic.** |
|
|
45
|
+
| `compile_skill({name})` | Agent-mediated | Returns the rendered plan/prompt for inspection. No side effects, nothing executed. |
|
|
46
|
+
| `# Output: agent: <name>` | Runtime-mediated + push delivery | A runtime-executed skill pushes its rendered output to an agent. "Agent" is the delivery target, not the executor. |
|
|
47
|
+
|
|
48
|
+
The language is identical across paths. Which path applies is a deployment-time + invocation-time decision — but **execution determinism is a property of runtime dispatch (`execute_skill` and friends), never of the caller's context.** That is the guarantee the runtime makes, and the reason `execute_skill` mid-conversation is a deterministic replacement for ad-hoc agent tool-sequencing.
|
|
37
49
|
|
|
38
50
|
## Output production and delivery channels
|
|
39
51
|
|
|
@@ -301,9 +313,9 @@ Closed list of language-intrinsic ops the runtime knows directly. Each is a func
|
|
|
301
313
|
| `emit` | `emit(text="...")` | none | Append to the skill's emission stream; consumed by the configured `# Output:` delivery channel. |
|
|
302
314
|
| `notify` | `notify(agent="...", message="...", [event_type=...], [correlation_id=...]) -> ACK` | optional | Mid-skill agent alert; synchronous send via configured AgentConnector. |
|
|
303
315
|
| `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(name="...", inputs
|
|
316
|
+
| `execute_skill` | `execute_skill(name="...", inputs={...}) -> R` | optional | Composition primitive. Runtime-resolved. `skill_name=` accepted as back-compat alias. See Composition section. |
|
|
305
317
|
| `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
|
-
| `file_read` | `file_read(path="...") -> R` | required | Read a file at `path`; binds string contents. Optional `encoding="utf8"
|
|
318
|
+
| `file_read` | `file_read(path="...") -> R` | required | Read a file at `path`; binds string contents. Optional `encoding="utf8"|"base64"` kwarg (default `utf8`). |
|
|
307
319
|
| `file_write` | `file_write(path="...", content="...")` | none | Write `content` to `path`. `mkdir -p` semantics for parent directories. Mutation-classified. |
|
|
308
320
|
|
|
309
321
|
**Unknown op name** → tier-1 lint `unknown-runtime-op` with remediation pointing at MCP dispatch: "if this is an external tool, use `$ <connector>.<tool> args -> R`."
|
|
@@ -517,7 +529,7 @@ $ youtrack.run_report report_id="INFRA-weekly" timeout=120 -> DISPATCH
|
|
|
517
529
|
$ data_write content="${REPORT}" tags=["oncall"] approved="morning roundup" -> ACK
|
|
518
530
|
```
|
|
519
531
|
|
|
520
|
-
**`(fallback: "value")` trailer** — Uniform
|
|
532
|
+
**`(fallback: "value")` trailer** — Uniform: 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
533
|
|
|
522
534
|
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
535
|
|
|
@@ -664,14 +676,14 @@ deliver:
|
|
|
664
676
|
|
|
665
677
|
| Class | Op | Shape | Binding |
|
|
666
678
|
|---|---|---|---|
|
|
667
|
-
| Mutation | `$set` | `$set NAME = value` (with
|
|
679
|
+
| Mutation | `$set` | `$set NAME = value` (with `${VAR}` interpolation at bind) | NAME (no arrow) |
|
|
668
680
|
| Mutation | `$append` | `$append VAR <value>` (type-dispatched: list element / string concat) | VAR (no arrow) |
|
|
669
681
|
| Runtime-intrinsic | `emit` | `emit(text="...")` or `emit(text="""...""")` for multi-line | none |
|
|
670
682
|
| Runtime-intrinsic | `notify` | `notify(agent="...", [message=...], [event_type=...], [correlation_id=...]) -> ACK` | optional |
|
|
671
683
|
| Runtime-intrinsic | `inline` | `inline(skill="<name>")` | none (compile-time) |
|
|
672
|
-
| Runtime-intrinsic | `execute_skill` | `execute_skill(name="...", inputs
|
|
684
|
+
| Runtime-intrinsic | `execute_skill` | `execute_skill(name="...", inputs={...}) -> R` (`skill_name=` back-compat alias) | optional |
|
|
673
685
|
| 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
|
-
| Runtime-intrinsic | `file_read` | `file_read(path="...", [encoding="utf8"
|
|
686
|
+
| Runtime-intrinsic | `file_read` | `file_read(path="...", [encoding="utf8"|"base64"]) -> R` | required |
|
|
675
687
|
| Runtime-intrinsic | `file_write` | `file_write(path="...", content="...", [approved="..."])` | none |
|
|
676
688
|
| External MCP — substrate-specific | `$ <connector>.<tool>` | `$ <connector>.<tool> kwarg=value, ... [timeout=N] [approved="..."] [-> R] [(fallback: "...")]` | optional |
|
|
677
689
|
| External MCP — typed-contract | `$ <tool>` | `$ <tool> kwarg=value, ... [timeout=N] [approved="..."] [-> R] [(fallback: "...")]` (typed-contract ops only: `data_*`, `skill_*`, `json_parse`, `llm`) | optional |
|
|
@@ -698,13 +710,13 @@ Injected automatically at runtime; never declared by the author.
|
|
|
698
710
|
|
|
699
711
|
| Var | Value |
|
|
700
712
|
|-----|-------|
|
|
701
|
-
|
|
|
702
|
-
|
|
|
703
|
-
|
|
|
704
|
-
|
|
|
705
|
-
|
|
|
706
|
-
|
|
|
707
|
-
|
|
|
713
|
+
| `${NOW}` | ISO-8601 timestamp at op-dispatch time |
|
|
714
|
+
| `${USER}` | The configured user identity |
|
|
715
|
+
| `${SESSION_CONTEXT}` | Current session-scope context (project/entity/etc., substrate-defined) |
|
|
716
|
+
| `${TRIGGER_TYPE}` | What event fired this skill |
|
|
717
|
+
| `${TRIGGER_PAYLOAD}` | Event-specific data |
|
|
718
|
+
| `${EVENT.*}` | Event-payload fields populated by the trigger source |
|
|
719
|
+
| `${ERROR_CONTEXT}` | Inside a target's `else:` error-handler: kind + message + target of the failure. |
|
|
708
720
|
|
|
709
721
|
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.
|
|
710
722
|
|
|
@@ -886,14 +898,14 @@ Pipe filters apply transforms to resolved variables before substitution. Syntax:
|
|
|
886
898
|
|
|
887
899
|
| Filter | Effect | Example | Output |
|
|
888
900
|
|--------|--------|---------|--------|
|
|
889
|
-
| `url` | `encodeURIComponent(value)` |
|
|
890
|
-
| `shell` | POSIX single-quote escape with outer quotes |
|
|
891
|
-
| `json` | `JSON.stringify(value)` |
|
|
892
|
-
| `trim` | Whitespace trim |
|
|
893
|
-
| `length` | Count of items (array) or characters (string) |
|
|
894
|
-
| `contains:"X"` | Boolean: type-aware substring / element membership |
|
|
895
|
-
| `fallback:"X"` | Coalesce on missing/undefined/empty ref |
|
|
896
|
-
| `isodate` | Epoch seconds → ISO-8601 timestamp |
|
|
901
|
+
| `url` | `encodeURIComponent(value)` | `${location|url}` for `"Asheville, NC"` | `Asheville%2C%20NC` |
|
|
902
|
+
| `shell` | POSIX single-quote escape with outer quotes | `${arg|shell}` for `it's safe` | `'it'\''s safe'` |
|
|
903
|
+
| `json` | `JSON.stringify(value)` | `${payload|json}` for `{k:"v"}` | `"{\"k\":\"v\"}"` |
|
|
904
|
+
| `trim` | Whitespace trim | `${VERDICT|trim}` for `"urgent\n"` | `urgent` |
|
|
905
|
+
| `length` | Count of items (array) or characters (string) | `${ITEMS|length}` for `["a","b","c"]` | `3` |
|
|
906
|
+
| `contains:"X"` | Boolean: type-aware substring / element membership | `${MSG|contains:"urgent"}` for `"Yes, urgent"` | `true` |
|
|
907
|
+
| `fallback:"X"` | Coalesce on missing/undefined/empty ref | `${VAR.missing|fallback:"-"}` | `-` |
|
|
908
|
+
| `isodate` | Epoch seconds → ISO-8601 timestamp | `${EPOCH|isodate}` for `1779660000` | `2026-05-24T22:00:00.000Z` |
|
|
897
909
|
|
|
898
910
|
### `length` semantics
|
|
899
911
|
|
|
@@ -954,7 +966,7 @@ emit:
|
|
|
954
966
|
emit(text="nested: ${ISSUE.customFields.Assignee|fallback:\"unassigned\"}")
|
|
955
967
|
```
|
|
956
968
|
|
|
957
|
-
**Order-independent in a filter chain
|
|
969
|
+
**Order-independent in a filter chain.** 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
970
|
|
|
959
971
|
**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.
|
|
960
972
|
|
|
@@ -1037,7 +1049,7 @@ Several filters are planned but not yet shipped:
|
|
|
1037
1049
|
| `summary` | One-line abbreviation | Compress for human-facing emissions |
|
|
1038
1050
|
| `pluck:<field>` | Project array of objects to array of field values | Paired with `in`/`not in` for dedup-by-id workflows |
|
|
1039
1051
|
| `join:"<sep>"` | List → string with separator | Filter-shape alternative to string `$append`; reconsider if filter-chain demand surfaces |
|
|
1040
|
-
| `isodate_ms` | Epoch ms → ISO-8601 | Companion to
|
|
1052
|
+
| `isodate_ms` | Epoch ms → ISO-8601 | Companion to `|isodate`; defer until demand |
|
|
1041
1053
|
|
|
1042
1054
|
`pluck` is the highest-priority remaining filter — it closes the structural-dedup gap for skills that iterate retrieval results and want to exclude already-seen items by ID without manual comparison loops.
|
|
1043
1055
|
|
|
@@ -1261,7 +1273,7 @@ run:
|
|
|
1261
1273
|
foreach D in ${DS}:
|
|
1262
1274
|
...
|
|
1263
1275
|
```
|
|
1264
|
-
An empty list (`DOMAINS='[]'`, or an empty/whitespace string) iterates zero times
|
|
1276
|
+
An empty list (`DOMAINS='[]'`, or an empty/whitespace string) iterates zero times. The comma-split affordance is for `# Vars:` DEFAULTS only, not runtime inputs — there is no `|split` filter (tracked as a DX enhancement).
|
|
1265
1277
|
|
|
1266
1278
|
## Triggers — # Triggers: header, declarative + imperative registration, source types
|
|
1267
1279
|
|
|
@@ -1798,6 +1810,14 @@ interface McpDispatchCtx {
|
|
|
1798
1810
|
|
|
1799
1811
|
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.
|
|
1800
1812
|
|
|
1813
|
+
## Writing a non-MCP connector — "MCP" names the verb, not the wire
|
|
1814
|
+
|
|
1815
|
+
`McpConnector` is the dispatch surface for `$ connector.tool` ops; the name refers to the skill-facing verb, **not** a wire-protocol requirement. The contract is just `call(toolName, args, ctxOverrides?)` plus an optional `describeTools()` — which surfaces the connector's tool set (inputSchema per tool) for authoring and lint. MCP JSON-RPC framing is `HttpMcpConnector`'s implementation detail, not part of the interface.
|
|
1816
|
+
|
|
1817
|
+
So a backend that speaks plain REST is first-class: `class RestConnector implements McpConnector` whose `call()` maps `tool + kwargs` to an HTTPS request (path-templating, query-vs-body routing, auth header) gets the full typed-contract surface — closed tool set for lint, per-tool kwarg validation, `$`-prefix state-affecting gating — with zero MCP framing. MCP and REST connectors coexist freely in `connectors.json`; a single skill body can use `$ gmail.send` (MCP) and `$ tickets.create` (REST) side by side.
|
|
1818
|
+
|
|
1819
|
+
Full contract spec + a runnable worked example: see `connector-contract-reference.md` (McpConnector section) and `examples/connectors/RestConnector/`.
|
|
1820
|
+
|
|
1801
1821
|
## Lifecycle and status — # Status: header, three canonical states (Draft / Approved / Disabled), compile + runtime enforcement
|
|
1802
1822
|
|
|
1803
1823
|
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.
|
|
@@ -1861,13 +1881,29 @@ Test, Deployed, and Deprecated were considered and deferred — today's Draft/Ap
|
|
|
1861
1881
|
|
|
1862
1882
|
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.
|
|
1863
1883
|
|
|
1884
|
+
## Frontmatter tags — # Tags: classification metadata
|
|
1885
|
+
|
|
1886
|
+
`# Tags:` is optional frontmatter for **skill classification** — a comma-separated list of free-form labels used to organize and filter skills. It has no effect on execution, dispatch, or approval.
|
|
1887
|
+
|
|
1888
|
+
```
|
|
1889
|
+
# Tags: skill-classification, email, deterministic
|
|
1890
|
+
```
|
|
1891
|
+
|
|
1892
|
+
**Parsing.** The comma-list parses to a `tags: string[]` on the parsed skill. Omitted → empty list (`[]`), never null; every skill carries a `tags` array.
|
|
1893
|
+
|
|
1894
|
+
**Approval-neutral.** Tags are excluded from the approval signing hash — the signature is computed over the canonicalized body with the `# Tags:` line stripped, the same treatment as `# Status:`. Editing a skill's tags therefore does **not** invalidate its approval or force re-approval. Nothing security-relevant reads tags; they are organizational metadata only.
|
|
1895
|
+
|
|
1896
|
+
**Runtime-derived, surfaced on the contract.** Like `# Description:` and `# Vars:`, the runtime derives tags from parsed frontmatter, so a `SkillStore` that omits a dedicated tags field needs no change. Tags surface as `SkillMeta.tags` and on the `skill_list` / `skill_preflight` payloads (empty `[]` when untagged). Authoring surfaces (e.g. the dashboard) facet the skills list by tag — filter chips plus per-row pills.
|
|
1897
|
+
|
|
1898
|
+
**When to use.** Group related skills for discovery in a growing library (by domain, by delivery kind, by team). Tags complement the `# Description:` trigger-condition discipline: description drives *invocation selection*, tags drive *organization and filtering*.
|
|
1899
|
+
|
|
1864
1900
|
## Error handling — else: blocks and op-level (fallback:) values
|
|
1865
1901
|
|
|
1866
1902
|
Skillscript has no try/catch — error handling is authored. Two runtime mechanisms (local to global) plus a structural discipline that prevents failure outright.
|
|
1867
1903
|
|
|
1868
1904
|
## Layer 1: Target-level `else:` block — the throw-container
|
|
1869
1905
|
|
|
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
|
|
1906
|
+
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.
|
|
1871
1907
|
|
|
1872
1908
|
```
|
|
1873
1909
|
fetch:
|
|
@@ -1879,9 +1915,9 @@ else:
|
|
|
1879
1915
|
|
|
1880
1916
|
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.
|
|
1881
1917
|
|
|
1882
|
-
## Layer 2: Op-level `(fallback:)` value — uniform
|
|
1918
|
+
## Layer 2: Op-level `(fallback:)` value — uniform
|
|
1883
1919
|
|
|
1884
|
-
Inline fallback on the op line.
|
|
1920
|
+
Inline fallback on the op line. 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`, `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.
|
|
1885
1921
|
|
|
1886
1922
|
```
|
|
1887
1923
|
weather:
|
|
@@ -1910,7 +1946,7 @@ Pre-bind defaults (`$set`) for every var the template/downstream reads, then gat
|
|
|
1910
1946
|
|
|
1911
1947
|
## Deprecated: `# OnError:` — parsed but not wired
|
|
1912
1948
|
|
|
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.
|
|
1949
|
+
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. The header is inert, not a compile error; removal is planned. To recover at the whole-skill level, wrap the body in a target with an `else:` that dispatches your recovery skill:
|
|
1914
1950
|
|
|
1915
1951
|
```
|
|
1916
1952
|
run:
|
|
@@ -1930,14 +1966,14 @@ else:
|
|
|
1930
1966
|
|
|
1931
1967
|
**Skillscript has no try/catch — by design. Robustness is authored, not caught.** An unguarded fallible op that fails aborts the whole target — and in a fan-out, aborts every sibling that hadn't run yet. You contain failures explicitly.
|
|
1932
1968
|
|
|
1933
|
-
**One uniform rule
|
|
1969
|
+
**One uniform rule: `(fallback:)` contains ANY op failure.** A `-> VAR (fallback: "<default>")` trailer catches whatever goes wrong with that op — a `$`/`shell` dispatch error, a `shell` spawn-fail/timeout, an empty result, AND a raised throw (`$ json_parse` on off-shape input, `execute_skill` whose child throws). The op degrades to the fallback value and the target continues.
|
|
1934
1970
|
|
|
1935
1971
|
**The containment tools — pick by how much you want to handle:**
|
|
1936
1972
|
| Tool | Catches | Scope |
|
|
1937
1973
|
|---|---|---|
|
|
1938
1974
|
| `(fallback: "…")` op trailer | ANY failure of that op — dispatch error / spawn-fail / timeout / empty result / raised throw | the single op |
|
|
1939
|
-
|
|
|
1940
|
-
| `else:` block | a raised throw anywhere in the target body, WITH error context (
|
|
1975
|
+
| `${ref\|fallback:"x"}` filter | a missing / unresolved / empty value at use-time | the single reference |
|
|
1976
|
+
| `else:` block | a raised throw anywhere in the target body, WITH error context (`${ERROR_CONTEXT.kind/.message/.target}`) | the whole target |
|
|
1941
1977
|
| structural guard (pre-bind defaults + a `contains`/shape check before the risky op) | PREVENTS the failure — the op isn't reached on bad input, so it can't fail | the risky op |
|
|
1942
1978
|
| `# OnError: <skill>` | INERT in the current runtime (fallbackSkillExecutor never wired) — do NOT rely on it; prefer `else:` | — |
|
|
1943
1979
|
|
|
@@ -1958,14 +1994,12 @@ default: fetch
|
|
|
1958
1994
|
```
|
|
1959
1995
|
Bad input skips the `if`, the default stands, the child never throws — so a parent gather can't be sunk by it (independent of the parent's own fallback).
|
|
1960
1996
|
|
|
1961
|
-
**Rule 3 — a `fallback` filter anywhere in a chain rescues an unresolved reference** (order-independent
|
|
1997
|
+
**Rule 3 — a `fallback` filter anywhere in a chain rescues an unresolved reference** (order-independent; also rescues a missing dotted/numeric path on a present object — `${W.a.0.b|fallback:"d"}` when `W` lacks `a` → `"d"`). A chain with NO `fallback` still throws on an unresolved ref — that's your signal to add one. It rescues only genuinely-unresolved refs; a present-but-empty value (`" "`) still flows through the other filters, so `${x|length|fallback:"0"}` on `" "` is `"2"`.
|
|
1962
1998
|
|
|
1963
1999
|
**Rule 4 — A body-text output template must not reference a var a fallible step might leave unset.** The template renders after the target runs; an unset var hard-fails the render (`Unresolved variable reference: $(AREA)`) — and that is exactly how a child skill throws OUT to its `execute_skill` parent. Pre-bind every template var to a default before the fallible step (this doubles as the throw-prevention structure in Rule 2), or source each with a `|fallback`.
|
|
1964
2000
|
|
|
1965
2001
|
**Rule 5 — Degrade LOUD, not silent.** A fallback value should be a visible marker ("unavailable", "—", "n/a") — never empty, never a plausible-but-wrong value. A degraded run must be diagnosable: the throw's reason is preserved in `result.fallbacks[].reason`, so a degraded leg stays traceable. Don't silently ship a blank or a fake reading. Clean-degrade beats both a hard abort and a silent lie.
|
|
1966
2002
|
|
|
1967
|
-
**Legacy note (≤0.26.x runtimes):** before v0.27.0, `(fallback:)` did NOT catch a raised throw from `execute_skill` or `$ json_parse` (they bypassed op.fallback). On an older runtime, contain those two with `else:` or a structural guard — a `(fallback:)` on them is a no-op against a throw. v0.27.0+ unifies it, so this only matters if you must support an old runtime.
|
|
1968
|
-
|
|
1969
2003
|
## Composition — skills calling skills
|
|
1970
2004
|
|
|
1971
2005
|
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.
|
|
@@ -2027,10 +2061,6 @@ default: fetch
|
|
|
2027
2061
|
|
|
2028
2062
|
**Top-level MCP result is filtered too.** A direct `execute_skill` MCP call of a no-`# Returns:` skill returns `final_vars: {}` — the filter applies to direct invocation, not just child-propagation. Adopters inspecting `final_vars` via the MCP tool see the declared-returns surface, not the full variable dump.
|
|
2029
2063
|
|
|
2030
|
-
**Lint:**
|
|
2031
|
-
- `unknown-returns-ref` (tier-1) — `# Returns: X` where `X` isn't bound anywhere in the skill body. Same shape as undeclared-var, for the export side.
|
|
2032
|
-
- `unexported-final-var-access` (tier-2 advisory) — caller accesses `${R.X}` where `X` isn't in the called skill's `# Returns:`. Catches the "forgot to export it" footgun (forward-reference deferred-resolution if the called skill isn't yet stored).
|
|
2033
|
-
|
|
2034
2064
|
## Semantics
|
|
2035
2065
|
|
|
2036
2066
|
**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.
|
|
@@ -2047,7 +2077,7 @@ default: fetch
|
|
|
2047
2077
|
|
|
2048
2078
|
Two non-obvious behaviors when reading a child's failures from the parent (both confirmed by the v1.0 runtime-semantics test battery):
|
|
2049
2079
|
|
|
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.
|
|
2080
|
+
- **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. 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`.
|
|
2051
2081
|
- **`# 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.
|
|
2052
2082
|
|
|
2053
2083
|
## Forward-reference resolution
|
|
@@ -2143,7 +2173,7 @@ call_maybe_missing:
|
|
|
2143
2173
|
default: call_maybe_missing
|
|
2144
2174
|
```
|
|
2145
2175
|
|
|
2146
|
-
|
|
2176
|
+
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
2177
|
|
|
2148
2178
|
**TestFlight preview (from the runtime caller, not from inside a skill):**
|
|
2149
2179
|
|
|
@@ -2167,7 +2197,7 @@ For *data skills* (skills marked `# Type: data`), the compile-time inline primit
|
|
|
2167
2197
|
- 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.
|
|
2168
2198
|
- 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.
|
|
2169
2199
|
- 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: []`.
|
|
2170
|
-
- Pair composition with `(fallback: ...)` when the child skill might fail and the parent has a sensible degraded path.
|
|
2200
|
+
- Pair composition with `(fallback: ...)` when the child skill might fail and the parent has a sensible degraded path. The `(fallback:)` catches a raised child throw, not just a missing bind.
|
|
2171
2201
|
- Use mechanical mode to TestFlight any multi-skill chain before shipping it as a Headless skill on a cron trigger.
|
|
2172
2202
|
- Forward references work — author sibling skills in any order, validate independently. The tier-2 warning surfaces the deferred-resolution path; runtime catches genuine misses.
|
|
2173
2203
|
- 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.
|
|
@@ -2565,5 +2595,5 @@ When any of these primitives ship, the relevant grammar moves into its canonical
|
|
|
2565
2595
|
|
|
2566
2596
|
---
|
|
2567
2597
|
|
|
2568
|
-
*Rendered from `skillscript/skillscript-language-reference` — 2026-07-
|
|
2569
|
-
*Source of truth: AMP (`amp_render_document("skillscript/skillscript-language-reference")`)*
|
|
2598
|
+
*Rendered from `skillscript/skillscript-language-reference` — 2026-07-16 18:49 EDT*
|
|
2599
|
+
*Source of truth: AMP (`amp_render_document("skillscript/skillscript-language-reference")`)*
|
|
@@ -9,12 +9,12 @@ Worked examples + fork-me templates for adopter-written connectors. The bundled
|
|
|
9
9
|
| `SkillStore` | `FilesystemSkillStore`, `SqliteSkillStore` (in `src/connectors/`) | — | **[SkillStoreTemplate/](./SkillStoreTemplate/)** |
|
|
10
10
|
| `DataStore` | `SqliteDataStore` (in `src/connectors/`) | — | **[DataStoreTemplate/](./DataStoreTemplate/)** |
|
|
11
11
|
| `LocalModel` | `OllamaLocalModel` (in `src/connectors/`; opt-in via substrate config) | — | **[LocalModelTemplate/](./LocalModelTemplate/)** |
|
|
12
|
-
| `McpConnector` | `RemoteMcpConnector`, `CallbackMcpConnector`, `LocalModelMcpConnector`, `DataStoreMcpConnector`, `SkillStoreMcpConnector` (in `src/connectors/`) |
|
|
12
|
+
| `McpConnector` | `HttpMcpConnector`, `RemoteMcpConnector`, `CallbackMcpConnector`, `LocalModelMcpConnector`, `DataStoreMcpConnector`, `SkillStoreMcpConnector` (in `src/connectors/`) | **[RestConnector/](./RestConnector/)** | **[McpConnectorTemplate/](./McpConnectorTemplate/)** |
|
|
13
13
|
| `AgentConnector` | `NoOpAgentConnector` (in `src/connectors/`) | **[HttpWebhookAgentConnector/](./HttpWebhookAgentConnector/)** | — |
|
|
14
14
|
|
|
15
15
|
**Bundled defaults** are runnable out of the box — wired through `connectors.json` substrate config or programmatic bootstrap.
|
|
16
16
|
|
|
17
|
-
**Worked examples** are real implementations for substrates that aren't bundled — copy + customize for your specific deployment. HttpWebhookAgentConnector demonstrates the AgentConnector contract against a generic HTTP-webhook substrate.
|
|
17
|
+
**Worked examples** are real implementations for substrates that aren't bundled — copy + customize for your specific deployment. HttpWebhookAgentConnector demonstrates the AgentConnector contract against a generic HTTP-webhook substrate. RestConnector demonstrates that the McpConnector contract is wire-protocol-agnostic — it fronts a plain REST/HTTP API (no MCP wire protocol), and coexists in the same registry with actual MCP connectors.
|
|
18
18
|
|
|
19
19
|
**Fork templates** are skeletons (every method throws TODO). Useful when you want the bare contract surface without any specific substrate assumptions. SkillStoreTemplate is the starting point for Postgres-, MongoDB-, AMP-, or vector-DB-backed SkillStore impls.
|
|
20
20
|
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# RestConnector — example .env config.
|
|
2
|
+
# Copy to .env in your project root + fill in real values. Never commit the token.
|
|
3
|
+
#
|
|
4
|
+
# This connector reads its token from the env var named by `authTokenEnvVar`
|
|
5
|
+
# in your wiring. The example wires:
|
|
6
|
+
# new RestConnector({ baseUrl: "...", authTokenEnvVar: "TICKETS_API_TOKEN" })
|
|
7
|
+
# so it looks up process.env.TICKETS_API_TOKEN at call time.
|
|
8
|
+
#
|
|
9
|
+
# Rename the var to match your service; keep the wiring and the var name in sync.
|
|
10
|
+
TICKETS_API_TOKEN=
|
|
11
|
+
|
|
12
|
+
# The connector's `baseUrl` is passed in code / connectors.json, not here — but
|
|
13
|
+
# if you templatize connectors.json with env expansion, you might also keep:
|
|
14
|
+
# TICKETS_API_BASE_URL=https://api.internal.acme.io/v1
|
|
15
|
+
|
|
16
|
+
# Credential-free egress alternative: if you deploy behind an outbound proxy
|
|
17
|
+
# that injects auth, leave TICKETS_API_TOKEN empty, drop authTokenEnvVar from
|
|
18
|
+
# the wiring, and point baseUrl at the gateway. No secret lives in the runtime.
|
|
19
|
+
# See docs/adopter-playbook.md (proxy-egress pattern).
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# RestConnector — a working REST-backed connector
|
|
2
|
+
|
|
3
|
+
A **complete, runnable** `McpConnector` that fronts a plain REST/HTTP API. Unlike [`McpConnectorTemplate`](../McpConnectorTemplate/) (a throws-`TODO` skeleton), you can register this as-is and dispatch to a real backend — you only edit the endpoint table and auth.
|
|
4
|
+
|
|
5
|
+
## Why this exists
|
|
6
|
+
|
|
7
|
+
It answers one question adopters keep asking: **"my backend speaks REST, not the MCP wire protocol — can skillscript still call it?"**
|
|
8
|
+
|
|
9
|
+
Yes. `McpConnector` is the *dispatch surface skills call* (`$ connector.tool`), **not** a requirement that the backend speak MCP. The name is about the skill-facing verb, not the wire:
|
|
10
|
+
|
|
11
|
+
| Connector | Backend wire protocol | Same contract? |
|
|
12
|
+
|---|---|---|
|
|
13
|
+
| `HttpMcpConnector` (bundled) | JSON-RPC-over-HTTP (it fronts MCP servers) | ✅ |
|
|
14
|
+
| **`RestConnector` (this)** | plain REST/HTTP | ✅ |
|
|
15
|
+
| a WebSocket / gRPC / in-process fork | anything | ✅ |
|
|
16
|
+
|
|
17
|
+
All satisfy the same two-method contract (`call` + `manifest`). **A skill can't tell them apart**, and one skill body can mix them freely:
|
|
18
|
+
|
|
19
|
+
```
|
|
20
|
+
$ gmail.send to="ops@acme.io" subject="Deploy done" # an MCP connector
|
|
21
|
+
-> _
|
|
22
|
+
$ tickets.create title="Deploy 4.2" severity="info" # this REST connector
|
|
23
|
+
-> ticket
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
The registry holds a heterogeneous set; each `$ <name>.<tool>` op routes to whichever connector owns that name.
|
|
27
|
+
|
|
28
|
+
## What you edit
|
|
29
|
+
|
|
30
|
+
Three things, all in [`RestConnector.ts`](./RestConnector.ts):
|
|
31
|
+
|
|
32
|
+
1. **`ENDPOINTS`** — one entry per tool: HTTP method + path (with `:param` placeholders) + description + optional `inputSchema`. This is the tool surface skills dispatch to.
|
|
33
|
+
2. **`RestConnectorConfig`** (via the constructor or `connectors.json`) — `baseUrl`, auth header, token source.
|
|
34
|
+
3. **Registration** — programmatic or declarative (below).
|
|
35
|
+
|
|
36
|
+
Everything else works as written: path templating (`:id` → path, filled from args), query-vs-body routing (GET/DELETE → query string, POST/PUT/PATCH → JSON body), auth header injection, per-request timeout, error surfacing via `throw` (so op-level `(fallback: ...)` catches it), `staticTools()` lint, and `describeTools()` discovery.
|
|
37
|
+
|
|
38
|
+
## Wiring
|
|
39
|
+
|
|
40
|
+
**Programmatic** (your bootstrap):
|
|
41
|
+
|
|
42
|
+
```typescript
|
|
43
|
+
import { Registry } from "skillscript-runtime";
|
|
44
|
+
import { RestConnector } from "./RestConnector.js";
|
|
45
|
+
|
|
46
|
+
const registry = new Registry();
|
|
47
|
+
registry.registerMcpConnector(
|
|
48
|
+
"tickets",
|
|
49
|
+
new RestConnector({
|
|
50
|
+
baseUrl: "https://api.internal.acme.io/v1",
|
|
51
|
+
authTokenEnvVar: "TICKETS_API_TOKEN", // read from env, never hardcode
|
|
52
|
+
}),
|
|
53
|
+
);
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
**Declarative** (`connectors.json`) — register the class once, then adopters declare instances in JSON:
|
|
57
|
+
|
|
58
|
+
```typescript
|
|
59
|
+
import { registerConnectorClass } from "skillscript-runtime/connectors";
|
|
60
|
+
import { RestConnector } from "./RestConnector.js";
|
|
61
|
+
|
|
62
|
+
registerConnectorClass("RestConnector", {
|
|
63
|
+
ctor: RestConnector,
|
|
64
|
+
fromConfig: (cfg) => RestConnector.fromConfig(cfg),
|
|
65
|
+
});
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
```json
|
|
69
|
+
{
|
|
70
|
+
"tickets": {
|
|
71
|
+
"class": "RestConnector",
|
|
72
|
+
"config": { "baseUrl": "https://api.internal.acme.io/v1", "authTokenEnvVar": "TICKETS_API_TOKEN" }
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Call `registerConnectorClass` **before** `loadConnectorsConfig` runs. This makes the *instance* JSON-configurable; the `ENDPOINTS` table is still code — a fully config-only REST connector (endpoints in JSON too) is a natural next fork.
|
|
78
|
+
|
|
79
|
+
## Credentials
|
|
80
|
+
|
|
81
|
+
- **Prefer `authTokenEnvVar`** over a literal `authToken` — never commit a token. See [`.env.example`](./.env.example).
|
|
82
|
+
- Default header is `Authorization: Bearer <token>`. For an API-key header, set `authHeader: "X-API-Key"` + `authScheme: "raw"`.
|
|
83
|
+
- **Credential-free egress:** if you deploy behind an outbound proxy that injects auth (so no token lives in the runtime at all), you don't need `authToken`/`authTokenEnvVar` here — point `baseUrl` at the gateway. See the proxy-egress pattern in [`docs/adopter-playbook.md`](../../../docs/adopter-playbook.md).
|
|
84
|
+
|
|
85
|
+
## What the contract does *not* have
|
|
86
|
+
|
|
87
|
+
There is **no `mutating` flag** on the tool descriptor (`McpToolDescriptor` is `{ name, description?, inputSchema? }`). The read/write distinction rides in the description text + the HTTP method — see how `describeTools()` prefixes `[POST]` / `[GET]`. If your host needs a first-class effect classification, that's a skill-body concern (the op's declared footprint), not a connector-descriptor field.
|
|
88
|
+
|
|
89
|
+
## Identity propagation
|
|
90
|
+
|
|
91
|
+
`call()` receives `ctxOverrides` (`agentId`, `isAdmin`) but this example ignores it (`supports_identity_propagation: false`). To honor it, forward it as a header (`headers["X-On-Behalf-Of"] = ctx.agentId`) and flip the flag — which then obligates the Level 1 / Level 2 conformance probes documented in [`McpConnectorTemplate`](../McpConnectorTemplate/McpConnectorTemplate.ts).
|
|
92
|
+
|
|
93
|
+
## Contract surface
|
|
94
|
+
|
|
95
|
+
| Method | Required | What it does |
|
|
96
|
+
|---|---|---|
|
|
97
|
+
| `call(toolName, args, ctx?)` | ✅ | Map tool + args → an HTTPS request; return parsed JSON |
|
|
98
|
+
| `manifest()` | ✅ | Transport metadata for `runtime_capabilities` discovery |
|
|
99
|
+
| `describeTools()` | optional | Endpoints-as-tools for author-time discovery + input lint |
|
|
100
|
+
| `staticCapabilities()` | ✅ static | Declare supported features |
|
|
101
|
+
| `staticTools()` | optional static | Closed tool set → lint validates `$ name.tool` at authoring time |
|
|
102
|
+
|
|
103
|
+
## Further reading
|
|
104
|
+
|
|
105
|
+
- **[`../McpConnectorTemplate/`](../McpConnectorTemplate/)** — the fork-me skeleton + the full contract walkthrough (identity propagation, `fromConfig`, capability flags)
|
|
106
|
+
- **[`../../../docs/connector-contract-reference.md`](../../../docs/connector-contract-reference.md)** — the connector contracts, and why they're wire-protocol-agnostic
|
|
107
|
+
- **[`../../../docs/adopter-playbook.md`](../../../docs/adopter-playbook.md)** — wiring patterns + the credential-free proxy-egress deployment
|
|
108
|
+
- **`src/connectors/types.ts`** — authoritative `McpConnector` interface
|
|
109
|
+
- **`src/connectors/http-mcp.ts`** — `HttpMcpConnector`, the bundled HTTP impl (JSON-RPC-over-HTTP, for actual MCP servers)
|