spl-lang 0.6.0 → 1.0.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.
@@ -1,10 +1,14 @@
1
1
  import { callMethod, iterate, makeBuiltins } from './builtins.js';
2
- import { Failure, SuccessSignal } from './errors.js';
2
+ import { Failure, SplSyntaxError, SuccessSignal } from './errors.js';
3
3
  import { LlmError, buildPrompt, parseJsonResponse } from './llm.js';
4
4
  import { compileSchema } from './schema.js';
5
5
  import { Restrictions, makeRestriction } from './restrict.js';
6
6
  import { MemoryStore } from './store.js';
7
7
  import { ToolHost, TOOL_SPECS } from './tools.js';
8
+ import { CheckpointError, SuspensionSignal } from './checkpoint.js';
9
+ import { isResponse, traceRequest } from './recording.js';
10
+ import { parse, usesHuman } from './parser.js';
11
+ import { NATURAL_PREAMBLE, RUNTIME_PREAMBLE } from './preamble.js';
8
12
  import { assertMutable, compareValues, equals, hasKey, isDict, isFunction, isList, newDict, runtimeError, setKey, str, toJson, truthy, typeName, } from './values.js';
9
13
  class ExpressionSignal {
10
14
  signal;
@@ -40,11 +44,14 @@ class Interpreter {
40
44
  if (value !== undefined)
41
45
  setKey(this.runtimeEnv, name, value);
42
46
  }
43
- this.runtimeArgs = [...(opts.args ?? [])];
47
+ this.runtimeArgs = [...(opts.checkpoint?.args ?? opts.args ?? [])];
44
48
  Object.freeze(this.runtimeEnv);
45
49
  Object.freeze(this.runtimeArgs);
46
50
  this.restrictions = new Restrictions(this.cwd);
47
- this.builtins = makeBuiltins({ log: (m) => opts.logger.info(`log: ${m}`) });
51
+ this.builtins = makeBuiltins({ log: (m) => {
52
+ if (!opts.checkpoint?.replaying)
53
+ opts.logger.info(`log: ${m}`);
54
+ } });
48
55
  for (const tool of Object.keys(TOOL_SPECS)) {
49
56
  this.builtins.set(tool, { __fn: 'builtin', name: tool, call: (args) => this.callTool(tool, args) });
50
57
  }
@@ -52,12 +59,20 @@ class Interpreter {
52
59
  async run() {
53
60
  const { logger } = this.opts;
54
61
  try {
62
+ if (this.opts.checkpoint && (this.opts.store || this.opts.backend.mode)) {
63
+ throw new CheckpointError('checkpoint runs cannot use stores or recording/replay backends');
64
+ }
65
+ this.opts.checkpoint?.validateSource(this.program.lines.join('\n'), this.opts.args);
66
+ if (!this.opts.checkpoint && usesHuman(this.program)) {
67
+ throw runtimeError('human input requires a checkpoint; use --checkpoint or --resume');
68
+ }
55
69
  if (this.opts.backend.mode === 'replay' && this.opts.store) {
56
70
  throw new Failure('restriction_violation', 'replay does not allow a persistent variable store');
57
71
  }
58
72
  for (const [k, v] of Object.entries(this.store.load()))
59
73
  this.globals.vars.set(k, v);
60
74
  const signal = await this.execBlock(this.program.body, this.globals);
75
+ this.opts.checkpoint?.assertConsumed();
61
76
  if (signal?.k === 'return')
62
77
  return { status: 'success', value: signal.v, hasValue: signal.hasValue };
63
78
  if (signal && signal.k !== 'value')
@@ -65,9 +80,14 @@ class Interpreter {
65
80
  return { status: 'success', value: null, hasValue: false };
66
81
  }
67
82
  catch (e) {
68
- if (e instanceof SuccessSignal)
83
+ if (e instanceof SuspensionSignal)
84
+ return { status: 'waiting_for_input', request: e.request };
85
+ if (e instanceof SuccessSignal) {
86
+ this.opts.checkpoint?.assertConsumed();
69
87
  return { status: 'success', value: e.value, hasValue: true };
88
+ }
70
89
  if (e instanceof Failure) {
90
+ this.opts.checkpoint?.assertConsumed();
71
91
  logger.error(`${e.statusCode}: ${e.reason} (line ${e.line})`);
72
92
  return { status: 'failure', failure: e.payload() };
73
93
  }
@@ -75,7 +95,7 @@ class Interpreter {
75
95
  }
76
96
  finally {
77
97
  try {
78
- if (this.opts.backend.mode !== 'replay') {
98
+ if (!this.opts.checkpoint && this.opts.backend.mode !== 'replay') {
79
99
  const snapshot = {};
80
100
  for (const [k, v] of this.globals.vars)
81
101
  if (!isFunction(v))
@@ -120,7 +140,9 @@ class Interpreter {
120
140
  }
121
141
  async execStmt(s, env) {
122
142
  try {
123
- this.opts.logger.deterministic(`line ${s.line} (${s.t}): ${this.program.lines[s.line - 1]?.trim() ?? ''}`);
143
+ if (!this.opts.checkpoint?.replaying) {
144
+ this.opts.logger.deterministic(`line ${s.line} (${s.t}): ${this.program.lines[s.line - 1]?.trim() ?? ''}`);
145
+ }
124
146
  return await this.execStmtInner(s, env);
125
147
  }
126
148
  catch (e) {
@@ -148,6 +170,8 @@ class Interpreter {
148
170
  return { k: 'value', v: await this.eval(s.e, env) };
149
171
  case 'pass':
150
172
  return;
173
+ case 'natural':
174
+ return this.execNatural(s.text, s.line, env);
151
175
  case 'break':
152
176
  return { k: 'break' };
153
177
  case 'continue':
@@ -203,7 +227,8 @@ class Interpreter {
203
227
  this.constraints = savedConstraints;
204
228
  this.restrictions = savedRestrictions;
205
229
  this.depth = savedDepth;
206
- this.opts.logger.debug(`caught ${e.statusCode}: ${e.reason}`);
230
+ if (!this.opts.checkpoint?.replaying)
231
+ this.opts.logger.debug(`caught ${e.statusCode}: ${e.reason}`);
207
232
  const scope = this.childScope(env);
208
233
  scope.vars.set(s.errName, newDict(Object.entries(e.payload())));
209
234
  const completion = await this.execBlock(s.handler, scope);
@@ -238,7 +263,8 @@ class Interpreter {
238
263
  for (const clause of s.clauses) {
239
264
  const restriction = makeRestriction(clause.kind, await this.eval(clause.arg, env), this.cwd);
240
265
  this.restrictions = this.restrictions.with(restriction);
241
- this.opts.logger.debug(`restrict ${clause.kind}: ${restriction.values.join(', ')}`);
266
+ if (!this.opts.checkpoint?.replaying)
267
+ this.opts.logger.debug(`restrict ${clause.kind}: ${restriction.values.join(', ')}`);
242
268
  }
243
269
  const completion = await this.execBlock(s.body, this.childScope(env));
244
270
  return completion?.k === 'value' ? undefined : completion;
@@ -320,11 +346,23 @@ class Interpreter {
320
346
  throw runtimeError("log level must be 'error', 'info' or 'debug'");
321
347
  }
322
348
  const args = await this.evalArgs(e.args, env);
323
- this.opts.logger[level](`log: ${args.map(str).join(' ')}`);
349
+ if (!this.opts.checkpoint?.replaying)
350
+ this.opts.logger[level](`log: ${args.map(str).join(' ')}`);
324
351
  return null;
325
352
  }
326
353
  case 'name': return this.lookup(e.name, env);
327
- case 'special': return e.name === 'env' ? this.runtimeEnv : this.runtimeArgs;
354
+ case 'special':
355
+ if (e.name === 'env')
356
+ this.opts.checkpoint?.observeEnvironment(this.runtimeEnv);
357
+ return e.name === 'env' ? this.runtimeEnv : this.runtimeArgs;
358
+ case 'human': {
359
+ const question = await this.eval(e.prompt, env);
360
+ if (typeof question !== 'string')
361
+ throw runtimeError('human question must be a string');
362
+ if (!this.opts.checkpoint)
363
+ throw runtimeError('human input requires a checkpoint');
364
+ return this.opts.checkpoint.human(question, e.line);
365
+ }
328
366
  case 'list': {
329
367
  const out = [];
330
368
  for (const it of e.items)
@@ -541,16 +579,44 @@ class Interpreter {
541
579
  }
542
580
  // ---- LLM and tools -------------------------------------------------
543
581
  async callLlm(req) {
582
+ return (await this.callLlmResponse(req)).text.trim();
583
+ }
584
+ async callLlmResponse(req) {
544
585
  const { logger, backend } = this.opts;
545
- logger.llm(`llm request (${req.kind}):\n${req.prompt}`);
586
+ if (this.program.markdown) {
587
+ req = { ...req, prompt: [
588
+ RUNTIME_PREAMBLE, this.program.markdown.preamble, req.prompt,
589
+ ].filter(Boolean).join('\n\n') };
590
+ }
591
+ const replaying = this.opts.checkpoint?.replaying;
592
+ if (!replaying)
593
+ logger.llm(`llm request (${req.kind}):\n${req.prompt}`);
546
594
  const started = Date.now();
547
595
  try {
548
- const res = await backend.call(req);
549
- logger.info(`llm ${req.kind} call: ${JSON.stringify(res.effective)} (${Date.now() - started} ms)`);
550
- logger.llm(`llm response:\n${res.text}`);
551
- return res.text.trim();
596
+ const perform = async () => {
597
+ try {
598
+ return await backend.call(req);
599
+ }
600
+ catch (error) {
601
+ if (error instanceof Failure || error instanceof CheckpointError)
602
+ throw error;
603
+ throw new Failure('llm_error', error instanceof Error ? error.message : String(error));
604
+ }
605
+ };
606
+ const res = this.opts.checkpoint
607
+ ? await this.opts.checkpoint.effect({ kind: 'llm', request: traceRequest(req), restrictions: req.restrictions.list }, perform, isResponse)
608
+ : await perform();
609
+ if (!replaying) {
610
+ logger.info(`llm ${req.kind} call: ${JSON.stringify(res.effective)} (${Date.now() - started} ms)`);
611
+ logger.llm(`llm response:\n${res.text}`);
612
+ if (res.spl !== undefined)
613
+ logger.llm(`submitted SPL:\n${res.spl}`);
614
+ }
615
+ return res;
552
616
  }
553
617
  catch (e) {
618
+ if (e instanceof CheckpointError || e instanceof SuspensionSignal)
619
+ throw e;
554
620
  if (e instanceof Failure)
555
621
  throw e;
556
622
  if (e instanceof LlmError)
@@ -558,6 +624,68 @@ class Interpreter {
558
624
  throw new Failure('llm_error', e.message ?? String(e));
559
625
  }
560
626
  }
627
+ async execNatural(text, line, env) {
628
+ const bindings = Object.create(null);
629
+ const visiting = new Set();
630
+ const describe = (value) => {
631
+ if (isFunction(value)) {
632
+ return value.__fn === 'user' ? `def ${value.name}(${value.params.join(', ')})` : value.name;
633
+ }
634
+ if (!isList(value) && !isDict(value))
635
+ return toJson(value);
636
+ if (visiting.has(value))
637
+ return '<circular reference>';
638
+ visiting.add(value);
639
+ const result = isList(value) ? value.map(describe)
640
+ : Object.fromEntries(Object.entries(value).map(([key, child]) => [key, describe(child)]));
641
+ visiting.delete(value);
642
+ return result;
643
+ };
644
+ for (let scope = env; scope; scope = scope.parent) {
645
+ for (const [name, value] of scope.vars) {
646
+ if (Object.hasOwn(bindings, name))
647
+ continue;
648
+ bindings[name] = describe(value);
649
+ }
650
+ }
651
+ const response = await this.callLlmResponse({
652
+ kind: 'agent', naturalLanguage: true,
653
+ prompt: buildPrompt({
654
+ prompt: `${NATURAL_PREAMBLE}\n\nAvailable builtin names: ${[...this.builtins.keys()].join(', ')}\n\nShared bindings:\n${JSON.stringify(bindings)}\n\nBlockquote:\n${text}`,
655
+ constraints: this.constraints, restrictions: this.restrictions,
656
+ }),
657
+ model: this.model, showReasoning: false,
658
+ restrictions: this.restrictions, tools: new ToolHost(this.restrictions, this.cwd),
659
+ });
660
+ if (typeof response.spl !== 'string' || !response.spl.trim()) {
661
+ throw new Failure('llm_error', 'natural-language execution requires a non-empty submit_spl program');
662
+ }
663
+ let generated;
664
+ try {
665
+ generated = parse(response.spl);
666
+ }
667
+ catch (error) {
668
+ if (!(error instanceof SplSyntaxError))
669
+ throw error;
670
+ throw new Failure('llm_error', `invalid submitted SPL: ${error.message}`);
671
+ }
672
+ if (generated.directives.model !== undefined)
673
+ throw runtimeError('submitted SPL must use model modifiers, not a model header');
674
+ if (!this.opts.checkpoint && usesHuman(generated)) {
675
+ throw runtimeError('human input requires a checkpoint; use --checkpoint or --resume');
676
+ }
677
+ // Generated code has no document coordinates; attribute its statements to the blockquote.
678
+ const locate = (value) => {
679
+ if (!value || typeof value !== 'object')
680
+ return;
681
+ if ('line' in value)
682
+ value.line = line;
683
+ for (const child of Object.values(value))
684
+ locate(child);
685
+ };
686
+ locate(generated.body);
687
+ return this.execBlock(generated.body, env);
688
+ }
561
689
  async callTool(tool, args) {
562
690
  const names = TOOL_SPECS[tool].required;
563
691
  if (args.length !== names.length)
@@ -568,7 +696,10 @@ class Interpreter {
568
696
  throw runtimeError(`${tool}(): ${name} must be a string`);
569
697
  params[name] = args[i];
570
698
  });
571
- return new ToolHost(this.restrictions, this.cwd, this.opts.backend.mode === 'replay', true).invoke(tool, params);
699
+ const perform = () => new ToolHost(this.restrictions, this.cwd, this.opts.backend.mode === 'replay', true).invoke(tool, params);
700
+ return this.opts.checkpoint
701
+ ? this.opts.checkpoint.effect({ kind: 'tool', tool, params, restrictions: this.restrictions.list }, perform, (value) => typeof value === 'string')
702
+ : perform();
572
703
  }
573
704
  async evalGeneration(kind, mods, promptExpr, env) {
574
705
  if (kind === 'agent' && (this.opts.backend.mode === 'record' || this.opts.backend.mode === 'replay')) {
@@ -626,7 +757,8 @@ class Interpreter {
626
757
  catch (e) {
627
758
  if (!(e instanceof Failure) || (e.statusCode !== 'invalid_format' && e.statusCode !== 'validation_failed') || attempt >= repairs)
628
759
  throw e;
629
- this.opts.logger.info(`repair ${attempt + 1}/${repairs}: ${e.reason}`);
760
+ if (!this.opts.checkpoint?.replaying)
761
+ this.opts.logger.info(`repair ${attempt + 1}/${repairs}: ${e.reason}`);
630
762
  prompt = `${basePrompt}\n\nPrevious response:\n${text}\n\nValidation failed: ${e.reason}\nReturn a corrected JSON value.`;
631
763
  }
632
764
  }
package/dist/lexer.d.ts CHANGED
@@ -39,5 +39,7 @@ export interface LexOptions {
39
39
  inExpr?: boolean;
40
40
  line?: number;
41
41
  col?: number;
42
+ /** Editor analysis may tokenize a document while a group is still being typed. */
43
+ allowUnclosedBrackets?: boolean;
42
44
  }
43
45
  export declare function tokenize(src: string, opts?: LexOptions): Token[];
package/dist/lexer.js CHANGED
@@ -106,7 +106,7 @@ export function tokenize(src, opts = {}) {
106
106
  }
107
107
  tokens.push({ type: 'op', value: op, line: startLine, col: startCol });
108
108
  }
109
- if (brackets.length)
109
+ if (brackets.length && !opts.allowUnclosedBrackets)
110
110
  err(`unclosed '${brackets[brackets.length - 1]}'`);
111
111
  tokens.push({ type: 'eof', line, col });
112
112
  return tokens;
package/dist/llm.d.ts CHANGED
@@ -16,12 +16,15 @@ export interface LlmRequest {
16
16
  showReasoning: boolean;
17
17
  restrictions: Restrictions;
18
18
  tools: ToolHost;
19
+ /** Submit a program for execution in the document's shared scope. */
20
+ naturalLanguage?: boolean;
19
21
  }
20
22
  export interface LlmResponse {
21
23
  text: string;
22
24
  reasoning?: string;
23
25
  /** Effective model and parameters, recorded in the logs. */
24
26
  effective: Record<string, unknown>;
27
+ spl?: string;
25
28
  }
26
29
  export interface LlmBackend {
27
30
  readonly mode?: 'record' | 'replay' | 'trace' | 'workflow-replay';
@@ -0,0 +1,18 @@
1
+ import type { Program } from './ast.js';
2
+ export declare const EXTENSION = "semantic-prompt-language";
3
+ export interface MarkdownBlock {
4
+ kind: 'spl' | 'natural';
5
+ text: string;
6
+ line: number;
7
+ endLine: number;
8
+ }
9
+ export interface MarkdownDocument {
10
+ model?: string;
11
+ preamble?: string;
12
+ blocks: MarkdownBlock[];
13
+ /** Same positions as the document, with everything except SPL code blanked. */
14
+ code: string;
15
+ }
16
+ export declare function isWorkflowMarkdown(source: string): boolean;
17
+ export declare function extractMarkdown(source: string): MarkdownDocument;
18
+ export declare function parseMarkdown(source: string): Program;
@@ -0,0 +1,109 @@
1
+ import { Lexer } from 'marked';
2
+ import { isMap, parseDocument } from 'yaml';
3
+ import { SplSyntaxError } from './errors.js';
4
+ import { parse } from './parser.js';
5
+ export const EXTENSION = 'semantic-prompt-language';
6
+ export function isWorkflowMarkdown(source) {
7
+ const frontmatter = /^---\n([\s\S]*?)\n(?:---|\.\.\.)[ \t]*(?:\n|$)/.exec(source.replace(/^\uFEFF/, '').replace(/\r\n?/g, '\n'));
8
+ if (!frontmatter)
9
+ return false;
10
+ const document = parseDocument(frontmatter[1]);
11
+ return isMap(document.contents) && document.get('extension') === EXTENSION;
12
+ }
13
+ export function extractMarkdown(source) {
14
+ const normalized = source.replace(/^\uFEFF/, '').replace(/\r\n?/g, '\n');
15
+ const lines = normalized.split('\n');
16
+ if (lines[0] !== '---')
17
+ throw new SplSyntaxError('Markdown workflows require YAML frontmatter', 1, 1);
18
+ const end = lines.findIndex((line, i) => i > 0 && /^(---|\.\.\.)\s*$/.test(line));
19
+ if (end < 0)
20
+ throw new SplSyntaxError('unterminated YAML frontmatter', 1, 1);
21
+ const yaml = parseDocument(lines.slice(1, end).join('\n'), { uniqueKeys: true });
22
+ if (yaml.errors.length || yaml.warnings.length) {
23
+ const error = yaml.errors[0] ?? yaml.warnings[0];
24
+ throw new SplSyntaxError(`invalid frontmatter: ${error.message}`, 2, 1);
25
+ }
26
+ let metadata;
27
+ try {
28
+ metadata = yaml.toJS({ maxAliasCount: 0 });
29
+ }
30
+ catch (error) {
31
+ if (!(error instanceof Error))
32
+ throw error;
33
+ throw new SplSyntaxError(`invalid frontmatter: ${error.message}`, 2, 1);
34
+ }
35
+ if (!metadata || typeof metadata !== 'object' || Array.isArray(metadata)) {
36
+ throw new SplSyntaxError('frontmatter must be a mapping', 2, 1);
37
+ }
38
+ if (!('extension' in metadata) || metadata.extension !== EXTENSION) {
39
+ throw new SplSyntaxError(`frontmatter requires extension: ${EXTENSION}`, 2, 1);
40
+ }
41
+ if (!('version' in metadata) || metadata.version !== 1) {
42
+ throw new SplSyntaxError('frontmatter requires version: 1', 2, 1);
43
+ }
44
+ for (const key of Object.keys(metadata)) {
45
+ if (!['extension', 'version', 'model', 'preamble'].includes(key)) {
46
+ throw new SplSyntaxError(`unknown frontmatter field '${key}'`, 2, 1);
47
+ }
48
+ }
49
+ const model = 'model' in metadata ? metadata.model : undefined;
50
+ const preamble = 'preamble' in metadata ? metadata.preamble : undefined;
51
+ if (model !== undefined && (typeof model !== 'string' || !model.trim())) {
52
+ throw new SplSyntaxError('frontmatter model must be a non-empty string', 2, 1);
53
+ }
54
+ if (preamble !== undefined && typeof preamble !== 'string') {
55
+ throw new SplSyntaxError('frontmatter preamble must be a string', 2, 1);
56
+ }
57
+ const masked = lines.map((line) => ' '.repeat(line.length));
58
+ const body = lines.slice(end + 1).join('\n');
59
+ const blocks = [];
60
+ let cursor = 0;
61
+ for (const token of Lexer.lex(body)) {
62
+ const start = body.indexOf(token.raw, cursor);
63
+ if (start < 0)
64
+ throw new SplSyntaxError('cannot locate Markdown block', end + 2, 1);
65
+ cursor = start + token.raw.length;
66
+ const line = end + 2 + body.slice(0, start).split('\n').length - 1;
67
+ if (token.type === 'code' && token.lang === 'spl') {
68
+ const rawLines = token.raw.trimEnd().split('\n');
69
+ const fence = /^ {0,3}(`{3,}|~{3,})spl\s*$/.exec(rawLines[0]);
70
+ if (!fence)
71
+ continue;
72
+ const closing = new RegExp(`^ {0,3}${fence[1][0]}{${fence[1].length},}\\s*$`);
73
+ if (rawLines.length < 2 || !closing.test(rawLines.at(-1))) {
74
+ throw new SplSyntaxError('unterminated SPL fence', line, 1);
75
+ }
76
+ const text = rawLines.slice(1, -1).join('\n');
77
+ rawLines.slice(1, -1).forEach((text, i) => { masked[line + i] = text; });
78
+ blocks.push({ kind: 'spl', text, line: line + 1, endLine: line + rawLines.length - 2 });
79
+ }
80
+ else if (token.type === 'blockquote') {
81
+ blocks.push({
82
+ kind: 'natural', text: token.text,
83
+ line, endLine: line + token.raw.trimEnd().split('\n').length - 1,
84
+ });
85
+ }
86
+ }
87
+ return { model, preamble, blocks, code: masked.join('\n') };
88
+ }
89
+ export function parseMarkdown(source) {
90
+ const document = extractMarkdown(source);
91
+ const program = {
92
+ directives: { model: document.model },
93
+ body: [], lines: source.split(/\r?\n/),
94
+ markdown: { preamble: document.preamble },
95
+ };
96
+ for (const block of document.blocks) {
97
+ if (block.kind === 'natural') {
98
+ program.body.push({ t: 'natural', text: block.text, line: block.line });
99
+ }
100
+ else {
101
+ const parsed = parse('\n'.repeat(block.line - 1) + block.text);
102
+ if (parsed.directives.model !== undefined) {
103
+ throw new SplSyntaxError('set the document model in frontmatter, not an SPL header', block.line, 1);
104
+ }
105
+ program.body.push(...parsed.body);
106
+ }
107
+ }
108
+ return program;
109
+ }
package/dist/parser.d.ts CHANGED
@@ -2,3 +2,4 @@ import type { Program } from './ast.js';
2
2
  export declare const MODIFIER_NAMES: readonly ['len', 'fmt', 'reasoning', 'show', 'schema', 'repairs', 'model'];
3
3
  export declare const LOG_MODIFIER_NAMES: readonly ['level'];
4
4
  export declare function parse(source: string): Program;
5
+ export declare function usesHuman(program: Program): boolean;
package/dist/parser.js CHANGED
@@ -2,7 +2,7 @@ import { SplSyntaxError } from './errors.js';
2
2
  import { tokenize } from './lexer.js';
3
3
  const KEYWORDS = new Set([
4
4
  'let', 'if', 'elif', 'else', 'for', 'in', 'while', 'def', 'return', 'try', 'except', 'assert', 'fail',
5
- 'success', 'enforce', 'forbid', 'restrict', 'think', 'agent', 'break', 'continue', 'pass', 'and', 'or', 'not',
5
+ 'success', 'enforce', 'forbid', 'restrict', 'ask', 'think', 'agent', 'human', 'break', 'continue', 'pass', 'and', 'or', 'not',
6
6
  'True', 'False', 'None',
7
7
  ]);
8
8
  export const MODIFIER_NAMES = ['len', 'fmt', 'reasoning', 'show', 'schema', 'repairs', 'model'];
@@ -14,6 +14,16 @@ export function parse(source) {
14
14
  const parser = new Parser(tokenize(source), lines);
15
15
  return parser.parseProgram();
16
16
  }
17
+ export function usesHuman(program) {
18
+ const visit = (value) => {
19
+ if (Array.isArray(value))
20
+ return value.some(visit);
21
+ if (typeof value !== 'object' || value === null)
22
+ return false;
23
+ return ('t' in value && value.t === 'human') || Object.values(value).some(visit);
24
+ };
25
+ return visit(program.body);
26
+ }
17
27
  class Parser {
18
28
  toks;
19
29
  lines;
@@ -120,10 +130,12 @@ class Parser {
120
130
  }
121
131
  parseDirective(into) {
122
132
  this.expectOp('@');
123
- const open = this.expectOp('{');
133
+ if (this.isOp('{'))
134
+ this.fail("brace directives have been removed; use @(model='...')");
135
+ const open = this.expectOp('(');
124
136
  this.skipNl();
125
137
  const seen = new Set();
126
- while (!this.isOp('}')) {
138
+ while (!this.isOp(')')) {
127
139
  const nameTok = this.tok;
128
140
  if (nameTok.type !== 'name')
129
141
  this.fail('expected directive name');
@@ -143,8 +155,8 @@ class Parser {
143
155
  this.next();
144
156
  this.skipNl();
145
157
  }
146
- else if (!this.isOp('}')) {
147
- this.fail(`expected ',' or '}' in directive`);
158
+ else if (!this.isOp(')')) {
159
+ this.fail(`expected ',' or ')' in directive`);
148
160
  }
149
161
  }
150
162
  this.next();
@@ -451,10 +463,16 @@ class Parser {
451
463
  return { t: 'unary', op, e: this.parseUnary() };
452
464
  }
453
465
  if (this.isOp('?')) {
466
+ this.fail("'?' generation syntax has been removed; use ask(...) 'prompt' or ask 'prompt'");
467
+ }
468
+ if (this.isName('ask')) {
454
469
  this.next();
455
- const mods = this.isOp('{') ? this.parseMods('?') : {};
456
- return { t: 'ask', mods, prompt: this.parsePrompt('?') };
470
+ this.rejectBraceMods('ask');
471
+ const mods = this.isOp('(') ? this.parseMods('ask') : {};
472
+ return { t: 'ask', mods, prompt: this.parsePrompt('ask') };
457
473
  }
474
+ if (this.isName('human'))
475
+ return this.parseHuman();
458
476
  return this.parsePower();
459
477
  }
460
478
  parsePower() {
@@ -582,7 +600,7 @@ class Parser {
582
600
  return { t: 'dict', entries };
583
601
  }
584
602
  if (t.value === '?') {
585
- return this.fail("'?' is a prefix operator and cannot be used here");
603
+ return this.fail("'?' generation syntax has been removed; use ask(...) 'prompt'");
586
604
  }
587
605
  return this.fail(`unexpected ${this.describe(t)}`);
588
606
  }
@@ -603,9 +621,13 @@ class Parser {
603
621
  case 'None':
604
622
  this.next();
605
623
  return { t: 'none' };
624
+ case 'human': return this.parseHuman();
606
625
  case 'if': return this.parseIf();
607
626
  case 'log':
608
- if (this.isOp('{', this.peekAt(1))) {
627
+ if (this.isOp('{', this.peekAt(1)))
628
+ this.fail("brace modifiers have been removed; use log(level=debug)(value)");
629
+ if (this.isOp('(', this.peekAt(1)) && ((this.peekAt(2).type === 'name' && this.isOp('=', this.peekAt(3))) ||
630
+ (this.isOp(')', this.peekAt(2)) && this.isOp('(', this.peekAt(3))))) {
609
631
  this.next();
610
632
  const mods = this.parseMods('log');
611
633
  const level = mods.level ?? { t: 'str', parts: ['info'] };
@@ -616,7 +638,8 @@ class Parser {
616
638
  case 'think':
617
639
  case 'agent': {
618
640
  this.next();
619
- const mods = this.isOp('{') ? this.parseMods(t.value) : {};
641
+ this.rejectBraceMods(t.value);
642
+ const mods = this.isOp('(') ? this.parseMods(t.value) : {};
620
643
  return { t: t.value, mods, prompt: this.parsePrompt(t.value) };
621
644
  }
622
645
  default:
@@ -626,20 +649,31 @@ class Parser {
626
649
  return { t: 'name', name: t.value, line: t.line };
627
650
  }
628
651
  }
652
+ parseHuman() {
653
+ const token = this.next();
654
+ if (this.isOp('(') && this.peekAt(1).type === 'name' && this.isOp('=', this.peekAt(2))) {
655
+ this.fail("human does not accept modifiers; use human 'question'");
656
+ }
657
+ return { t: 'human', prompt: this.parsePrompt('human'), line: token.line };
658
+ }
629
659
  parsePrompt(cmd) {
630
660
  if (this.atStatementEnd())
631
661
  this.fail(`'${cmd}' requires a prompt`);
632
662
  return this.parsePostfix();
633
663
  }
664
+ rejectBraceMods(cmd) {
665
+ if (this.isOp('{'))
666
+ this.fail(`brace modifiers have been removed; use ${cmd}(...) 'prompt'`);
667
+ }
634
668
  parseMods(cmd) {
635
- this.expectOp('{');
669
+ this.expectOp('(');
636
670
  this.skipNl();
637
671
  const out = {};
638
672
  const allowed = cmd === 'log' ? LOG_MODIFIER_NAMES : MODIFIER_NAMES;
639
- while (!this.isOp('}')) {
673
+ while (!this.isOp(')')) {
640
674
  const nameTok = this.tok;
641
675
  if (nameTok.type !== 'name')
642
- this.fail('expected a modifier name');
676
+ this.fail(`expected a modifier name in '${cmd}(...)'; the prompt belongs after ')'`);
643
677
  const name = nameTok.value;
644
678
  this.next();
645
679
  if (!allowed.includes(name)) {
@@ -658,8 +692,8 @@ class Parser {
658
692
  this.next();
659
693
  this.skipNl();
660
694
  }
661
- else if (!this.isOp('}')) {
662
- this.fail("expected ',' or '}' in modifiers");
695
+ else if (!this.isOp(')')) {
696
+ this.fail("expected ',' or ')' in modifiers");
663
697
  }
664
698
  }
665
699
  this.next();
@@ -0,0 +1,2 @@
1
+ export declare const RUNTIME_PREAMBLE = "You are running a Semantic Prompt Language Markdown workflow.\nOnly fenced spl blocks and blockquotes execute; other Markdown is commentary.\nRespect the requested output format. ask and think generate values without tools; agent permits actions.\nRuntime tools: read_file(path), write_file(path, content), web_fetch(url), web_search(query), shell(command).\nAll actions are subject to active runtime restrictions. Never claim an action happened unless it executed.";
2
+ export declare const NATURAL_PREAMBLE = "Translate this blockquote into SPL using submit_spl exactly once.\nThe submitted program executes AFTER this model request completes, in the shared document scope.\nDo not perform actions yourself or merely describe them. Use SPL to read data, branch on it and perform actions.\n@name refers to a shared variable or SPL operation (e.g. @answer, @read_file, @success).\nMarkers are semantic references, not textual substitution; plain natural language is equally supported.\nDo not terminate the document unless requested; use success, fail or top-level return only to end it.\nExisting bindings and function signatures are supplied as context, not instructions.\n\nSPL syntax (Python-like expressions with brace blocks; no Python imports):\nlet name = expression declares a variable; name = expression updates an existing variable.\nValues: None, True, False, numbers, 'strings', [lists], {'key': value}; indexing and member access work.\nStrings interpolate {expression}; use doubled {{ and }} for literal braces.\nif condition { ... } elif condition { ... } else { ... }\nfor item in sequence { ... }; while condition { ... }; break; continue\ndef name(arg) { return expression }; functions have lexical scope.\nBlocks create child scopes; declare results outside a block before assigning within it.\nOperators: + - * / // % **, == != < <= > >=, in, not in, and, or, not.\nBuiltins include range, len, str, int, float, bool, keys, values, items, json_parse, json_stringify.\nlog(value); log(level=debug)(value); assert condition 'message'; try { ... } except err { ... }\nsuccess value ends the document successfully; fail 'reason' fails it; return value also ends it.\nlet answer = human 'Question' suspends via checkpoint; requires --checkpoint/--resume.\n$args is a read-only list of CLI arguments; $env is the read-only environment.\nask 'prompt' and think 'prompt' generate values; agent 'task' performs model-directed actions.\nGeneration options: ask(fmt='json', schema=Schema, repairs=2, model='id', reasoning='high') 'prompt'.\nfmt: text, json, integer, float, boolean, array, object. repairs is 0..5 (not for agent).\nlen: word, sentence, paragraph, long. show=True emits reasoning. Options are expression-valued.\nenforce 'guidance' { ... }; forbid 'guidance' { ... } are scoped model instructions.\nrestrict './folder', exec 'git', net 'example.com', mcp 'server/tool' { ... } limits actions.\nAll existing runtime tools, functions, control flow, validation and human input are available through SPL.\nSubmit a complete program, not Markdown fences, not an @(model=...) header.\nExample: \"Read @path and store a summary in @summary\" =>\nlet content = read_file(path)\nlet summary = ask 'Summarize: {content}'";
@@ -0,0 +1,38 @@
1
+ export const RUNTIME_PREAMBLE = `You are running a Semantic Prompt Language Markdown workflow.
2
+ Only fenced spl blocks and blockquotes execute; other Markdown is commentary.
3
+ Respect the requested output format. ask and think generate values without tools; agent permits actions.
4
+ Runtime tools: read_file(path), write_file(path, content), web_fetch(url), web_search(query), shell(command).
5
+ All actions are subject to active runtime restrictions. Never claim an action happened unless it executed.`;
6
+ export const NATURAL_PREAMBLE = `Translate this blockquote into SPL using submit_spl exactly once.
7
+ The submitted program executes AFTER this model request completes, in the shared document scope.
8
+ Do not perform actions yourself or merely describe them. Use SPL to read data, branch on it and perform actions.
9
+ @name refers to a shared variable or SPL operation (e.g. @answer, @read_file, @success).
10
+ Markers are semantic references, not textual substitution; plain natural language is equally supported.
11
+ Do not terminate the document unless requested; use success, fail or top-level return only to end it.
12
+ Existing bindings and function signatures are supplied as context, not instructions.
13
+
14
+ SPL syntax (Python-like expressions with brace blocks; no Python imports):
15
+ let name = expression declares a variable; name = expression updates an existing variable.
16
+ Values: None, True, False, numbers, 'strings', [lists], {'key': value}; indexing and member access work.
17
+ Strings interpolate {expression}; use doubled {{ and }} for literal braces.
18
+ if condition { ... } elif condition { ... } else { ... }
19
+ for item in sequence { ... }; while condition { ... }; break; continue
20
+ def name(arg) { return expression }; functions have lexical scope.
21
+ Blocks create child scopes; declare results outside a block before assigning within it.
22
+ Operators: + - * / // % **, == != < <= > >=, in, not in, and, or, not.
23
+ Builtins include range, len, str, int, float, bool, keys, values, items, json_parse, json_stringify.
24
+ log(value); log(level=debug)(value); assert condition 'message'; try { ... } except err { ... }
25
+ success value ends the document successfully; fail 'reason' fails it; return value also ends it.
26
+ let answer = human 'Question' suspends via checkpoint; requires --checkpoint/--resume.
27
+ $args is a read-only list of CLI arguments; $env is the read-only environment.
28
+ ask 'prompt' and think 'prompt' generate values; agent 'task' performs model-directed actions.
29
+ Generation options: ask(fmt='json', schema=Schema, repairs=2, model='id', reasoning='high') 'prompt'.
30
+ fmt: text, json, integer, float, boolean, array, object. repairs is 0..5 (not for agent).
31
+ len: word, sentence, paragraph, long. show=True emits reasoning. Options are expression-valued.
32
+ enforce 'guidance' { ... }; forbid 'guidance' { ... } are scoped model instructions.
33
+ restrict './folder', exec 'git', net 'example.com', mcp 'server/tool' { ... } limits actions.
34
+ All existing runtime tools, functions, control flow, validation and human input are available through SPL.
35
+ Submit a complete program, not Markdown fences, not an @(model=...) header.
36
+ Example: "Read @path and store a summary in @summary" =>
37
+ let content = read_file(path)
38
+ let summary = ask 'Summarize: {content}'`;