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 +24 -14
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/interpreter.js +1 -1
- package/dist/llm.d.ts +1 -1
- package/dist/recording.d.ts +9 -1
- package/dist/recording.js +90 -18
- package/package.json +1 -1
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>
|
|
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
|
|
126
|
-
usageError('--record and --replay are mutually exclusive');
|
|
127
|
-
|
|
128
|
-
|
|
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:
|
|
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';
|
package/dist/interpreter.js
CHANGED
|
@@ -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
|
}
|
package/dist/recording.d.ts
CHANGED
|
@@ -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
|
|
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
|
|
4
|
-
return
|
|
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
|
|
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)
|
|
63
|
-
|
|
64
|
-
|
|
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
|
-
|
|
68
|
-
request: entry.
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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
|
-
|
|
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) {
|