spl-lang 0.2.0 → 0.3.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/README.md CHANGED
@@ -1,188 +1,91 @@
1
- # spl-lang: Semantic Prompt Language interpreter
1
+ # spl-lang 0.3
2
2
 
3
- A Python-like language where LLM calls are part of the syntax. Everything outside LLM calls is deterministic; LLM
4
- calls go through the GitHub Copilot SDK. See [../LANG-PRD.md](../LANG-PRD.md) for the full spec,
5
- [docs/syntax.ebnf](docs/syntax.ebnf) for the grammar and the [documentation site](../docs/index.html) (open it in a
6
- browser, or publish `docs/` with GitHub Pages) for guides and reference.
3
+ The Semantic Prompt Language interpreter. Requires Node.js 20+.
7
4
 
8
- ## Run
5
+ See the [project guide](../README.md), [language specification](../LANG-PRD.md),
6
+ [EBNF](docs/syntax.ebnf), and [documentation site](../docs/index.html).
9
7
 
10
- ```
11
- npm install && npm run build
12
- node dist/cli.js login # once: authenticate with GitHub Copilot
13
- node dist/cli.js examples/live.spl
14
- npx file:./lang examples/live.spl # npx from a checkout (or `npx spl-lang` once published)
15
- ```
16
-
17
- Requires Node 20+ and an authenticated GitHub Copilot.
8
+ ## Run from a checkout
18
9
 
10
+ ```sh
11
+ npm install
12
+ node dist/cli.js login
13
+ node dist/cli.js examples/hi.spl
19
14
  ```
20
- spl <script.spl> [--log-level error|info|debug] [--silent] [--model <id>]
21
- [--store <file.json> | --store-module <module>]
22
- [--timeout <s>] [--cwd <dir>] [--mcp <file.json>] [--check]
23
- spl login # GitHub device flow: prints a code, you confirm it in the browser
24
- spl logout # removes the credentials saved by `spl login`
25
- ```
26
-
27
- * stdout: the value of `success` or a top-level `return` (strings raw, everything else as JSON) or the failure payload.
28
- * stderr: logs, including full LLM requests and raw responses (`[llm]`) and executed statement traces with line numbers (`[deterministic]`) at every non-silent log level (`error`, `info`, `debug`). Model and reasoning effort are included at `info` and `debug`. Logs may contain sensitive data; use `--silent` to suppress them.
29
- * Exit code: `0` success, `1` uncaught failure, `2` syntax/usage error.
30
-
31
- `spl login` does nothing if the runtime is already authenticated (an earlier login, a Copilot CLI / `gh` login or
32
- `COPILOT_GITHUB_TOKEN`/`GH_TOKEN`/`GITHUB_TOKEN`). Otherwise it runs the device flow and saves the token to
33
- `~/.spl/credentials.json` (override the folder with `SPL_HOME`); scripts use that token automatically. A script that
34
- needs an LLM and finds no credentials fails with `llm_error` and tells you to run `spl login`.
35
-
36
- ## Language at a glance
37
15
 
16
+ `npm install` runs the TypeScript build. Installed packages expose `spl` and `spl-lang`.
17
+ Authentication is needed only for live generation and explicit agents; raw tools and replay
18
+ work without Copilot. Existing Copilot/CLI credentials and supported GitHub token environment
19
+ variables are reused. `spl login` otherwise runs device authentication and saves credentials
20
+ under `~/.spl` (override with `SPL_HOME`); `spl logout` removes those saved credentials.
21
+
22
+ ## Language contract
23
+
24
+ - **Exact tools:** `read_file(path)`, `write_file(path, content)`, `web_fetch(url)`,
25
+ `web_search(query)`, `shell(command)` never call a model.
26
+ - **Generation:** `?` and `think` have no tools or MCP access.
27
+ - **Agency:** `agent` explicitly enables model-directed runtime and configured MCP tools.
28
+ - **Validation:** expression-valued `schema` uses strict synchronous JSON Schema draft-07;
29
+ `fmt='json'` accepts any JSON value, `fmt='object'` requires an object/array.
30
+ Prose and code fences are not silently extracted.
31
+ - **Repairs:** `repairs=0..5` retries only malformed/invalid generation, not transport errors
32
+ or external actions. Agents require `repairs=0`. Use `assert` for business rules.
33
+ - **Guidance:** `instruct`, `avoid`, and `len` influence prompts, not runtime guarantees.
34
+ - **Capabilities:** nested `restrict fs/net/exec/mcp` clauses intersect. Shell command
35
+ inspection is best effort; MCP servers are trusted services, not OS-sandboxed.
36
+ - **Bindings:** `let` declares/redeclares function/script locals; `x = ...` and `x += ...`
37
+ update existing locals. Every block shares that scope. Enclosing names are readable but
38
+ cannot be rebound by a function; shared collections remain mutable.
39
+
40
+ Common generation modifiers are `len`, `fmt`, `reasoning`, `show`, `schema`, `repairs`,
41
+ and `model`. Values are evaluated once, then validated before calling the model.
42
+ `think` and `agent` default to medium reasoning; `?` uses the model default.
43
+ CLI `--model` overrides per-call `model`, which overrides the header directive.
44
+
45
+ ## Record/replay
46
+
47
+ ```sh
48
+ node dist/cli.js examples/triage-issues.spl --record triage.jsonl
49
+ node dist/cli.js examples/triage-issues.spl --replay triage.jsonl --silent
38
50
  ```
39
- @{model='gpt-5.5'}
40
-
41
- def fib(n) {
42
- if n < 2 { return n }
43
- return fib(n - 1) + fib(n - 2)
44
- }
45
-
46
- let joke = ?{len='sentence', fmt='object'} 'A joke about programmers' # JSON validated and parsed
47
- let why = think{level='high', show=True} 'Why is {joke.setup} funny?'
48
51
 
49
- let color = None
50
- enforce 'Answer in English' {
51
- forbid 'profanity' { let color = ?{len='word'} 'Your favorite color?' } # results leave the block
52
- }
52
+ Record creates a new file and rejects agents. Replay performs exact, ordered request matching
53
+ without starting Copilot/MCP. Local reads are permitted; writes, shell, network, agents,
54
+ and persistent stores are blocked. Recordings include prompts and raw responses and must
55
+ be protected like logs. Mismatched, missing, or unused entries fail explicitly.
53
56
 
54
- { # a bare block is a nested scope
55
- let color = 'Red' # shadows the outer variable
56
- log(color)
57
- }
57
+ ## CLI
58
58
 
59
- restrict './out' { # also: restrict net 'host,..' / restrict exec 'git,npm'
60
- let f = write_file{path='./out/a.txt', content='{fib(10)}'} 'Confirm'
61
- }
62
-
63
- try { assert fib(5) == 4 'wrong' } except err { log(err.reason) }
64
- success {'joke': joke, 'why': why, 'color': color}
59
+ ```text
60
+ spl script.spl [--model id] [--mcp file.json] [--cwd dir] [--timeout seconds]
61
+ [--store file.json | --store-module module]
62
+ [--record file.jsonl | --replay file.jsonl]
63
+ [--log-level error|info|debug] [--silent] [--check]
65
64
  ```
66
65
 
67
- Decisions worth knowing (the PRD leaves them open):
68
-
69
- * **Blocks use braces**, statements end at a newline.
70
- * **Every modification of a variable starts with `let`**: `let x = 1`, `let x += 1`, `let d['k'] = 2`, `let d.k = 3`.
71
- A bare `x = 1` is a syntax error.
72
- * **Scoping.** `let NAME = ...` binds in the current scope. Bare `{ ... }` blocks and function bodies open a new scope,
73
- so `let` there shadows an outer variable and the outer one is untouched. `if`/`elif`/`else`, `for`, `while`,
74
- `try`/`except`, `enforce`, `forbid` and `restrict` bodies share the enclosing scope, so `let` inside them updates the
75
- enclosing variable: this is how the answer of an LLM call leaves an `enforce`/`forbid`/`restrict` block. A function
76
- cannot rebind its caller's variables (use a list or dict to share state); names are read through all enclosing scopes.
77
- * **`return` exits immediately** from any scope: the innermost function, or the whole script when used at the top level
78
- (like `success`). `break`/`continue` still belong to loops.
79
- * **`?` is a prefix operator**: `?'a' + b` is `(?'a') + b`, `-?'n'` is `-(?'n')`.
80
- * **LLM commands have defaults and braces are optional**: `?'q'`, `think 'q'` (level `medium`), and tools without
81
- parameters, e.g. `read_file 'Read notes.txt and summarize it'`.
82
- * **Every string interpolates `{expr}`**. Write `{{`/`}}` or `\{`/`\}` for literal braces, or use a raw string
83
- `r'{"a": 1}'`. Values are substituted as is: strings raw, `True`/`False`/`None`, lists and dicts as JSON.
84
- * Tool calls (`read_file`, `write_file`, `web_fetch`, `web_search`, `shell`): when the script supplies every parameter
85
- the tool needs, the runtime runs it first (restriction-checked), and the output plus your instructions go to the LLM
86
- whose answer is the value. When a parameter is missing (`read_file 'summarize notes.txt'`,
87
- `write_file{path='a.txt'} 'write a haiku'`, `web_search 'latest Node.js LTS'`) the LLM calls the tool itself with the
88
- parameters you gave kept fixed. `web_search` queries DuckDuckGo and counts as network access for `restrict net`.
89
- * Builtins: `len str int float bool type list range abs round min max sum sorted reversed enumerate zip any all keys
90
- values items json_parse json_stringify log`, plus Python-style string/list/dict methods. `log(...)` writes to stderr.
91
- * Unknown modifiers/values, `break` outside loops, `try` without `except`, a modification without `let`, etc. are
92
- syntax errors (exit code 2).
93
-
94
- ## Restrictions
95
-
96
- Two layers: the LLM is told about active restrictions in the prompt, and the runtime enforces them. The LLM only gets
97
- the five runtime tools (`read_file`, `write_file`, `web_fetch`, `web_search`, `shell`) plus configured MCP tools; every call is checked against
98
- the active `restrict` scopes and any other Copilot permission request is rejected. A violation raises a
99
- `restriction_violation` failure (catchable with `try/except`).
100
-
101
- * Paths are canonicalized (symlinks resolved, `..` removed) relative to `--cwd` before the check.
102
- * `net` matches exact hosts (and `*.example.com` wildcards); redirects are re-checked on every hop. `web_search`
103
- needs `html.duckduckgo.com` to be allowed.
104
- * `exec` matches the executable name. Executables in pipelines and chains are checked against `restrict exec`,
105
- and path-like and URL arguments and redirection targets are checked against `fs`/`net`. This is best effort for
106
- programs that touch files or the network on their own.
107
- * Nested restrictions of one kind intersect and can never widen.
108
- * Clauses can be combined: `restrict './src', net 'example.com', exec 'git', mcp 'docs/search' { ... }`.
109
- Repeating a kind intersects its clauses, just like nesting.
110
- * `mcp` allows configured servers (`'docs'` or `'docs/*'`) or individual tools (`'docs/search'`).
111
- Selectors are case-sensitive; nested scopes cannot widen the configuration's tool allowlist.
112
-
113
- On Windows, `dir` switches such as `/s`, `/b` and `/a:-d` are recognized as options,
114
- not filesystem paths. Paths and redirection targets remain subject to filesystem restrictions.
115
-
116
- ## MCP tools
117
-
118
- Pass `--mcp .vscode/mcp.json` to load MCP servers in VS Code's JSON format:
119
-
120
- See the [MCP configuration reference](../docs/cli.html#mcp) for HTTP/SSE and stdio examples,
121
- all supported fields, authentication, variable expansion and validation commands.
122
- Remote servers must explicitly set `"type": "http"` or `"type": "sse"`; omitting it defaults to stdio.
123
-
124
- ```json
125
- {
126
- "servers": {
127
- "docs": {
128
- "type": "stdio",
129
- "command": "node",
130
- "args": ["${workspaceFolder}/tools/docs-server.js"],
131
- "tools": ["search"]
132
- }
133
- }
134
- }
135
- ```
66
+ Results are printed on stdout, logs on stderr. Full prompts/responses and statement traces
67
+ appear at every non-silent log level. Exit codes are `0` success, `1` script failure,
68
+ `2` syntax/usage error, and `70` internal error. `--check` does not execute modifier expressions
69
+ or compile schemas. See [CLI and MCP configuration](../docs/cli.html).
136
70
 
137
- The LLM can call these tools during `?`, `think`, and tool-backed calls. Stdio servers support `command`,
138
- `args`, `env`, and `cwd`; `http`/`sse` servers use `url` and optional `headers`. All support optional `tools`
139
- (`["*"]` or omitted means all, `[]` disables the server) and `timeout` in milliseconds.
140
- `${workspaceFolder}` expands to `--cwd`; `${env:NAME}` expands environment variables. Server working
141
- directories default to `--cwd`; relative server `cwd` values resolve against it. The config filename is
142
- relative to the invoking directory. `--check` also validates this config without starting servers.
143
- Interactive `inputs`, unsupported fields/variables, JSON comments and trailing commas are rejected.
71
+ ## Tests and embedding
144
72
 
145
- Only configured, scope-allowed MCP tools are exposed and approved. Trust your MCP servers: `restrict mcp`
146
- controls tool access, not a server's internal filesystem/network operations. Other restriction kinds do
147
- not sandbox MCP server processes or their configured connections.
148
-
149
- ## Reproducibility
150
-
151
- Control flow, arithmetic and interpolation are deterministic. The Copilot SDK has no `temperature` or `seed` setting,
152
- so the language has none either. `think` levels map to the SDK reasoning effort; if the selected model does not support
153
- it a warning is logged and the model default is used. Guard against variance with `assert` and `fmt='object'`.
154
-
155
- ## Variable store
156
-
157
- `--store vars.json` loads top-level variables from the file at start and writes them back at exit (also on failure).
158
- `--store-module ./mystore.mjs` plugs in your own store: export `createStore()` returning `{ load(): Record, save(vars) }`.
159
-
160
- ## Examples
161
-
162
- All in [examples/](examples); run from the project root, `--cwd <dir>` points a script at another project. The example
163
- syntax is checked by `tests/examples.test.ts`; `triage-issues` is also run on Copilot there.
164
-
165
- | Script | Shows |
166
- |--------|-------|
167
- | `commit-message.spl` | `shell` under `restrict exec 'git'`, validation helper, retry loop |
168
- | `mcp.spl` | MCP search via `--mcp`, combined filesystem/exec/MCP restrictions |
169
- | `triage-issues.spl` | `fmt='object'` + `assert` validation with feedback retries, aggregation, summary |
170
- | `extract-invoice.spl` | structured extraction cross-checked with arithmetic before it is accepted |
171
- | `code-review.spl` | `think` then structure, `restrict` on read and on write, `fail` as a CI gate |
172
- | `release-notes.spl` | nested `forbid`/`enforce`, git history to a file inside `./out` |
173
- | `web-digest.spl` | `restrict net`, `web_fetch`, catching a blocked second host |
174
- | `fix-failing-tests.spl` | agent loop: run `npm test`, let the LLM fix the source, repeat |
175
- | `translate-docs.spl` | `write_file` without `content` (LLM writes, path pinned), dict iteration |
176
- | `journal.spl` | state across runs with `--store` |
177
- | `basics.spl`, `live.spl`, `restrict-live.spl`, `tools-live.spl` | small feature tours |
73
+ ```sh
74
+ npm test
75
+ ```
178
76
 
179
- ## Develop
77
+ The default suite is offline. In PowerShell, opt into authenticated live checks with
78
+ `$env:SPL_LIVE_TESTS='1'; npm test`, then remove the environment variable.
180
79
 
181
- ```
182
- npm test # builds, then runs parser, interpreter, restriction and CLI tests, plus live tests on Copilot
183
- ```
80
+ The package exports `parse`, `run`, `Logger`, `CopilotBackend`, `RecordingBackend`,
81
+ `ReplayBackend`, variable stores, authentication helpers, and related types.
82
+ Call `close()` on the backend after running; replay also checks for unused entries there.
83
+ Custom backends must honor the request kind: only `agent` may execute tools.
84
+ See [embedding](../docs/internals.html#embedding).
184
85
 
185
- The live tests (`tests/live.test.ts`, one example in `tests/examples.test.ts`) call the real GitHub Copilot service and
186
- are skipped automatically when the runtime is not authenticated: run `spl login` first to include them.
86
+ ## Migration
187
87
 
188
- The VS Code extension (syntax highlighting, snippets) lives in `../vscode`.
88
+ 0.3 intentionally removes `enforce`/`forbid`, `think{level=...}`, tool-command syntax,
89
+ and `let` before compound/member/index assignments. Use `instruct`/`avoid`,
90
+ `reasoning`, ordinary raw functions, and explicit agents instead.
91
+ Bare blocks no longer create scopes. See the [migration table](../README.md#breaking-changes-in-03).
package/dist/ast.d.ts CHANGED
@@ -61,27 +61,14 @@ export type Expr = {
61
61
  lo?: Expr;
62
62
  hi?: Expr;
63
63
  } | {
64
- t: 'ask';
65
- mods: AskMods;
64
+ t: 'ask' | 'think' | 'agent';
65
+ mods: Record<string, Expr>;
66
66
  prompt: Expr;
67
- } | {
68
- t: 'think';
69
- mods: ThinkMods;
70
- prompt: Expr;
71
- } | {
72
- t: 'tool';
73
- tool: ToolName;
74
- params: Record<string, Expr>;
75
- instructions: Expr;
76
67
  };
77
68
  export interface AskMods {
78
69
  len?: 'word' | 'sentence' | 'paragraph' | 'long';
79
70
  fmt?: 'text' | 'object';
80
71
  }
81
- export interface ThinkMods {
82
- level?: 'low' | 'medium' | 'high';
83
- show?: boolean;
84
- }
85
72
  export type ToolName = 'read_file' | 'write_file' | 'web_fetch' | 'web_search' | 'shell';
86
73
  export type RestrictKind = 'fs' | 'net' | 'exec' | 'mcp';
87
74
  export type AssignTarget = {
@@ -101,6 +88,10 @@ interface Base {
101
88
  }
102
89
  export type Stmt = Base & ({
103
90
  t: 'let';
91
+ name: string;
92
+ value: Expr;
93
+ } | {
94
+ t: 'assign';
104
95
  target: AssignTarget;
105
96
  op: '=' | '+=' | '-=' | '*=' | '/=' | '//=' | '%=';
106
97
  value: Expr;
@@ -157,7 +148,7 @@ export type Stmt = Base & ({
157
148
  value?: Expr;
158
149
  } | {
159
150
  t: 'constraint';
160
- kind: 'enforce' | 'forbid';
151
+ kind: 'instruct' | 'avoid';
161
152
  text: Expr;
162
153
  body: Stmt[];
163
154
  } | {
package/dist/cli.js CHANGED
@@ -4,13 +4,15 @@ import * as path from 'node:path';
4
4
  import { parseArgs } from 'node:util';
5
5
  import { CopilotBackend } from './copilot.js';
6
6
  import { AuthError, login, logout } from './auth.js';
7
- import { SplSyntaxError } from './errors.js';
7
+ import { Failure, SplSyntaxError } from './errors.js';
8
+ import { LlmError } from './llm.js';
8
9
  import { Logger } from './logger.js';
9
10
  import { parse } from './parser.js';
10
11
  import { run } from './interpreter.js';
11
12
  import { JsonFileStore, MemoryStore, loadStoreModule } from './store.js';
12
13
  import { toJson } from './values.js';
13
14
  import { loadMcpConfig } from './mcp.js';
15
+ import { RecordingBackend, ReplayBackend } from './recording.js';
14
16
  const HELP = `spl - Semantic Prompt Language interpreter
15
17
 
16
18
  Usage: spl <script.spl> [options]
@@ -26,6 +28,8 @@ Options:
26
28
  --store <file.json> persist top-level variables in a JSON file
27
29
  --store-module <path> custom variable store: module exporting createStore() -> { load(), save(vars) }
28
30
  --timeout <seconds> per LLM call timeout (default: 300)
31
+ --record <file.jsonl> record generation responses (new file; no agents)
32
+ --replay <file.jsonl> offline generation replay; only local reads allowed
29
33
  --cwd <dir> base directory for relative paths and restrictions (default: current directory)
30
34
  --check validate script syntax and any --mcp config without executing
31
35
  -v, --version print version
@@ -79,6 +83,8 @@ async function main() {
79
83
  store: { type: 'string' },
80
84
  'store-module': { type: 'string' },
81
85
  timeout: { type: 'string' },
86
+ record: { type: 'string' },
87
+ replay: { type: 'string' },
82
88
  cwd: { type: 'string' },
83
89
  check: { type: 'boolean' },
84
90
  version: { type: 'boolean', short: 'v' },
@@ -114,6 +120,10 @@ async function main() {
114
120
  usageError('--timeout must be a positive number');
115
121
  if (values.store && values['store-module'])
116
122
  usageError('--store and --store-module are mutually exclusive');
123
+ if (values.record && values.replay)
124
+ usageError('--record and --replay are mutually exclusive');
125
+ if (values.replay && (values.store || values['store-module']))
126
+ usageError('--replay cannot use a persistent variable store');
117
127
  const cwd = path.resolve(values.cwd ?? process.cwd());
118
128
  const file = positionals[0];
119
129
  let source;
@@ -150,22 +160,46 @@ async function main() {
150
160
  catch (e) {
151
161
  return usageError(e.message);
152
162
  }
153
- const backend = new CopilotBackend({ logger, cwd, timeoutMs: timeoutSec * 1000, mcpServers });
163
+ let backend;
154
164
  try {
155
- const result = await run(program, { backend, logger, store, cwd, model: values.model });
156
- if (result.status === 'failure') {
157
- process.stdout.write(JSON.stringify(result.failure, null, 4) + '\n');
158
- return 1;
165
+ if (values.replay)
166
+ backend = new ReplayBackend(values.replay);
167
+ else {
168
+ backend = new CopilotBackend({ logger, cwd, timeoutMs: timeoutSec * 1000, mcpServers });
169
+ if (values.record)
170
+ backend = new RecordingBackend(backend, values.record);
159
171
  }
160
- if (result.hasValue) {
161
- const v = result.value;
162
- process.stdout.write((typeof v === 'string' ? v : JSON.stringify(toJson(v), null, 2)) + '\n');
163
- }
164
- return 0;
172
+ }
173
+ catch (e) {
174
+ if (!(e instanceof Error))
175
+ throw e;
176
+ return usageError(`cannot open recording: ${e.message}`);
177
+ }
178
+ let result;
179
+ try {
180
+ result = await run(program, { backend, logger, store: values.replay ? undefined : store, cwd, model: values.model });
165
181
  }
166
182
  finally {
167
- await backend.close();
183
+ try {
184
+ await backend.close();
185
+ }
186
+ catch (e) {
187
+ if (!(e instanceof LlmError))
188
+ throw e;
189
+ logger.error(e.message);
190
+ if (result?.status !== 'failure')
191
+ result = { status: 'failure', failure: new Failure('llm_error', e.message).payload() };
192
+ }
193
+ }
194
+ if (result.status === 'failure') {
195
+ process.stdout.write(JSON.stringify(result.failure, null, 4) + '\n');
196
+ return 1;
197
+ }
198
+ if (result.hasValue) {
199
+ const v = result.value;
200
+ process.stdout.write((typeof v === 'string' ? v : JSON.stringify(toJson(v), null, 2)) + '\n');
168
201
  }
202
+ return 0;
169
203
  }
170
204
  main().then((code) => process.exit(code), (e) => {
171
205
  process.stderr.write(`spl: internal error: ${e.stack ?? e}\n`);
package/dist/copilot.d.ts CHANGED
@@ -16,6 +16,7 @@ export declare class CopilotBackend implements LlmBackend {
16
16
  private models?;
17
17
  constructor(opts: CopilotBackendOptions);
18
18
  private getClient;
19
+ private denyGenerationPermission;
19
20
  private listModels;
20
21
  /** Maps the `think` level to the SDK reasoning effort, warning when the model cannot honor it. */
21
22
  private resolveEffort;
package/dist/copilot.js CHANGED
@@ -36,6 +36,11 @@ export class CopilotBackend {
36
36
  }
37
37
  return this.client;
38
38
  }
39
+ denyGenerationPermission(block) {
40
+ const failure = new Failure('restriction_violation', 'generation cannot use tools; use an explicit agent operation');
41
+ block(failure);
42
+ return { kind: 'reject', feedback: failure.reason };
43
+ }
39
44
  listModels(client) {
40
45
  this.models ??= client.listModels().catch((e) => {
41
46
  this.opts.logger.debug(`could not list models: ${e.message}`);
@@ -76,7 +81,8 @@ export class CopilotBackend {
76
81
  violation ??= failure;
77
82
  void session?.abort().catch(() => undefined);
78
83
  };
79
- const tools = TOOL_NAMES.map((name) => {
84
+ const agent = req.kind === 'agent';
85
+ const tools = (agent ? TOOL_NAMES : []).map((name) => {
80
86
  const spec = TOOL_SPECS[name];
81
87
  return defineTool(name, {
82
88
  description: spec.description,
@@ -84,8 +90,6 @@ export class CopilotBackend {
84
90
  parameters: { type: 'object', properties: spec.properties, required: spec.required, additionalProperties: false },
85
91
  handler: async (args) => {
86
92
  const params = { ...args };
87
- if (req.pinned?.tool === name)
88
- Object.assign(params, req.pinned.params);
89
93
  try {
90
94
  return await req.tools.invoke(name, params);
91
95
  }
@@ -101,15 +105,15 @@ export class CopilotBackend {
101
105
  },
102
106
  });
103
107
  });
104
- const mcpServers = filterMcpServers(this.opts.mcpServers ?? {}, req.restrictions);
108
+ const mcpServers = agent ? filterMcpServers(this.opts.mcpServers ?? {}, req.restrictions) : {};
105
109
  const baseConfig = {
106
110
  model: req.model,
107
111
  tools,
108
- availableTools: [...TOOL_NAMES, ...Object.keys(mcpServers).map((name) => `mcp:${name}-*`)],
112
+ availableTools: [...(agent ? TOOL_NAMES : []), ...Object.keys(mcpServers).map((name) => `mcp:${name}-*`)],
109
113
  mcpServers,
110
114
  systemMessage: { mode: 'append', content: SYSTEM_MESSAGE },
111
115
  workingDirectory: this.opts.cwd,
112
- onPermissionRequest: (r) => decidePermission(r, req.restrictions, block, mcpServers),
116
+ onPermissionRequest: (r) => agent ? decidePermission(r, req.restrictions, block, mcpServers) : this.denyGenerationPermission(block),
113
117
  };
114
118
  try {
115
119
  try {
package/dist/errors.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- export type StatusCode = 'assertion_failed' | 'explicit_failure' | 'invalid_format' | 'restriction_violation' | 'llm_error' | 'tool_error' | 'runtime_error';
1
+ export type StatusCode = 'assertion_failed' | 'explicit_failure' | 'invalid_format' | 'validation_failed' | 'restriction_violation' | 'llm_error' | 'tool_error' | 'runtime_error';
2
2
  export interface FailurePayload {
3
3
  status: 'failure';
4
4
  reason: string;
package/dist/index.d.ts CHANGED
@@ -3,6 +3,7 @@ export { run, type RunOptions, type RunResult } from './interpreter.js';
3
3
  export { Failure, SplSyntaxError, type FailurePayload, type StatusCode } from './errors.js';
4
4
  export { Logger, type LogLevel } from './logger.js';
5
5
  export { CopilotBackend } from './copilot.js';
6
+ export { RecordingBackend, ReplayBackend } from './recording.js';
6
7
  export { loadMcpConfig, type McpServers } from './mcp.js';
7
8
  export { login, logout, authStatus, AuthError } from './auth.js';
8
9
  export { JsonFileStore, MemoryStore, loadStoreModule, type VariableStore } from './store.js';
package/dist/index.js CHANGED
@@ -3,6 +3,7 @@ export { run } from './interpreter.js';
3
3
  export { Failure, SplSyntaxError } from './errors.js';
4
4
  export { Logger } from './logger.js';
5
5
  export { CopilotBackend } from './copilot.js';
6
+ export { RecordingBackend, ReplayBackend } from './recording.js';
6
7
  export { loadMcpConfig } from './mcp.js';
7
8
  export { login, logout, authStatus, AuthError } from './auth.js';
8
9
  export { JsonFileStore, MemoryStore, loadStoreModule } from './store.js';