spl-lang 0.5.0 → 0.6.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/dist/cli.js CHANGED
@@ -12,7 +12,7 @@ import { run } from './interpreter.js';
12
12
  import { JsonFileStore, MemoryStore, loadStoreModule } from './store.js';
13
13
  import { toJson } from './values.js';
14
14
  import { loadMcpConfig } from './mcp.js';
15
- import { RecordingBackend, ReplayBackend } from './recording.js';
15
+ import { RecordingBackend, ReplayBackend, TraceBackend } from './recording.js';
16
16
  const HELP = `spl - Semantic Prompt Language interpreter
17
17
 
18
18
  Usage: spl <script.spl> [options]
@@ -29,8 +29,10 @@ Options:
29
29
  --store <file.json> persist top-level variables in a JSON file
30
30
  --store-module <path> custom variable store: module exporting createStore() -> { load(), save(vars) }
31
31
  --timeout <seconds> per LLM call timeout (default: 300)
32
+ --trace <file.jsonl> trace all LLM requests, responses, and failures (new file)
32
33
  --record <file.jsonl> record generation responses (new file; no agents)
33
- --replay <file.jsonl> offline generation replay; only local reads allowed
34
+ --replay <file.jsonl> replay LLM results without Copilot/MCP; explicit script tools still run
35
+ Legacy --record files allow only local reads and no stores.
34
36
  --cwd <dir> base directory for relative paths and restrictions (default: current directory)
35
37
  --check validate script syntax and any --mcp config without executing
36
38
  -v, --version print version
@@ -85,6 +87,7 @@ async function main() {
85
87
  store: { type: 'string' },
86
88
  'store-module': { type: 'string' },
87
89
  timeout: { type: 'string' },
90
+ trace: { type: 'string' },
88
91
  record: { type: 'string' },
89
92
  replay: { type: 'string' },
90
93
  cwd: { type: 'string' },
@@ -122,10 +125,12 @@ async function main() {
122
125
  usageError('--timeout must be a positive number');
123
126
  if (values.store && values['store-module'])
124
127
  usageError('--store and --store-module are mutually exclusive');
125
- if (values.record && values.replay)
126
- usageError('--record and --replay are mutually exclusive');
127
- if (values.replay && (values.store || values['store-module']))
128
- usageError('--replay cannot use a persistent variable store');
128
+ if ([values.trace, values.record, values.replay].filter((value) => value !== undefined).length > 1) {
129
+ usageError('--trace, --record, and --replay are mutually exclusive');
130
+ }
131
+ if ([values.trace, values.record, values.replay].some((value) => value !== undefined && value.trim() === '')) {
132
+ usageError('--trace, --record, and --replay require a non-empty file path');
133
+ }
129
134
  const cwd = path.resolve(values.cwd ?? process.cwd());
130
135
  const file = positionals[0];
131
136
  let source;
@@ -155,13 +160,6 @@ async function main() {
155
160
  }
156
161
  if (values.check)
157
162
  return 0;
158
- let store;
159
- try {
160
- store = values.store ? new JsonFileStore(values.store) : values['store-module'] ? await loadStoreModule(values['store-module']) : new MemoryStore();
161
- }
162
- catch (e) {
163
- return usageError(e.message);
164
- }
165
163
  let backend;
166
164
  try {
167
165
  if (values.replay)
@@ -170,6 +168,8 @@ async function main() {
170
168
  backend = new CopilotBackend({ logger, cwd, timeoutMs: timeoutSec * 1000, mcpServers });
171
169
  if (values.record)
172
170
  backend = new RecordingBackend(backend, values.record);
171
+ if (values.trace)
172
+ backend = new TraceBackend(backend, values.trace);
173
173
  }
174
174
  }
175
175
  catch (e) {
@@ -177,9 +177,19 @@ async function main() {
177
177
  throw e;
178
178
  return usageError(`cannot open recording: ${e.message}`);
179
179
  }
180
+ if (backend.mode === 'replay' && (values.store || values['store-module'])) {
181
+ return usageError('--replay of legacy --record files cannot use a persistent variable store');
182
+ }
183
+ let store;
184
+ try {
185
+ store = values.store ? new JsonFileStore(values.store) : values['store-module'] ? await loadStoreModule(values['store-module']) : new MemoryStore();
186
+ }
187
+ catch (e) {
188
+ return usageError(e.message);
189
+ }
180
190
  let result;
181
191
  try {
182
- result = await run(program, { backend, logger, store: values.replay ? undefined : store, cwd, model: values.model, args: values.arg });
192
+ result = await run(program, { backend, logger, store: backend.mode === 'replay' ? undefined : store, cwd, model: values.model, args: values.arg });
183
193
  }
184
194
  finally {
185
195
  try {
package/dist/index.d.ts CHANGED
@@ -3,7 +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
+ export { RecordingBackend, ReplayBackend, TraceBackend } from './recording.js';
7
7
  export { loadMcpConfig, type McpServers } from './mcp.js';
8
8
  export { login, logout, authStatus, AuthError } from './auth.js';
9
9
  export { JsonFileStore, MemoryStore, loadStoreModule, type VariableStore } from './store.js';
package/dist/index.js CHANGED
@@ -3,7 +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
+ export { RecordingBackend, ReplayBackend, TraceBackend } from './recording.js';
7
7
  export { loadMcpConfig } from './mcp.js';
8
8
  export { login, logout, authStatus, AuthError } from './auth.js';
9
9
  export { JsonFileStore, MemoryStore, loadStoreModule } from './store.js';
@@ -571,7 +571,7 @@ class Interpreter {
571
571
  return new ToolHost(this.restrictions, this.cwd, this.opts.backend.mode === 'replay', true).invoke(tool, params);
572
572
  }
573
573
  async evalGeneration(kind, mods, promptExpr, env) {
574
- if (kind === 'agent' && this.opts.backend.mode) {
574
+ if (kind === 'agent' && (this.opts.backend.mode === 'record' || this.opts.backend.mode === 'replay')) {
575
575
  throw new Failure('restriction_violation', 'agents cannot run in record/replay mode; record generation-only workflows');
576
576
  }
577
577
  const options = {};
package/dist/llm.d.ts CHANGED
@@ -24,7 +24,7 @@ export interface LlmResponse {
24
24
  effective: Record<string, unknown>;
25
25
  }
26
26
  export interface LlmBackend {
27
- readonly mode?: 'record' | 'replay';
27
+ readonly mode?: 'record' | 'replay' | 'trace' | 'workflow-replay';
28
28
  call(req: LlmRequest): Promise<LlmResponse>;
29
29
  close(): Promise<void>;
30
30
  }
@@ -7,8 +7,16 @@ export declare class RecordingBackend implements LlmBackend {
7
7
  call(req: LlmRequest): Promise<LlmResponse>;
8
8
  close(): Promise<void>;
9
9
  }
10
+ export declare class TraceBackend implements LlmBackend {
11
+ private readonly backend;
12
+ private readonly file;
13
+ readonly mode = "trace";
14
+ constructor(backend: LlmBackend, file: string);
15
+ call(req: LlmRequest): Promise<LlmResponse>;
16
+ close(): Promise<void>;
17
+ }
10
18
  export declare class ReplayBackend implements LlmBackend {
11
- readonly mode = "replay";
19
+ readonly mode: 'replay' | 'workflow-replay';
12
20
  private readonly entries;
13
21
  private position;
14
22
  constructor(file: string);
package/dist/recording.js CHANGED
@@ -1,14 +1,36 @@
1
1
  import * as fs from 'node:fs';
2
+ import { Failure } from './errors.js';
2
3
  import { LlmError } from './llm.js';
3
- function requestKey(req) {
4
- return JSON.stringify({
4
+ function traceRequest(req) {
5
+ return {
5
6
  kind: req.kind, prompt: req.prompt, model: req.model ?? null,
6
7
  reasoningEffort: req.reasoningEffort ?? null, showReasoning: req.showReasoning,
7
- });
8
+ };
9
+ }
10
+ function requestKey(req) {
11
+ return JSON.stringify(traceRequest(req));
8
12
  }
9
13
  function isRecord(value) {
10
14
  return typeof value === 'object' && value !== null && !Array.isArray(value);
11
15
  }
16
+ function isResponse(value) {
17
+ return isRecord(value) && typeof value.text === 'string' && isRecord(value.effective)
18
+ && (value.reasoning === undefined || typeof value.reasoning === 'string');
19
+ }
20
+ function isTraceRequest(value) {
21
+ return isRecord(value) && (value.kind === 'ask' || value.kind === 'think' || value.kind === 'agent')
22
+ && typeof value.prompt === 'string' && (value.model === null || typeof value.model === 'string')
23
+ && (value.reasoningEffort === null || value.reasoningEffort === 'low' || value.reasoningEffort === 'medium' || value.reasoningEffort === 'high')
24
+ && typeof value.showReasoning === 'boolean';
25
+ }
26
+ function isTraceError(value) {
27
+ const codes = [
28
+ 'assertion_failed', 'explicit_failure', 'invalid_format', 'validation_failed',
29
+ 'restriction_violation', 'llm_error', 'tool_error', 'runtime_error',
30
+ ];
31
+ return isRecord(value) && typeof value.reason === 'string'
32
+ && codes.some((code) => code === value.status_code);
33
+ }
12
34
  function parseLine(line, number) {
13
35
  try {
14
36
  return JSON.parse(line);
@@ -47,35 +69,81 @@ export class RecordingBackend {
47
69
  return this.backend.close();
48
70
  }
49
71
  }
72
+ export class TraceBackend {
73
+ backend;
74
+ file;
75
+ mode = 'trace';
76
+ constructor(backend, file) {
77
+ this.backend = backend;
78
+ this.file = file;
79
+ fs.writeFileSync(file, JSON.stringify({ format: 'spl-trace', version: 1 }) + '\n', { flag: 'wx', mode: 0o600 });
80
+ }
81
+ async call(req) {
82
+ const request = traceRequest(req);
83
+ let outcome;
84
+ let failure;
85
+ try {
86
+ outcome = { response: await this.backend.call(req) };
87
+ }
88
+ catch (e) {
89
+ failure = e instanceof Error ? e : new LlmError(String(e));
90
+ outcome = { error: {
91
+ status_code: e instanceof Failure ? e.statusCode : 'llm_error',
92
+ reason: e instanceof Failure ? e.reason : e instanceof Error ? e.message : String(e),
93
+ } };
94
+ }
95
+ try {
96
+ fs.appendFileSync(this.file, JSON.stringify({ request, ...outcome }) + '\n');
97
+ }
98
+ catch (e) {
99
+ if (!(e instanceof Error))
100
+ throw e;
101
+ throw new LlmError(`could not save trace: ${e.message}`);
102
+ }
103
+ if ('error' in outcome)
104
+ throw failure;
105
+ return outcome.response;
106
+ }
107
+ close() {
108
+ return this.backend.close();
109
+ }
110
+ }
50
111
  export class ReplayBackend {
51
- mode = 'replay';
112
+ mode;
52
113
  entries;
53
114
  position = 0;
54
115
  constructor(file) {
55
116
  const lines = fs.readFileSync(file, 'utf8').trimEnd().split(/\r?\n/);
56
117
  const header = parseLine(lines.shift() ?? '', 1);
57
- if (!isRecord(header) || header.format !== 'spl-generation' || header.version !== 1) {
118
+ if (!isRecord(header) || (header.format !== 'spl-generation' && header.format !== 'spl-trace') || header.version !== 1) {
58
119
  throw new LlmError('unsupported recording format/version');
59
120
  }
121
+ this.mode = header.format === 'spl-trace' ? 'workflow-replay' : 'replay';
60
122
  this.entries = lines.map((line, i) => {
61
123
  const entry = parseLine(line, i + 2);
62
- if (!isRecord(entry) || typeof entry.request !== 'string' || !isRecord(entry.response)
63
- || typeof entry.response.text !== 'string' || !isRecord(entry.response.effective)
64
- || (entry.response.reasoning !== undefined && typeof entry.response.reasoning !== 'string')) {
124
+ if (!isRecord(entry))
125
+ throw new LlmError(`invalid recording entry at line ${i + 2}`);
126
+ let request;
127
+ if (this.mode === 'workflow-replay' && isTraceRequest(entry.request)) {
128
+ request = JSON.stringify(traceRequest(entry.request));
129
+ }
130
+ else if (this.mode === 'replay' && typeof entry.request === 'string') {
131
+ request = entry.request;
132
+ }
133
+ else {
65
134
  throw new LlmError(`invalid recording entry at line ${i + 2}`);
66
135
  }
67
- return {
68
- request: entry.request,
69
- response: {
70
- text: entry.response.text,
71
- effective: entry.response.effective,
72
- reasoning: entry.response.reasoning,
73
- },
74
- };
136
+ if (isResponse(entry.response) && entry.error === undefined) {
137
+ return { request, outcome: { response: entry.response } };
138
+ }
139
+ if (this.mode === 'workflow-replay' && entry.response === undefined && isTraceError(entry.error)) {
140
+ return { request, outcome: { error: entry.error } };
141
+ }
142
+ throw new LlmError(`invalid recording entry at line ${i + 2}`);
75
143
  });
76
144
  }
77
145
  async call(req) {
78
- if (req.kind === 'agent')
146
+ if (req.kind === 'agent' && this.mode === 'replay')
79
147
  throw new LlmError('agent actions cannot be replayed');
80
148
  const entry = this.entries[this.position];
81
149
  if (!entry)
@@ -83,7 +151,11 @@ export class ReplayBackend {
83
151
  if (entry.request !== requestKey(req))
84
152
  throw new LlmError(`replay request mismatch at call ${this.position + 1}`);
85
153
  this.position++;
86
- return structuredClone(entry.response);
154
+ if ('error' in entry.outcome) {
155
+ const { status_code, reason } = entry.outcome.error;
156
+ throw new Failure(status_code, reason);
157
+ }
158
+ return structuredClone(entry.outcome.response);
87
159
  }
88
160
  async close() {
89
161
  if (this.position !== this.entries.length) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "spl-lang",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "description": "Semantic Prompt Language: a Python-like language with LLM calls built into the syntax",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",