spl-lang 0.6.0 → 1.0.1

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.
@@ -0,0 +1,42 @@
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: a let inside if/for/enforce/forbid/restrict/try disappears when the block ends.
22
+ Declare every @name the instruction saves at the top level, then assign it inside blocks without let.
23
+ Operators: + - * / // % **, == != < <= > >=, in, not in, and, or, not.
24
+ Builtins include range, len, str, int, float, bool, keys, values, items, json_parse, json_stringify.
25
+ log(value); log(level=debug)(value); assert condition 'message'; try { ... } except err { ... }
26
+ success value ends the document successfully; fail 'reason' fails it; return value also ends it.
27
+ let answer = human 'Question' suspends via checkpoint; requires --checkpoint/--resume.
28
+ $args is a read-only list of CLI arguments; $env is the read-only environment.
29
+ ask 'prompt' and think 'prompt' generate values; agent 'task' performs model-directed actions.
30
+ Generation options: ask(fmt='json', schema=Schema, repairs=2, model='id', reasoning='high') 'prompt'.
31
+ fmt: text, json, integer, float, boolean, array, object. repairs is 0..5 (not for agent).
32
+ len: word, sentence, paragraph, long. show=True emits reasoning. Options are expression-valued.
33
+ enforce 'guidance' { ... }; forbid 'guidance' { ... } are scoped model instructions.
34
+ restrict './folder', exec 'git', net 'example.com', mcp 'server/tool' { ... } limits actions.
35
+ All existing runtime tools, functions, control flow, validation and human input are available through SPL.
36
+ Submit a complete program, not Markdown fences, not an @(model=...) header.
37
+ Example: "Read @path and store a summary in @summary" =>
38
+ let content = read_file(path)
39
+ let summary = ask 'Summarize: {content}'
40
+ Example: "@ENFORCE Be formal. Write a greeting for @name -> @greeting" =>
41
+ let greeting = None
42
+ enforce 'Be formal' { greeting = ask 'Write a greeting for {name}' }`;
@@ -1,4 +1,26 @@
1
+ import { type StatusCode } from './errors.js';
1
2
  import { type LlmBackend, type LlmRequest, type LlmResponse } from './llm.js';
3
+ interface TraceRequest {
4
+ kind: LlmRequest['kind'];
5
+ prompt: string;
6
+ model: string | null;
7
+ reasoningEffort: LlmRequest['reasoningEffort'] | null;
8
+ showReasoning: boolean;
9
+ naturalLanguage?: boolean;
10
+ }
11
+ type Outcome = {
12
+ response: LlmResponse;
13
+ } | {
14
+ error: {
15
+ status_code: StatusCode;
16
+ reason: string;
17
+ };
18
+ };
19
+ export declare function traceRequest(req: LlmRequest | TraceRequest): TraceRequest;
20
+ export declare function isResponse(value: unknown): value is LlmResponse;
21
+ export declare function isTraceError(value: unknown): value is Extract<Outcome, {
22
+ error: unknown;
23
+ }>['error'];
2
24
  export declare class RecordingBackend implements LlmBackend {
3
25
  private readonly backend;
4
26
  private readonly file;
@@ -23,3 +45,4 @@ export declare class ReplayBackend implements LlmBackend {
23
45
  call(req: LlmRequest): Promise<LlmResponse>;
24
46
  close(): Promise<void>;
25
47
  }
48
+ export {};
package/dist/recording.js CHANGED
@@ -1,10 +1,11 @@
1
1
  import * as fs from 'node:fs';
2
2
  import { Failure } from './errors.js';
3
3
  import { LlmError } from './llm.js';
4
- function traceRequest(req) {
4
+ export function traceRequest(req) {
5
5
  return {
6
6
  kind: req.kind, prompt: req.prompt, model: req.model ?? null,
7
7
  reasoningEffort: req.reasoningEffort ?? null, showReasoning: req.showReasoning,
8
+ ...(req.naturalLanguage ? { naturalLanguage: true } : {}),
8
9
  };
9
10
  }
10
11
  function requestKey(req) {
@@ -13,17 +14,19 @@ function requestKey(req) {
13
14
  function isRecord(value) {
14
15
  return typeof value === 'object' && value !== null && !Array.isArray(value);
15
16
  }
16
- function isResponse(value) {
17
+ export function isResponse(value) {
17
18
  return isRecord(value) && typeof value.text === 'string' && isRecord(value.effective)
18
- && (value.reasoning === undefined || typeof value.reasoning === 'string');
19
+ && (value.reasoning === undefined || typeof value.reasoning === 'string')
20
+ && (value.spl === undefined || typeof value.spl === 'string');
19
21
  }
20
22
  function isTraceRequest(value) {
21
23
  return isRecord(value) && (value.kind === 'ask' || value.kind === 'think' || value.kind === 'agent')
22
24
  && typeof value.prompt === 'string' && (value.model === null || typeof value.model === 'string')
23
25
  && (value.reasoningEffort === null || value.reasoningEffort === 'low' || value.reasoningEffort === 'medium' || value.reasoningEffort === 'high')
24
- && typeof value.showReasoning === 'boolean';
26
+ && typeof value.showReasoning === 'boolean'
27
+ && (value.naturalLanguage === undefined || typeof value.naturalLanguage === 'boolean');
25
28
  }
26
- function isTraceError(value) {
29
+ export function isTraceError(value) {
27
30
  const codes = [
28
31
  'assertion_failed', 'explicit_failure', 'invalid_format', 'validation_failed',
29
32
  'restriction_violation', 'llm_error', 'tool_error', 'runtime_error',
@@ -51,7 +54,7 @@ export class RecordingBackend {
51
54
  fs.writeFileSync(file, JSON.stringify({ format: 'spl-generation', version: 1 }) + '\n', { flag: 'wx', mode: 0o600 });
52
55
  }
53
56
  async call(req) {
54
- if (req.kind === 'agent')
57
+ if (req.kind === 'agent' && !req.naturalLanguage)
55
58
  throw new LlmError('agent actions cannot be recorded; record generation-only workflows');
56
59
  const request = requestKey(req);
57
60
  const response = await this.backend.call(req);
@@ -143,7 +146,7 @@ export class ReplayBackend {
143
146
  });
144
147
  }
145
148
  async call(req) {
146
- if (req.kind === 'agent' && this.mode === 'replay')
149
+ if (req.kind === 'agent' && !req.naturalLanguage && this.mode === 'replay')
147
150
  throw new LlmError('agent actions cannot be replayed');
148
151
  const entry = this.entries[this.position];
149
152
  if (!entry)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "spl-lang",
3
- "version": "0.6.0",
3
+ "version": "1.0.1",
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",
@@ -30,7 +30,9 @@
30
30
  "license": "ISC",
31
31
  "dependencies": {
32
32
  "@github/copilot-sdk": "^1.0.22",
33
- "ajv": "^8.17.1"
33
+ "ajv": "^8.17.1",
34
+ "marked": "^18.1.1",
35
+ "yaml": "^2.8.1"
34
36
  },
35
37
  "devDependencies": {
36
38
  "@types/node": "^26.6.5",
package/docs/syntax.ebnf DELETED
@@ -1,97 +0,0 @@
1
- (* SPL 0.3. { x } means repetition; [ x ] means optional.
2
- Statements end at newline, ';', '}' or EOF. Newlines inside () and [] are ignored.
3
- '#' starts a comment. Opening block braces must be on the keyword's line. *)
4
-
5
- program = { directive , separator } , { statement , separator } ;
6
- separator = NL | ";" | end_of_block | EOF ;
7
- directive = "@" , "{" , "model" , "=" , plain_string , [ "," ] , "}" ;
8
- block = "{" , { statement , separator } , "}" ;
9
-
10
- statement = declaration | assignment | if_stmt | for_stmt | while_stmt
11
- | def_stmt | try_stmt | guidance_stmt | restrict_stmt | block
12
- | "return" , [ expr ] | "success" , [ expr ]
13
- | "assert" , expr , [ expr ] | "fail" , expr
14
- | "break" | "continue" | "pass" | expr ;
15
- declaration = "let" , NAME , "=" , expr ;
16
- assignment = target , ( "=" | "+=" | "-=" | "*=" | "/=" | "//=" | "%=" ) , expr ;
17
- target = NAME | postfix , "." , NAME | postfix , "[" , expr , "]" ;
18
- if_stmt = "if" , expr , block , { NL* , "elif" , expr , block } , [ NL* , "else" , block ] ;
19
- for_stmt = "for" , NAME , { "," , NAME } , "in" , expr , block ;
20
- while_stmt = "while" , expr , block ;
21
- def_stmt = "def" , NAME , "(" , [ NAME , { "," , NAME } , [ "," ] ] , ")" , block ;
22
- try_stmt = "try" , block , NL* , "except" , NAME , block ;
23
- guidance_stmt = ( "enforce" | "forbid" ) , expr , block ;
24
- restrict_stmt = "restrict" , restrict_clause , { "," , restrict_clause } , block ;
25
- restrict_clause = [ "fs" | "net" | "exec" | "mcp" ] , expr ;
26
-
27
- (* Every block creates a lexical child scope. let declares/redeclares locally and shadows parents.
28
- Reads and assignments search outward for the first declaration; undeclared names are errors.
29
- Each loop iteration has fresh locals; caught failure names belong to the handler scope.
30
- Blocks/if yield their final expression, otherwise None. An unmatched if yields None.
31
- {} remains an empty dict; { pass } is a block yielding None. Functions need explicit return.
32
- return exits the innermost function, or the script at top level.
33
- success exits the script, including when used in a function. *)
34
-
35
- expr = or_expr , [ "if" , or_expr , "else" , expr ] ;
36
- or_expr = and_expr , { "or" , and_expr } ;
37
- and_expr = not_expr , { "and" , not_expr } ;
38
- not_expr = "not" , not_expr | comparison ;
39
- comparison = sum , { ( "==" | "!=" | "<" | "<=" | ">" | ">=" | "in" | "not" , "in" ) , sum } ;
40
- sum = term , { ( "+" | "-" ) , term } ;
41
- term = unary , { ( "*" | "/" | "//" | "%" ) , unary } ;
42
- unary = ( "-" | "+" ) , unary | "?" , [ modifiers ] , postfix | power ;
43
- power = postfix , [ "**" , unary ] ;
44
- postfix = primary , { call | index | slice | member } ;
45
- call = "(" , [ expr , { "," , expr } , [ "," ] ] , ")" ;
46
- index = "[" , expr , "]" ;
47
- slice = "[" , [ expr ] , ":" , [ expr ] , "]" ;
48
- member = "." , NAME ;
49
- primary = NUMBER | string | "True" | "False" | "None" | NAME | special_var
50
- | "(" , expr , ")" | list | dict | block | if_stmt | log_expr
51
- | ( "think" | "agent" ) , [ modifiers ] , postfix ;
52
- special_var = "$env" | "$args" ;
53
- list = "[" , [ expr , { "," , expr } , [ "," ] ] , "]" ;
54
- dict = "{" , [ expr , ":" , expr , { "," , expr , ":" , expr } , [ "," ] ] , "}" ;
55
- modifiers = "{" , [ modifier , { "," , modifier } , [ "," ] ] , "}" ;
56
- modifier = ( "len" | "fmt" | "reasoning" | "show" | "schema" | "repairs" | "model" ) , "=" , expr ;
57
- log_expr = "log" , "{" , [ "level" , "=" , log_level , [ "," ] ] , "}" , call ;
58
- log_level = "error" | "info" | "debug" | expr ;
59
-
60
- (* Unknown/duplicate modifier names are syntax errors. Values are expressions, validated at runtime:
61
- len: word/sentence/paragraph/long (guidance only)
62
- fmt: integer/float/array/object/text/boolean/json; schema implies json unless fmt is explicit
63
- Non-text formats require strict JSON of the selected type; float accepts finite numbers,
64
- object excludes arrays, and json accepts any JSON value.
65
- reasoning: low/medium/high; think and agent default to medium
66
- show: True/False; schema: synchronous strict JSON Schema draft-07 object or boolean
67
- repairs: integer 0..5 (additional generation attempts, default 0); agents require 0
68
- model: non-empty string; CLI override > per-call model > header > SDK default
69
- Generation (? and think) has no tools. agent explicitly enables restricted runtime/MCP tools.
70
- Raw tools are ordinary positional functions: read_file(path), write_file(path, content),
71
- web_fetch(url), web_search(query), shell(command). They never invoke a model.
72
- log{level=...}(args) selects error/info/debug severity; plain log(args) defaults to info.
73
- Level values are bare names, strings or expressions, validated before evaluating arguments.
74
- Debug messages are emitted only at debug log level; silent suppresses all logs.
75
- enforce/forbid are scoped prompt guidance.
76
- schema/assert validate data; restrict checks actions. *)
77
-
78
- string = [ "r" ] , quote , { char | escape | interpolation } , closing_quote ;
79
- quote = "'" | '"' | "'''" | '"""' ;
80
- plain_string = string_without_interpolation ;
81
- escape = "\" , ( "n" | "t" | "r" | "0" | "\" | "'" | '"' | "{" | "}" ) ;
82
- interpolation = "{" , expr , "}" ;
83
- NUMBER = digit+ , [ "." , digit+ ] , [ ( "e" | "E" ) , [ "+" | "-" ] , digit+ ] ;
84
- NAME = ( letter | "_" ) , { letter | digit | "_" } ;
85
- NL = newline ;
86
-
87
- (* "{{"/"}}" are literal braces; raw strings have no interpolation or escapes.
88
- $env is a read-only process-environment dict snapshot; missing keys return None.
89
- $args is a read-only list of repeated CLI --arg strings (empty by default);
90
- out-of-range integer indexes return None, and negative indexes count from the end.
91
- Both are available in every scope and interpolation, cannot be rebound or mutated
92
- (including through aliases), and are not automatically persisted.
93
- Other $-prefixed names are syntax errors. Ordinary dict/list missing reads still fail.
94
- Reserved words: let if elif else for in while def return try except assert fail success
95
- enforce forbid restrict think agent break continue pass and or not True False None.
96
- Removed syntax: think{level=...}, tool{param=...} 'instructions',
97
- let x += ... and let d.key = ... . *)