skillscript-runtime 0.10.0 → 0.13.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +23 -45
- package/dist/bootstrap.d.ts.map +1 -1
- package/dist/bootstrap.js +18 -8
- package/dist/bootstrap.js.map +1 -1
- package/dist/cli.js +5 -6
- package/dist/cli.js.map +1 -1
- package/dist/connectors/agent-noop.d.ts +2 -2
- package/dist/connectors/agent-noop.d.ts.map +1 -1
- package/dist/connectors/index.d.ts.map +1 -1
- package/dist/connectors/index.js +5 -3
- package/dist/connectors/index.js.map +1 -1
- package/dist/connectors/local-model-mcp.d.ts +3 -3
- package/dist/connectors/local-model-mcp.d.ts.map +1 -1
- package/dist/connectors/local-model.d.ts +30 -11
- package/dist/connectors/local-model.d.ts.map +1 -1
- package/dist/connectors/local-model.js +44 -12
- package/dist/connectors/local-model.js.map +1 -1
- package/dist/connectors/mcp-remote.d.ts +3 -3
- package/dist/connectors/mcp-remote.d.ts.map +1 -1
- package/dist/connectors/mcp.d.ts +3 -3
- package/dist/connectors/mcp.d.ts.map +1 -1
- package/dist/connectors/memory-store-mcp.d.ts +3 -3
- package/dist/connectors/memory-store-mcp.d.ts.map +1 -1
- package/dist/connectors/memory-store-mcp.js +3 -1
- package/dist/connectors/memory-store-mcp.js.map +1 -1
- package/dist/connectors/memory-store.d.ts +26 -6
- package/dist/connectors/memory-store.d.ts.map +1 -1
- package/dist/connectors/memory-store.js +84 -15
- package/dist/connectors/memory-store.js.map +1 -1
- package/dist/connectors/registry.d.ts +28 -5
- package/dist/connectors/registry.d.ts.map +1 -1
- package/dist/connectors/registry.js +34 -11
- package/dist/connectors/registry.js.map +1 -1
- package/dist/connectors/skill-store.d.ts +3 -3
- package/dist/connectors/skill-store.d.ts.map +1 -1
- package/dist/connectors/sqlite-skill-store.d.ts +27 -3
- package/dist/connectors/sqlite-skill-store.d.ts.map +1 -1
- package/dist/connectors/sqlite-skill-store.js +85 -58
- package/dist/connectors/sqlite-skill-store.js.map +1 -1
- package/dist/connectors/types.d.ts +86 -9
- package/dist/connectors/types.d.ts.map +1 -1
- package/dist/connectors/types.js.map +1 -1
- package/dist/dashboard/spa/app.js +9 -1
- package/dist/errors.d.ts +6 -0
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +8 -0
- package/dist/errors.js.map +1 -1
- package/dist/mcp-server.d.ts +3 -2
- package/dist/mcp-server.d.ts.map +1 -1
- package/dist/mcp-server.js +24 -2
- package/dist/mcp-server.js.map +1 -1
- package/dist/runtime-config.d.ts +0 -2
- package/dist/runtime-config.d.ts.map +1 -1
- package/dist/runtime-config.js +1 -1
- package/dist/runtime-config.js.map +1 -1
- package/dist/runtime.d.ts.map +1 -1
- package/dist/runtime.js +24 -29
- package/dist/runtime.js.map +1 -1
- package/dist/testing/conformance.d.ts.map +1 -1
- package/dist/testing/conformance.js +12 -6
- package/dist/testing/conformance.js.map +1 -1
- package/docs/ERD.md +1139 -0
- package/docs/adopter-playbook.md +240 -0
- package/docs/configuration.md +296 -0
- package/docs/connector-contract-reference.md +164 -0
- package/docs/language-reference.md +1972 -0
- package/docs/sqlite-skill-store.md +167 -0
- package/examples/README.md +13 -13
- package/examples/connectors/LocalModelTemplate/LocalModelTemplate.ts +138 -0
- package/examples/connectors/LocalModelTemplate/README.md +134 -0
- package/examples/connectors/McpConnectorTemplate/McpConnectorTemplate.ts +200 -0
- package/examples/connectors/McpConnectorTemplate/README.md +153 -0
- package/examples/connectors/MemoryStoreTemplate/MemoryStoreTemplate.ts +155 -0
- package/examples/connectors/MemoryStoreTemplate/README.md +169 -0
- package/examples/connectors/README.md +47 -0
- package/examples/connectors/SkillStoreTemplate/README.md +143 -0
- package/examples/connectors/SkillStoreTemplate/SkillStoreTemplate.ts +199 -0
- package/examples/{hello.skill.provenance.json → skillscripts/hello.skill.provenance.json} +1 -1
- package/package.json +4 -3
- package/scaffold/config.toml +8 -12
- package/scaffold/connectors.json +4 -4
- package/ARCHITECTURE.md +0 -102
- /package/examples/{classify-support-ticket.skill.md → skillscripts/classify-support-ticket.skill.md} +0 -0
- /package/examples/{cut-release-tag.skill.md → skillscripts/cut-release-tag.skill.md} +0 -0
- /package/examples/{doc-qa-with-citations.skill.md → skillscripts/doc-qa-with-citations.skill.md} +0 -0
- /package/examples/{feedback-sentiment-scan.skill.md → skillscripts/feedback-sentiment-scan.skill.md} +0 -0
- /package/examples/{hello.skill.md → skillscripts/hello.skill.md} +0 -0
- /package/examples/{morning-brief.skill.md → skillscripts/morning-brief.skill.md} +0 -0
- /package/examples/{queue-length-monitor.skill.md → skillscripts/queue-length-monitor.skill.md} +0 -0
- /package/examples/{service-health-watch.skill.md → skillscripts/service-health-watch.skill.md} +0 -0
- /package/examples/{youtrack-morning-sweep.skill.md → skillscripts/youtrack-morning-sweep.skill.md} +0 -0
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
# SqliteSkillStore — example
|
|
2
|
+
|
|
3
|
+
Worked example of `SkillStore` against a SQLite database. Copy this directory into your codebase, customize per your storage needs, register with skillscript-runtime's `Registry`. This README is written for the agent implementing your adopter's connector — including the human reviewing the PR.
|
|
4
|
+
|
|
5
|
+
**What this demonstrates**: the locked SkillStore contract surface (8 methods + `staticCapabilities`) wired through real SQL with two-table versioning, transactional status transitions, and JSON-extract tag filters.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## The three-leg model
|
|
10
|
+
|
|
11
|
+
SkillStore is pluggable. Three legs ship out of the box, but the third is the open one:
|
|
12
|
+
|
|
13
|
+
```
|
|
14
|
+
SkillStore (choose which connector)
|
|
15
|
+
├── FilesystemSkillStore (bundled, FS-backed)
|
|
16
|
+
├── SqliteSkillStore (this example, DB-backed)
|
|
17
|
+
└── Adopter-custom (you write your own)
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
If your substrate is AMP, Pinecone, S3, Postgres, or anything else, write a `class FooSkillStore implements SkillStore { ... }` and call `registry.registerSkillStore("primary", new FooSkillStore(...))`. The runtime is none the wiser.
|
|
21
|
+
|
|
22
|
+
This SqliteSkillStore is one such impl — useful as a copy-paste starting point, or directly usable if your needs match.
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## Quick start
|
|
27
|
+
|
|
28
|
+
```typescript
|
|
29
|
+
import { Registry } from "skillscript-runtime";
|
|
30
|
+
import { SqliteSkillStore } from "./SqliteSkillStore.js";
|
|
31
|
+
|
|
32
|
+
const registry = new Registry();
|
|
33
|
+
registry.registerSkillStore("primary", new SqliteSkillStore({ dbPath: "skills.db" }));
|
|
34
|
+
|
|
35
|
+
// Wire the rest of the runtime (scheduler, mcpServer) using the registry.
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Author skills programmatically:
|
|
39
|
+
|
|
40
|
+
```typescript
|
|
41
|
+
const store = registry.getSkillStore("primary");
|
|
42
|
+
await store.store("morning-status", `# Skill: morning-status
|
|
43
|
+
# Status: Approved
|
|
44
|
+
t:
|
|
45
|
+
! report status
|
|
46
|
+
default: t
|
|
47
|
+
`);
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Or via the dashboard, or via `skill_write` MCP tool — same backend, same result.
|
|
51
|
+
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
## When to use SqliteSkillStore (and when not)
|
|
55
|
+
|
|
56
|
+
**Use it when**:
|
|
57
|
+
|
|
58
|
+
- You're embedding skillscript-runtime as a library and want skills in a database rather than `.skill.md` files
|
|
59
|
+
- Your deployment has no persistent filesystem (container-only) but does have SQLite
|
|
60
|
+
- You need richer query semantics than filesystem listing (tag filters, transactional status transitions)
|
|
61
|
+
|
|
62
|
+
**Don't use it when**:
|
|
63
|
+
|
|
64
|
+
- You're using the bundled CLI (`skillfile compile`, `skillfile lint`, `skillfile list`). The CLI is filesystem-first by design — `vim foo.skill.md && skillfile lint foo` is the natural authoring loop. The CLI does NOT use SqliteSkillStore. Sqlite-backed skills are authored via dashboard or `skill_write` MCP tool.
|
|
65
|
+
- Your skills are authored as files-on-disk and committed to git as part of the source tree. FilesystemSkillStore is the right choice there.
|
|
66
|
+
|
|
67
|
+
This is "first-class" in the **programmatic-embedding** sense, not the CLI sense. Treat the example as a substrate-portability proof point + a copy-paste starting point.
|
|
68
|
+
|
|
69
|
+
---
|
|
70
|
+
|
|
71
|
+
## Schema
|
|
72
|
+
|
|
73
|
+
Two tables:
|
|
74
|
+
|
|
75
|
+
```sql
|
|
76
|
+
CREATE TABLE skills (
|
|
77
|
+
name TEXT PRIMARY KEY,
|
|
78
|
+
current_version TEXT NOT NULL,
|
|
79
|
+
content_hash TEXT NOT NULL,
|
|
80
|
+
status TEXT NOT NULL, -- 'Draft' | 'Approved' | 'Disabled'
|
|
81
|
+
source TEXT NOT NULL, -- the .skill.md body bytes
|
|
82
|
+
description TEXT, -- extracted from `# Description:` header
|
|
83
|
+
metadata_json TEXT, -- optional metadata bag (tags, author, etc.)
|
|
84
|
+
created_at INTEGER NOT NULL,
|
|
85
|
+
updated_at INTEGER NOT NULL,
|
|
86
|
+
status_changed_at INTEGER
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
CREATE TABLE skill_versions (
|
|
90
|
+
name TEXT NOT NULL,
|
|
91
|
+
version TEXT NOT NULL, -- short hash (12 chars)
|
|
92
|
+
content_hash TEXT NOT NULL,
|
|
93
|
+
source TEXT NOT NULL, -- full body, preserved per version
|
|
94
|
+
status TEXT NOT NULL,
|
|
95
|
+
previous_status TEXT, -- populated by update_status
|
|
96
|
+
changed_at INTEGER NOT NULL,
|
|
97
|
+
changed_by TEXT,
|
|
98
|
+
PRIMARY KEY (name, version)
|
|
99
|
+
);
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
`skills` is the fast-path read for `load()` / `metadata()` / `query()`. `skill_versions` is the append-only history for `versions()` — and unlike FilesystemSkillStore, it preserves full body bytes per version, so `load(name, version)` can return historical content.
|
|
103
|
+
|
|
104
|
+
WAL is enabled at bootstrap so concurrent readers don't block writers.
|
|
105
|
+
|
|
106
|
+
---
|
|
107
|
+
|
|
108
|
+
## Footgun guard: `delete()` is hard-cascade
|
|
109
|
+
|
|
110
|
+
**`delete()` removes both the `skills` row AND its `skill_versions` rows.** If you need recovery, back up adopter-side BEFORE calling delete. Skill names can be reused after delete (no orphan history left behind).
|
|
111
|
+
|
|
112
|
+
This is the locked semantic. If your adopter substrate has compliance requirements (audit-grade retention) or you hit a "wait, I deleted that" moment, the upgrade path is soft-delete (tombstone `status='Deleted'` + filter from query results) — but that's an adopter-side choice; the bundled SqliteSkillStore stays hard-cascade.
|
|
113
|
+
|
|
114
|
+
---
|
|
115
|
+
|
|
116
|
+
## Feature flags
|
|
117
|
+
|
|
118
|
+
`staticCapabilities()` declares:
|
|
119
|
+
|
|
120
|
+
| Feature | Value | What it means |
|
|
121
|
+
|---|---|---|
|
|
122
|
+
| `supports_writes` | ✓ | `store()` / `update_status()` / `delete()` mutate state |
|
|
123
|
+
| `supports_versioning` | ✓ | `versions()` returns history; `load(name, version)` returns historical bytes |
|
|
124
|
+
| `supports_tag_filter` | ✓ | `query({ tag: "foo" })` works via `json_extract(metadata_json, '$.tags')` (O(n) scan) |
|
|
125
|
+
| `supports_audit_trail` | ✓ | `update_status()` populates `previous_status` on every transition |
|
|
126
|
+
| `supports_atomic_status_transitions` | ✓ | UPDATE skills + INSERT skill_versions wrapped in a transaction |
|
|
127
|
+
|
|
128
|
+
The atomic transitions are the SQL advantage over FilesystemSkillStore (which declares `supports_atomic_status_transitions: false` because filesystem writes can tear between body rewrite + sidecar append).
|
|
129
|
+
|
|
130
|
+
---
|
|
131
|
+
|
|
132
|
+
## Authoring loop
|
|
133
|
+
|
|
134
|
+
SqliteSkillStore is the storage layer. Authoring happens above:
|
|
135
|
+
|
|
136
|
+
- **Dashboard**: visit `http://localhost:7878`, create/edit skills through the UI; dashboard writes via `skill_write` MCP tool → SqliteSkillStore
|
|
137
|
+
- **MCP tool**: agents call `skill_write` directly; same path as the dashboard
|
|
138
|
+
- **Programmatic**: your code calls `store.store(name, source, metadata?)` directly
|
|
139
|
+
|
|
140
|
+
The dashboard does NOT default to SqliteSkillStore today — `skillfile dashboard` bootstraps with FilesystemSkillStore. If you want a SqliteSkillStore-backed dashboard, write a custom bootstrap (small surface; see the runtime `Registry` API).
|
|
141
|
+
|
|
142
|
+
---
|
|
143
|
+
|
|
144
|
+
## Approval-token stamping
|
|
145
|
+
|
|
146
|
+
Skill bodies that declare `# Status: Approved` get a token stamped on `store()` automatically: `# Status: Approved v1:<hex>`. Same behavior as FilesystemSkillStore. Transitions to `Approved` via `update_status()` stamp the token; transitions away strip it.
|
|
147
|
+
|
|
148
|
+
Adopters who want a stronger `f()` for the token (HMAC-SHA256 instead of the default) can register a custom approval fn before calling `store()`/`update_status()`:
|
|
149
|
+
|
|
150
|
+
```typescript
|
|
151
|
+
import { registerApprovalFn } from "skillscript-runtime";
|
|
152
|
+
registerApprovalFn("v2", (body) => hmacSha256(SECRET, body));
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
---
|
|
156
|
+
|
|
157
|
+
## Forking checklist
|
|
158
|
+
|
|
159
|
+
When forking into your codebase:
|
|
160
|
+
|
|
161
|
+
1. Rename the class (e.g., `PostgresSkillStore`, `AmpSkillStore`)
|
|
162
|
+
2. Replace the SQL with your substrate's API (HTTP, MemoryStore, vector DB, etc.)
|
|
163
|
+
3. Update `staticCapabilities()` to match what your substrate actually supports — drop `supports_versioning` if you can't track history, drop `supports_tag_filter` if querying tags isn't tractable
|
|
164
|
+
4. Update `manifest()` to describe your substrate (`kind: "amp"` or whatever)
|
|
165
|
+
5. Tests: copy `tests/SqliteSkillStore.test.ts` as a starting point + run the conformance suite (`SkillStoreConformance.buildTests()` from `skillscript-runtime/testing`)
|
|
166
|
+
|
|
167
|
+
The conformance suite catches drift from the contract surface. If your fork passes, the runtime treats it interchangeably with FilesystemSkillStore / SqliteSkillStore / etc.
|
package/examples/README.md
CHANGED
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
# Examples
|
|
2
2
|
|
|
3
|
-
Curated production-quality skills demonstrating distinct language patterns.
|
|
3
|
+
Curated production-quality skills demonstrating distinct language patterns. The `.skill.md` files live in [`skillscripts/`](./skillscripts/); each compiles + lints clean and can be run directly via `skillfile run examples/skillscripts/<name>.skill.md` (some require connectors — see the per-skill notes below).
|
|
4
4
|
|
|
5
5
|
For full language semantics, see [`../docs/language-reference.md`](../docs/language-reference.md).
|
|
6
6
|
|
|
7
7
|
| Example | Patterns demonstrated |
|
|
8
8
|
|---|---|
|
|
9
|
-
| [`hello.skill.md`](./hello.skill.md) | Three-command first run; `! emit`; default target; `# Vars:` declared inputs |
|
|
10
|
-
| [`morning-brief.skill.md`](./morning-brief.skill.md) | Multi-target with `needs:`; cron-fired with `EVENT.fired_at_*`; `# OnError:` fallback; dual `# Output:` (slack + prompt-context); `# Requires:` user-var cascade with fallback; LocalModel + retrieval composition |
|
|
11
|
-
| [`doc-qa-with-citations.skill.md`](./doc-qa-with-citations.skill.md) | Single-target retrieval; `(fallback: [])` op-level fallback; LLM-with-citation pattern; pipe filter `\|json` for prompt embedding |
|
|
12
|
-
| [`classify-support-ticket.skill.md`](./classify-support-ticket.skill.md) | `if`/`elif`/`else` multi-branch routing; classifier-cascade pattern; MemoryStore `$` writes with structured `domain_tags`; `$set` literal binding |
|
|
13
|
-
| [`cut-release-tag.skill.md`](./cut-release-tag.skill.md) | `??` interactive ask-for-input; `else:` short-circuit on multiple targets; `@` shell ops with per-op error fallback; rollback-on-failure flow |
|
|
14
|
-
| [`service-health-watch.skill.md`](./service-health-watch.skill.md) | `foreach` iteration; cron-fired with `expires_at=$(EVENT.fired_at_plus_1d_unix)` TTL math; classifier-gated MemoryStore writes; pipe filter `\|url` for path-safe interpolation |
|
|
15
|
-
| [`feedback-sentiment-scan.skill.md`](./feedback-sentiment-scan.skill.md) | `in` / `not in` set-membership ops; dedupe-via-seen-markers idiom; nested `if`/`elif` classification cascade; TTL on bookkeeping writes |
|
|
9
|
+
| [`hello.skill.md`](./skillscripts/hello.skill.md) | Three-command first run; `! emit`; default target; `# Vars:` declared inputs |
|
|
10
|
+
| [`morning-brief.skill.md`](./skillscripts/morning-brief.skill.md) | Multi-target with `needs:`; cron-fired with `EVENT.fired_at_*`; `# OnError:` fallback; dual `# Output:` (slack + prompt-context); `# Requires:` user-var cascade with fallback; LocalModel + retrieval composition |
|
|
11
|
+
| [`doc-qa-with-citations.skill.md`](./skillscripts/doc-qa-with-citations.skill.md) | Single-target retrieval; `(fallback: [])` op-level fallback; LLM-with-citation pattern; pipe filter `\|json` for prompt embedding |
|
|
12
|
+
| [`classify-support-ticket.skill.md`](./skillscripts/classify-support-ticket.skill.md) | `if`/`elif`/`else` multi-branch routing; classifier-cascade pattern; MemoryStore `$` writes with structured `domain_tags`; `$set` literal binding |
|
|
13
|
+
| [`cut-release-tag.skill.md`](./skillscripts/cut-release-tag.skill.md) | `??` interactive ask-for-input; `else:` short-circuit on multiple targets; `@` shell ops with per-op error fallback; rollback-on-failure flow |
|
|
14
|
+
| [`service-health-watch.skill.md`](./skillscripts/service-health-watch.skill.md) | `foreach` iteration; cron-fired with `expires_at=$(EVENT.fired_at_plus_1d_unix)` TTL math; classifier-gated MemoryStore writes; pipe filter `\|url` for path-safe interpolation |
|
|
15
|
+
| [`feedback-sentiment-scan.skill.md`](./skillscripts/feedback-sentiment-scan.skill.md) | `in` / `not in` set-membership ops; dedupe-via-seen-markers idiom; nested `if`/`elif` classification cascade; TTL on bookkeeping writes |
|
|
16
16
|
|
|
17
17
|
## Connector requirements
|
|
18
18
|
|
|
@@ -31,20 +31,20 @@ Skills that talk to MCP tools (like `calendar.list_events`) need that tool wired
|
|
|
31
31
|
|
|
32
32
|
```sh
|
|
33
33
|
# Cold install — no Ollama, no MCP, no external state
|
|
34
|
-
skillfile run examples/hello.skill.md
|
|
34
|
+
skillfile run examples/skillscripts/hello.skill.md
|
|
35
35
|
|
|
36
36
|
# With overrides
|
|
37
|
-
skillfile run examples/hello.skill.md --input WHO=Scott
|
|
37
|
+
skillfile run examples/skillscripts/hello.skill.md --input WHO=Scott
|
|
38
38
|
|
|
39
39
|
# Preview the compiled artifact without execution
|
|
40
|
-
skillfile compile examples/hello.skill.md
|
|
40
|
+
skillfile compile examples/skillscripts/hello.skill.md
|
|
41
41
|
|
|
42
42
|
# Mechanical preview — $/~/> ops short-circuit (useful for examining flow
|
|
43
43
|
# without firing real LLM calls)
|
|
44
|
-
skillfile run examples/morning-brief.skill.md --mechanical
|
|
44
|
+
skillfile run examples/skillscripts/morning-brief.skill.md --mechanical
|
|
45
45
|
|
|
46
46
|
# Render the control-flow graph
|
|
47
|
-
skillfile diagram examples/cut-release-tag.skill.md
|
|
47
|
+
skillfile diagram examples/skillscripts/cut-release-tag.skill.md
|
|
48
48
|
```
|
|
49
49
|
|
|
50
50
|
## Programmatic example
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LocalModelTemplate — fork-me skeleton for writing your own LocalModel impl.
|
|
3
|
+
*
|
|
4
|
+
* This is NOT a runnable connector. Every method throws a "TODO" error.
|
|
5
|
+
* Adopters writing OpenAI-compat / Anthropic-compat / vLLM / TGI / SGLang /
|
|
6
|
+
* hosted-LLM / any-other LocalModel substrate fork this directory.
|
|
7
|
+
*
|
|
8
|
+
* The bundled `OllamaLocalModel` (in `src/connectors/local-model.ts`) is
|
|
9
|
+
* Ollama-specific. When that doesn't fit, fork from here.
|
|
10
|
+
*
|
|
11
|
+
* Forking workflow:
|
|
12
|
+
* 1. Copy this directory: `cp -r examples/connectors/LocalModelTemplate examples/connectors/MyLocalModel`
|
|
13
|
+
* 2. Rename the class — typically `<Substrate>LocalModel` (e.g., `OpenAICompatLocalModel`, `AnthropicLocalModel`, `VllmLocalModel`)
|
|
14
|
+
* 3. Implement `run()` against your transport
|
|
15
|
+
* 4. Implement `manifest()` returning substrate metadata
|
|
16
|
+
* 5. Update `staticCapabilities()` to declare what your impl supports
|
|
17
|
+
* 6. Register from your adopter bootstrap:
|
|
18
|
+
* `registry.registerLocalModel("default", new MyLocalModel({ ... }))`
|
|
19
|
+
* 7. Validate via the conformance suite:
|
|
20
|
+
* `LocalModelConformance.buildTests({ build: () => new MyLocalModel(...), ctor: MyLocalModel })`
|
|
21
|
+
*
|
|
22
|
+
* See `src/connectors/local-model.ts` (`OllamaLocalModel`) for a working
|
|
23
|
+
* reference implementation against the Ollama HTTP API. The full contract
|
|
24
|
+
* spec lives in `src/connectors/types.ts` (`LocalModel` interface).
|
|
25
|
+
*
|
|
26
|
+
* Runtime hosts (MCP server + web dashboard) honor whichever LocalModel impl
|
|
27
|
+
* you register. The auto-wired `$ llm` MCP bridge wraps your impl
|
|
28
|
+
* transparently — once registered, `$ llm prompt="..."` dispatches through
|
|
29
|
+
* your fork without any additional wiring.
|
|
30
|
+
*
|
|
31
|
+
* **Note on declarative wiring**: LocalModel is intrinsically *singleton* per
|
|
32
|
+
* deployment (one default per runtime), unlike McpConnector which is
|
|
33
|
+
* multi-instance. There's no `registerLocalModelClass()` equivalent of
|
|
34
|
+
* `registerConnectorClass()` — adopters wire their LocalModel
|
|
35
|
+
* programmatically via `registry.registerLocalModel()` from their bootstrap.
|
|
36
|
+
* Declarative custom-LocalModel via `connectors.json` substrate `{type:
|
|
37
|
+
* "custom", module, export, config}` form is deferred until async-bootstrap
|
|
38
|
+
* support lands (cross-cutting limitation across all three substrate slots).
|
|
39
|
+
*/
|
|
40
|
+
|
|
41
|
+
import type {
|
|
42
|
+
LocalModel,
|
|
43
|
+
LocalModelCapabilities,
|
|
44
|
+
ManifestInfo,
|
|
45
|
+
} from "../../../src/connectors/types.js";
|
|
46
|
+
|
|
47
|
+
/** Replace with your substrate's connection config (endpoint, auth, model selection, etc.). */
|
|
48
|
+
export interface LocalModelTemplateConfig {
|
|
49
|
+
// TODO — declare the fields your substrate needs to connect.
|
|
50
|
+
// Examples:
|
|
51
|
+
// endpoint?: string; // OpenAI-compat / vLLM / TGI URL
|
|
52
|
+
// apiKey?: string; // hosted services
|
|
53
|
+
// defaultModel?: string; // model name / tag
|
|
54
|
+
// timeoutMs?: number;
|
|
55
|
+
exampleConfigField?: string;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export class LocalModelTemplate implements LocalModel {
|
|
59
|
+
/**
|
|
60
|
+
* Declare what your impl supports. The runtime + lint consult these flags.
|
|
61
|
+
* Set conservatively — overclaiming triggers cryptic downstream failures;
|
|
62
|
+
* underclaiming hides usable features.
|
|
63
|
+
*/
|
|
64
|
+
static staticCapabilities(): LocalModelCapabilities {
|
|
65
|
+
return {
|
|
66
|
+
connector_type: "local_model",
|
|
67
|
+
implementation: "LocalModelTemplate", // ← rename to your class name
|
|
68
|
+
contract_version: "1.0.0",
|
|
69
|
+
features: {
|
|
70
|
+
// TODO — set each flag based on what your substrate can actually do.
|
|
71
|
+
supports_max_tokens: false, // can opts.maxTokens cap output?
|
|
72
|
+
supports_timeout: false, // do you honor a per-call timeout?
|
|
73
|
+
supports_streaming: false, // does your run() stream tokens? (v1 contract is non-streaming — flag for forward-compat)
|
|
74
|
+
supports_embedding: false, // does your substrate also expose embeddings?
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
80
|
+
constructor(_config: LocalModelTemplateConfig) {
|
|
81
|
+
// TODO — establish your substrate connection. For HTTP: store base URL
|
|
82
|
+
// + auth headers + default model. For SDK: cache the client instance.
|
|
83
|
+
throw new Error("LocalModelTemplate is a fork-me skeleton; replace with your impl.");
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Dispatch a prompt and return the model's response text.
|
|
88
|
+
*
|
|
89
|
+
* `prompt` — the substituted prompt body (template substitution already
|
|
90
|
+
* resolved by the runtime; you get the final string).
|
|
91
|
+
*
|
|
92
|
+
* `opts`:
|
|
93
|
+
* - `maxTokens` — optional cap on output length. Honor if `supports_max_tokens: true`.
|
|
94
|
+
* - `model` — optional override of the configured default. Honor if your
|
|
95
|
+
* substrate supports multiple models per instance.
|
|
96
|
+
*
|
|
97
|
+
* Return the response text directly (not wrapped in an envelope). The
|
|
98
|
+
* `LocalModelMcpConnector` bridge exposes this as `$ llm prompt="..." -> R`
|
|
99
|
+
* where R binds to your string return value.
|
|
100
|
+
*
|
|
101
|
+
* On dispatch failure: throw. Don't return error envelopes silently — the
|
|
102
|
+
* runtime's op-level `(fallback: ...)` machinery catches throws cleanly.
|
|
103
|
+
*/
|
|
104
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
105
|
+
async run(_prompt: string, _opts: { maxTokens?: number; model?: string }): Promise<string> {
|
|
106
|
+
// TODO — dispatch via your transport.
|
|
107
|
+
// - Map prompt + opts to your substrate's wire format (chat completion / completion / etc.)
|
|
108
|
+
// - Apply your per-call timeout
|
|
109
|
+
// - Parse the response; extract the text
|
|
110
|
+
// - Throw on dispatch failure (don't return error envelope silently)
|
|
111
|
+
throw new Error("TODO: run() — dispatch prompt + return response text.");
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Capability snapshot for `runtime_capabilities` discovery. Return free-form
|
|
116
|
+
* substrate-specific metadata.
|
|
117
|
+
*
|
|
118
|
+
* Per `LocalModelManifest` in `src/connectors/types.ts`, known curated fields:
|
|
119
|
+
* - `kind`: string tag for substrate flavor ("openai-compat", "vllm", etc.)
|
|
120
|
+
* - `default_model`: configured default model name
|
|
121
|
+
* - `endpoint`: URL the impl connects to
|
|
122
|
+
* - `models_available`: list when introspectable (e.g., `/v1/models` for OpenAI-compat)
|
|
123
|
+
* - `fetch_error`: set when introspection failed (don't silently cache empty array)
|
|
124
|
+
* Plus `[key: string]: unknown` for substrate extensions.
|
|
125
|
+
*
|
|
126
|
+
* The bundled `OllamaLocalModel.manifest()` returns:
|
|
127
|
+
* { capabilities_version: "1", manifest: { kind: "ollama",
|
|
128
|
+
* endpoint: "...", default_model: "gemma2:9b", models_available: [...] } }
|
|
129
|
+
*/
|
|
130
|
+
async manifest(): Promise<ManifestInfo<"local_model">> {
|
|
131
|
+
// TODO — return a snapshot of your substrate's metadata.
|
|
132
|
+
// - kind: identify the substrate
|
|
133
|
+
// - endpoint: where you connect
|
|
134
|
+
// - default_model: what model you'll dispatch to
|
|
135
|
+
// - models_available: list if you can introspect; surface fetch_error if introspection fails
|
|
136
|
+
throw new Error("TODO: manifest() — return substrate metadata snapshot.");
|
|
137
|
+
}
|
|
138
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
# LocalModelTemplate — fork-me skeleton
|
|
2
|
+
|
|
3
|
+
A skeleton `LocalModel` implementation for adopters writing their own. Not runnable; every method throws a `TODO` error. Copy this directory, rename, fill in the substrate-specific work.
|
|
4
|
+
|
|
5
|
+
**Use this when you want LocalModel inference backed by:**
|
|
6
|
+
- An OpenAI-compat HTTP endpoint (vLLM, TGI, SGLang, llama.cpp server)
|
|
7
|
+
- A hosted LLM API (OpenAI, Anthropic, Cohere — though most adopters wire those via `RemoteMcpConnector` + the provider's MCP server instead)
|
|
8
|
+
- A custom transport (gRPC, WebSocket, in-process inference)
|
|
9
|
+
- A multi-model gateway
|
|
10
|
+
|
|
11
|
+
The bundled `OllamaLocalModel` (in `src/connectors/local-model.ts`) is Ollama-specific. Fork from here when that doesn't fit.
|
|
12
|
+
|
|
13
|
+
## The two legs
|
|
14
|
+
|
|
15
|
+
```
|
|
16
|
+
LocalModel (choose which connector)
|
|
17
|
+
├── OllamaLocalModel (bundled, Ollama HTTP — src/connectors/local-model.ts)
|
|
18
|
+
└── Your fork from this template
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
LocalModel is the narrowest contract — just 2 methods + staticCapabilities. Most of the work in a fork is substrate-specific (auth, wire format, response parsing), not contract conformance.
|
|
22
|
+
|
|
23
|
+
## Forking workflow
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
cp -r examples/connectors/LocalModelTemplate examples/connectors/MyLocalModel
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
1. **Rename the class.** Convention: `<Substrate>LocalModel` (e.g., `OpenAICompatLocalModel`, `AnthropicLocalModel`, `VllmLocalModel`).
|
|
30
|
+
2. **Define your config interface.** Edit `LocalModelTemplateConfig` to declare what your substrate needs (endpoint URL, API key, default model, timeout, etc.).
|
|
31
|
+
3. **Implement `run()`** — translate `(prompt, opts)` to your substrate's wire format; dispatch; parse the response text.
|
|
32
|
+
4. **Implement `manifest()`** — return substrate metadata (kind, endpoint, default model, available models when introspectable).
|
|
33
|
+
5. **Update `staticCapabilities()`** to declare what your impl supports.
|
|
34
|
+
6. **Wire from your adopter bootstrap:**
|
|
35
|
+
|
|
36
|
+
```typescript
|
|
37
|
+
import { Registry } from "skillscript-runtime";
|
|
38
|
+
import { MyLocalModel } from "./MyLocalModel.js";
|
|
39
|
+
|
|
40
|
+
const registry = new Registry();
|
|
41
|
+
registry.registerLocalModel("default", new MyLocalModel({ /* config */ }));
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
The auto-wired `$ llm` MCP bridge wraps your impl transparently — once registered, `$ llm prompt="..."` dispatches through your fork without additional wiring.
|
|
45
|
+
|
|
46
|
+
7. **Validate via the conformance suite:**
|
|
47
|
+
|
|
48
|
+
```typescript
|
|
49
|
+
import { describe, it } from "vitest";
|
|
50
|
+
import { LocalModelConformance } from "skillscript-runtime/testing";
|
|
51
|
+
import { MyLocalModel } from "./MyLocalModel.js";
|
|
52
|
+
|
|
53
|
+
describe("MyLocalModel conformance", () => {
|
|
54
|
+
const tests = LocalModelConformance.buildTests({
|
|
55
|
+
build: () => new MyLocalModel({ /* test config */ }),
|
|
56
|
+
ctor: MyLocalModel,
|
|
57
|
+
});
|
|
58
|
+
for (const t of tests) it(`[${t.category}] ${t.name}`, t.run);
|
|
59
|
+
});
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Reference implementation
|
|
63
|
+
|
|
64
|
+
The bundled `OllamaLocalModel` at `src/connectors/local-model.ts` is the canonical reference (164 LOC):
|
|
65
|
+
|
|
66
|
+
- Ollama HTTP API (`POST /api/generate`, `GET /api/tags`)
|
|
67
|
+
- AbortController timeout pattern
|
|
68
|
+
- Manifest caches successful introspection but NOT failures (retries on next call)
|
|
69
|
+
- Deduped stderr warning for fetch errors
|
|
70
|
+
- Surfaces `fetch_error` in manifest when `/api/tags` introspection fails
|
|
71
|
+
|
|
72
|
+
The error-handling patterns (don't silently cache failures; surface in manifest; dedupe-log) apply to any LocalModel substrate that introspects.
|
|
73
|
+
|
|
74
|
+
## Contract surface (2 methods)
|
|
75
|
+
|
|
76
|
+
LocalModel is the narrowest contract surface in skillscript:
|
|
77
|
+
|
|
78
|
+
| Method | What it does | When called |
|
|
79
|
+
|---|---|---|
|
|
80
|
+
| `run(prompt, opts)` | Dispatch a prompt; return response text | Every `$ llm prompt="..." -> R` op (via the LocalModelMcpConnector bridge) |
|
|
81
|
+
| `manifest()` | Substrate metadata for discovery | At startup + on-demand from MCP clients |
|
|
82
|
+
|
|
83
|
+
Plus `staticCapabilities()` (required static).
|
|
84
|
+
|
|
85
|
+
### `run()` semantics
|
|
86
|
+
|
|
87
|
+
- **`prompt`** — already-substituted prompt body. Template substitution (`${VAR}` resolution) happens before your impl is called; you get the final string.
|
|
88
|
+
- **`opts.maxTokens`** — optional output length cap. Honor if `supports_max_tokens: true`.
|
|
89
|
+
- **`opts.model`** — optional per-call model override. Useful for adopters with multi-model gateways. Honor if your substrate supports it.
|
|
90
|
+
- **Return** — the response text directly. NOT wrapped in an envelope. The bridge passes this through to skill source as `$(R)`.
|
|
91
|
+
|
|
92
|
+
On dispatch failure: **throw**. The runtime's op-level `(fallback: ...)` machinery catches throws cleanly. Don't return error envelopes silently — that's the silent-recovery footgun pattern.
|
|
93
|
+
|
|
94
|
+
### `manifest()` semantics
|
|
95
|
+
|
|
96
|
+
Surface enough substrate metadata for adopters running `runtime_capabilities` to understand what's wired. Curated fields per `LocalModelManifest`:
|
|
97
|
+
|
|
98
|
+
- `kind` — substrate flavor tag (`"openai-compat"`, `"vllm"`, `"anthropic"`, etc.)
|
|
99
|
+
- `default_model` — configured default model name
|
|
100
|
+
- `endpoint` — URL your impl connects to (helps debugging)
|
|
101
|
+
- `models_available` — list when introspectable
|
|
102
|
+
- `fetch_error` — set when introspection failed; don't silently cache empty array
|
|
103
|
+
|
|
104
|
+
Plus `[key: string]: unknown` for substrate extensions.
|
|
105
|
+
|
|
106
|
+
## A note on declarative wiring
|
|
107
|
+
|
|
108
|
+
Unlike `McpConnector` (which has `registerConnectorClass()` for adopter-extensible declarative wiring via `connectors.json`), `LocalModel` is intrinsically *singleton* per deployment — one default per runtime. There's no equivalent class registry.
|
|
109
|
+
|
|
110
|
+
Adopters wire their custom LocalModel **programmatically** from their bootstrap:
|
|
111
|
+
|
|
112
|
+
```typescript
|
|
113
|
+
registry.registerLocalModel("default", new MyLocalModel({ /* config */ }));
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
Declarative custom-LocalModel via `connectors.json` `substrate.local_model: {type: "custom", module, export, config}` form is **deferred** until async-bootstrap support lands. Same limitation applies to `SkillStore` and `MemoryStore` custom forms — sync `bootstrap()` can't `await import()`. Not LocalModel-specific.
|
|
117
|
+
|
|
118
|
+
## Wiring against the dashboard / MCP
|
|
119
|
+
|
|
120
|
+
Runtime hosts (MCP server + web dashboard) honor whichever LocalModel impl you register. To make your fork visible through `skillfile dashboard`:
|
|
121
|
+
|
|
122
|
+
- Write a custom bootstrap that constructs the runtime with your LocalModel
|
|
123
|
+
- Or register via `registry.registerLocalModel("default", new MyLocalModel(...))` before `bootstrap()` returns
|
|
124
|
+
|
|
125
|
+
The auto-wired `$ llm` MCP bridge picks up your registration automatically — skills writing `$ llm prompt="..." -> R` get your impl without additional config.
|
|
126
|
+
|
|
127
|
+
## Further reading
|
|
128
|
+
|
|
129
|
+
- **[`../../../docs/configuration.md`](../../../docs/configuration.md)** — substrate selection via `connectors.json`
|
|
130
|
+
- **[`../../../docs/adopter-playbook.md`](../../../docs/adopter-playbook.md)** — programmatic-bootstrap patterns
|
|
131
|
+
- **`src/connectors/types.ts`** — authoritative `LocalModel` interface + `LocalModelManifest` curated fields
|
|
132
|
+
- **`src/connectors/local-model.ts`** — `OllamaLocalModel` reference impl
|
|
133
|
+
- **`src/connectors/local-model-mcp.ts`** — the `LocalModelMcpConnector` bridge that wraps your impl as `$ llm`
|
|
134
|
+
- **`src/testing/conformance.ts`** — the per-contract conformance test suites
|