@rong/agentscript 0.1.0 → 0.1.2

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/INSTALL.md CHANGED
@@ -4,18 +4,18 @@ AgentScript is distributed as an npm package and can also be run from source.
4
4
 
5
5
  ## Requirements
6
6
 
7
- - Node.js compatible with the runtime features used by this project.
7
+ - Node.js >= 22.5.
8
8
  - npm.
9
9
  - Optional: Ollama, OpenAI, or Anthropic credentials when running with `--real-llm`.
10
10
 
11
- The current development setup uses Node.js 25 types and the SQLite memory backend uses Node's built-in `node:sqlite` module.
11
+ The SQLite memory backend uses Node's built-in `node:sqlite` module, so Node.js 22.5 or newer is required.
12
12
 
13
13
  ## Install from npm
14
14
 
15
15
  After the package is published:
16
16
 
17
17
  ```bash
18
- npm install -g agentscript
18
+ npm install -g @rong/agentscript
19
19
  agentscript examples/review.as --input '{"path":"src"}'
20
20
  ```
21
21
 
@@ -24,7 +24,7 @@ agentscript examples/review.as --input '{"path":"src"}'
24
24
  After the package is published:
25
25
 
26
26
  ```bash
27
- npx agentscript examples/review.as --input '{"path":"src"}'
27
+ npx @rong/agentscript examples/review.as --input '{"path":"src"}'
28
28
  ```
29
29
 
30
30
  ## Run from source
package/README.md CHANGED
@@ -1,69 +1,40 @@
1
1
  # AgentScript
2
2
 
3
- > **Prompt context as a first-class citizen.**
4
- > `use` declares what the model sees. `generate` defines what it returns.
3
+ > **Agent context as code.**
4
+ > `use` declares what the model can see.
5
+ > `generate` defines the only LLM call site and optional output shape.
5
6
  > Zero runtime dependencies. TypeScript-powered.
6
7
 
7
8
  ```agentscript
8
9
  use scratch.summary < 2k
9
- return generate({ input: "Answer from observations" }) {
10
- return { ok boolean, text string }
10
+ generate({ input: "Answer from observations" }) -> {
11
+ ok boolean
12
+ text string
11
13
  }
12
14
  ```
13
15
 
14
- [![License: ISC](https://img.shields.io/badge/license-ISC-blue.svg)](LICENSE)
16
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
15
17
  ![Zero Dependencies](https://img.shields.io/badge/dependencies-0-brightgreen)
16
- ![Node >= 25](https://img.shields.io/badge/node-%3E%3D25-green)
18
+ ![Node >= 22.5](https://img.shields.io/badge/node-%3E%3D22.5-green)
17
19
 
18
20
  [中文版](./README-CN.md)
19
21
 
20
- LLMs are stateless by nature. Each call is a fresh start. To give an agent continuity of thought, every input must be carefully assembled — what researchers and practitioners call context engineering.
21
-
22
- After building agents with Python and TypeScript, the author kept running into the same problem: prompt context management. What data actually reaches the LLM? Where does one agent's context end and another's begin? How do you audit what the model saw?
23
-
24
- AgentScript was designed to solve this — not as a general-purpose language, nor a declarative config, nor a prompt template, but as a **DSL** that mixes imperative control flow with explicit, scope-governed context declarations.
25
-
26
- It gives you two things that general-purpose languages don't: a first-class `use` keyword that declares *which* data enters the LLM prompt, and a first-class `generate` expression that defines *what* the LLM must return. Everything else — variables, functions, agents, imports, loops — exists to support this core workflow. Scopes enforce context boundaries naturally: what's `use`d in one function stays there; child scopes inherit but never leak upward.
27
-
28
- The result is a language purpose-built for composing agent patterns — ReAct, Plan-and-Execute, Reflection, Multi-Agent — where prompt context is always visible, auditable, and under your control.
29
-
30
- ## How it works
22
+ ## Install
31
23
 
32
- ```mermaid
33
- graph LR
34
- A[".as source"] --> B["Parser"]
35
- B --> C["AST"]
36
- C --> D["Semantic Analyzer"]
37
- D --> E["Runtime"]
38
- E --> F["LLM Provider<br/>(OpenAI / Anthropic / Ollama)"]
39
- E --> G["Tools<br/>(Find / Grep / File / HTTP / ...)"]
40
- E --> H["Memory<br/>(JSONL / SQLite)"]
41
- E --> I["Trace Output"]
24
+ ```bash
25
+ npm install -g @rong/agentscript
42
26
  ```
43
27
 
44
- ## Agent patterns as composable primitives
45
-
46
- AgentScript doesn't hardcode agent patterns as keywords. You compose them from the same primitives:
47
-
48
- | Pattern | Tutorial | What it demonstrates |
49
- |---------|----------|---------------------|
50
- | **ReAct** | `tutorials/react.as` | Reason → Act → Observe loop with explicit context |
51
- | **Plan-and-Execute** | `tutorials/plan-execute.as` | Generate plan, execute steps, verify, re-plan on failure |
52
- | **Reflection / Self-Improvement** | `tutorials/self-improve.as` | Query past lessons → generate → reflect → persist new lessons |
53
- | **Multi-Agent** | `tutorials/plan-execute.as` | Independent agents with isolated context boundaries |
54
-
55
- Every pattern is explicit — which data enters the prompt, which tools each agent can use, and which output shape each LLM call must satisfy.
56
-
57
- ## Install
28
+ Then run the CLI:
58
29
 
59
30
  ```bash
60
- npm install -g agentscript
31
+ agentscript --help
61
32
  ```
62
33
 
63
34
  Or run without installing:
64
35
 
65
36
  ```bash
66
- npx agentscript examples/review.as --input '{"path":"src"}'
37
+ npx @rong/agentscript examples/review.as --input '{"path":"src"}'
67
38
  ```
68
39
 
69
40
  ## Quick start
@@ -93,16 +64,14 @@ main agent FileSummarizer {
93
64
  use input.path
94
65
  use content < 8k
95
66
 
96
- return generate({
67
+ generate({
97
68
  input: "Summarize the file for a busy teammate"
98
69
  limit: 1000
99
- }) {
100
- return {
101
- title string
102
- summary string
103
- key_points list[string]
104
- action_items list[string]
105
- }
70
+ }) -> {
71
+ title string
72
+ summary string
73
+ key_points list[string]
74
+ action_items list[string]
106
75
  }
107
76
  }
108
77
  }
@@ -124,6 +93,76 @@ Expected output (with mock LLM):
124
93
 
125
94
  With `--real-llm`, the fields are populated by the model.
126
95
 
96
+ The optional block after `generate` is an output schema, not ordinary object construction.
97
+
98
+ ## What problem it solves
99
+
100
+ LLMs are stateless by nature. Each call is a fresh start. To give an agent continuity of thought, every input must be carefully assembled — what researchers and practitioners call context engineering.
101
+
102
+ After building agents with Python and TypeScript, the author kept running into the same problem: prompt context management. What data actually reaches the LLM? Where does one agent's context end and another's begin? How do you audit what the model saw?
103
+
104
+ ## What makes AgentScript different?
105
+
106
+ AgentScript is not:
107
+
108
+ - a prompt template
109
+ - a YAML config format
110
+ - a general-purpose agent framework
111
+
112
+ It is a small language for one thing:
113
+
114
+ > making LLM prompt context explicit, scoped, typed, traceable, and compilable.
115
+
116
+ It gives you two things that general-purpose languages don't: a first-class `use` keyword that declares *which* data enters the LLM prompt, and a first-class `generate` expression that defines an LLM call with an optional output contract. Everything else — variables, functions, agents, imports, loops — exists to support this core workflow. Scopes enforce context boundaries naturally: what's `use`d in one function stays there; child scopes inherit but never leak upward. Functions can also return their final top-level expression directly, which keeps typical LLM workflows concise.
117
+
118
+ ## How it works
119
+
120
+ ```mermaid
121
+ graph LR
122
+ A[".as source"] --> B["Parser"]
123
+ B --> C["AST"]
124
+ C --> D["Semantic Analyzer"]
125
+ D --> E["Runtime"]
126
+ E --> F["LLM Provider<br/>(OpenAI / Anthropic / Ollama)"]
127
+ E --> G["Tools<br/>(Find / Grep / File / HTTP / ...)"]
128
+ E --> H["Memory<br/>(JSONL / SQLite)"]
129
+ E --> I["Trace Output"]
130
+ ```
131
+
132
+ ## Status
133
+
134
+ AgentScript is experimental.
135
+
136
+ Currently implemented:
137
+
138
+ - parser
139
+ - semantic checker
140
+ - mock runtime
141
+ - OpenAI / Anthropic / Ollama LLM adapters
142
+ - file and environment tools
143
+ - JSONL and SQLite memory backends
144
+ - trace output
145
+
146
+ Planned:
147
+
148
+ - stable IR
149
+ - richer diagnostics
150
+ - VS Code syntax support
151
+ - package publishing hardening
152
+
153
+ ## Agent patterns as composable primitives
154
+
155
+ AgentScript doesn't hardcode agent patterns as keywords. You compose them from the same primitives:
156
+
157
+ | Pattern | Tutorial | What it demonstrates |
158
+ |---------|----------|---------------------|
159
+ | **ReAct** | `tutorials/react.as` | Reason → Act → Observe loop with explicit context |
160
+ | **Plan-and-Execute** | `tutorials/plan-execute.as` | Generate plan, execute steps, verify, re-plan on failure |
161
+ | **Reflection / Self-Improvement** | `tutorials/self-improve.as` | Query past lessons → generate → reflect → persist new lessons |
162
+ | **Multi-Agent** | `tutorials/plan-execute.as` | Independent agents with isolated context boundaries |
163
+
164
+ Every pattern is explicit — which data enters the prompt, which tools each agent can use, and which output shape each LLM call must satisfy when one is declared.
165
+
127
166
  ## Language at a glance
128
167
 
129
168
  ```agentscript
@@ -152,18 +191,16 @@ main agent ResearchAgent {
152
191
  done = enough(input.question, scratch)
153
192
  }
154
193
 
155
- return answer(input.question, scratch)
194
+ answer(input.question, scratch)
156
195
  }
157
196
 
158
197
  func answer(question, scratch) {
159
198
  use question
160
199
  use scratch.summary < 2k
161
- return generate({ input: "Answer using only the observations" }) {
162
- return {
163
- ok boolean
164
- text string
165
- error string
166
- }
200
+ generate({ input: "Answer using only the observations" }) -> {
201
+ ok boolean
202
+ text string
203
+ error string
167
204
  }
168
205
  }
169
206
  }
@@ -172,10 +209,11 @@ main agent ResearchAgent {
172
209
  ## Key ideas
173
210
 
174
211
  1. **`use` is explicit context** — nothing enters the LLM prompt unless `use`d
175
- 2. **`generate` is the only LLM call site** — with a required input instruction and a return shape
176
- 3. **Scope is context boundary** — functions, agents, and blocks isolate prompt visibility
177
- 4. **Tools, memory, and files are imported resources** with auditable access
178
- 5. **Trace is built in** every `generate` and `use` is recorded for debugging
212
+ 2. **`generate` is the only LLM call site** — with a required input instruction and optional output shape
213
+ 3. **Final expression return keeps flows concise** — a function returns its final top-level expression
214
+ 4. **Scope is context boundary** functions, agents, and blocks isolate prompt visibility
215
+ 5. **Tools, memory, and files are imported resources** with auditable access
216
+ 6. **Trace is built in** — every `generate` and `use` is recorded for debugging
179
217
 
180
218
  ## Why not just Python or TypeScript?
181
219
 
@@ -408,10 +408,7 @@ class Parser {
408
408
  this.consume("(");
409
409
  const options = this.parseGenerateOptions();
410
410
  this.consume(")");
411
- this.consume("{");
412
- this.consume("return");
413
- const returnShape = this.parseShapeObject();
414
- this.consume("}");
411
+ const returnShape = this.match("->") ? this.parseShapeObject() : undefined;
415
412
  return {
416
413
  kind: "GenerateExpr",
417
414
  options,
@@ -40,6 +40,7 @@ const SYMBOLS = new Set([
40
40
  "=",
41
41
  "!",
42
42
  "<",
43
+ "-",
43
44
  "*"
44
45
  ]);
45
46
  export function tokenize(source) {
@@ -77,7 +78,7 @@ class Scanner {
77
78
  }
78
79
  if (SYMBOLS.has(char)) {
79
80
  const twoChar = `${char}${this.peekNext()}`;
80
- if (twoChar === "==" || twoChar === "!=") {
81
+ if (twoChar === "==" || twoChar === "!=" || twoChar === "->") {
81
82
  this.advance();
82
83
  this.advance();
83
84
  tokens.push({
@@ -19,7 +19,8 @@ export async function callAnthropic(request, parsed, options, fetchImpl, timeout
19
19
  "x-api-key": apiKey,
20
20
  "anthropic-version": "2023-06-01",
21
21
  });
22
- return parseJsonText(readAnthropicText(response));
22
+ const text = readAnthropicText(response);
23
+ return request.returnShape ? parseJsonText(text) : text;
23
24
  }
24
25
  function readAnthropicText(value) {
25
26
  if (!value || typeof value !== "object" || Array.isArray(value) || !Array.isArray(value.content)) {
@@ -4,16 +4,19 @@ export async function callOllama(request, parsed, fetchImpl, timeoutMs, baseUrl)
4
4
  model: parsed.model,
5
5
  stream: false,
6
6
  think: false,
7
- format: request.builtContext.returnSchema,
8
7
  messages: [
9
8
  { role: "system", content: request.builtContext.system },
10
9
  { role: "user", content: request.builtContext.finalUserMessage },
11
10
  ],
12
11
  };
12
+ if (request.builtContext.returnSchema) {
13
+ body.format = request.builtContext.returnSchema;
14
+ }
13
15
  const maxTokens = budgetToTokenLimit(request);
14
16
  if (maxTokens) {
15
17
  body.options = { num_predict: maxTokens };
16
18
  }
17
19
  const response = await postJson(fetchImpl, `${baseUrl}/api/chat`, body, timeoutMs);
18
- return parseJsonText(readPath(response, ["message", "content"]));
20
+ const text = readPath(response, ["message", "content"]);
21
+ return request.returnShape ? parseJsonText(text) : text;
19
22
  }
@@ -11,15 +11,17 @@ export async function callOpenAI(request, parsed, options, fetchImpl, timeoutMs,
11
11
  { role: "system", content: request.builtContext.system },
12
12
  { role: "user", content: request.builtContext.finalUserMessage },
13
13
  ],
14
- response_format: {
14
+ };
15
+ if (request.builtContext.returnSchema) {
16
+ body.response_format = {
15
17
  type: "json_schema",
16
18
  json_schema: {
17
19
  name: "agentscript_generate",
18
20
  strict: true,
19
21
  schema: request.builtContext.returnSchema,
20
22
  },
21
- },
22
- };
23
+ };
24
+ }
23
25
  const maxTokens = budgetToTokenLimit(request);
24
26
  if (maxTokens) {
25
27
  body.max_completion_tokens = maxTokens;
@@ -27,5 +29,6 @@ export async function callOpenAI(request, parsed, options, fetchImpl, timeoutMs,
27
29
  const response = await postJson(fetchImpl, `${baseUrl}/chat/completions`, body, timeoutMs, {
28
30
  authorization: `Bearer ${apiKey}`,
29
31
  });
30
- return parseJsonText(readPath(response, ["choices", 0, "message", "content"]));
32
+ const text = readPath(response, ["choices", 0, "message", "content"]);
33
+ return request.returnShape ? parseJsonText(text) : text;
31
34
  }
@@ -2,7 +2,7 @@ import { sanitizeForJson } from "../../runtime/json.js";
2
2
  import { buildValueFromShape } from "../../runtime/shape.js";
3
3
  export class MockLlmProvider {
4
4
  async generate(request) {
5
- return buildValueFromShape(request.returnShape);
5
+ return request.returnShape ? buildValueFromShape(request.returnShape) : null;
6
6
  }
7
7
  }
8
8
  export class MockToolProvider {
@@ -4,7 +4,7 @@ export function buildContext(input) {
4
4
  const instruction = sanitizeForJson(input.instruction);
5
5
  const instructionText = renderJson(instruction);
6
6
  const system = buildSystemPrompt(input.agentName, input.identity);
7
- const returnSchema = shapeToSchema(input.returnShape);
7
+ const returnSchema = input.returnShape ? shapeToSchema(input.returnShape) : undefined;
8
8
  return {
9
9
  agentName: input.agentName,
10
10
  model: input.model,
@@ -76,7 +76,9 @@ function buildFinalUserMessage(context, instructionText, returnSchema) {
76
76
  }
77
77
  }
78
78
  sections.push("Instruction:", instructionText);
79
- sections.push("Return JSON matching this schema:", renderJson(returnSchema));
79
+ if (returnSchema) {
80
+ sections.push("Return JSON matching this schema:", renderJson(returnSchema));
81
+ }
80
82
  return sections.join("\n");
81
83
  }
82
84
  function renderJson(value) {
@@ -153,7 +155,7 @@ export function builtContextToJson(context) {
153
155
  clippedSize: item.clippedSize,
154
156
  })),
155
157
  instruction: context.instruction,
156
- returnSchema: context.returnSchema,
158
+ returnSchema: context.returnSchema ?? null,
157
159
  budget: budgetToJson(context.budget),
158
160
  finalUserMessage: context.finalUserMessage,
159
161
  };
@@ -58,8 +58,10 @@ export class GenerateRuntime {
58
58
  continue;
59
59
  }
60
60
  try {
61
- const result = coerceValueToShape(rawResult, expr.returnShape);
62
- validateValueAgainstShape(result, expr.returnShape, expr.range);
61
+ const result = expr.returnShape ? coerceValueToShape(rawResult, expr.returnShape) : rawResult;
62
+ if (expr.returnShape) {
63
+ validateValueAgainstShape(result, expr.returnShape, expr.range);
64
+ }
63
65
  this.trace.push({
64
66
  kind: "generate",
65
67
  data: {
@@ -151,11 +151,8 @@ class Interpreter {
151
151
  this.callDepth += 1;
152
152
  const scope = await this.buildFunctionScope(agent, fn, args);
153
153
  try {
154
- const signal = await this.executeBlock(fn.body, scope);
155
- if (!signal) {
156
- throw new RuntimeError(`Function '${agent.name}.${name}' completed without return`, fn.range);
157
- }
158
- return signal.value;
154
+ const signal = await this.executeBlock(fn.body, scope, true);
155
+ return signal?.value ?? null;
159
156
  }
160
157
  finally {
161
158
  this.callDepth -= 1;
@@ -194,8 +191,11 @@ class Interpreter {
194
191
  findFunction(agent, name) {
195
192
  return agent.functions.find((fn) => fn.name === name);
196
193
  }
197
- async executeBlock(statements, scope) {
198
- for (const stmt of statements) {
194
+ async executeBlock(statements, scope, allowFinalExpressionReturn = false) {
195
+ for (const [index, stmt] of statements.entries()) {
196
+ if (allowFinalExpressionReturn && index === statements.length - 1 && stmt.kind === "ExprStmt") {
197
+ return { kind: "return", value: await this.evaluator.evaluate(stmt.expr, scope) };
198
+ }
199
199
  const result = await this.executeStatement(stmt, scope);
200
200
  if (result)
201
201
  return result;
@@ -366,7 +366,9 @@ class Analyzer {
366
366
  this.checkExpression(expr.options.input, scope);
367
367
  }
368
368
  this.checkGenerateInput(expr);
369
- this.checkShapeObject(expr.returnShape);
369
+ if (expr.returnShape) {
370
+ this.checkShapeObject(expr.returnShape);
371
+ }
370
372
  this.checkGenerateConfig("model", expr, scope);
371
373
  this.checkGenerateConfig("role", expr, scope);
372
374
  this.checkGenerateConfig("description", expr, scope);
@@ -10,13 +10,13 @@ AgentScript 表面上有变量、函数、循环和 Agent 调用,但它的主
10
10
 
11
11
  AgentScript 的控制流服务于 prompt context 构建。
12
12
 
13
- 普通语句负责组织数据、调用工具、调用 Agent 和更新中间状态。LLM 调用只能通过 `generate(...) { return ... }` 发生,而 `generate` 能看到的上下文必须由 `use` 显式声明。
13
+ 普通语句负责组织数据、调用工具、调用 Agent 和更新中间状态。LLM 调用只能通过 `generate(...) -> { ... }` 发生,而 `generate` 能看到的上下文必须由 `use` 显式声明。
14
14
 
15
15
  AgentScript 的核心对象是:
16
16
 
17
17
  - `Data`:普通变量、JSON、list、file import、tool observation 和 Agent 返回值。
18
18
  - `Context Source`:由 `use expr < budget` 声明的 prompt context 来源。
19
- - `Generation Site`:由 `generate({ input, limit, attempts, debug }) { return shape }` 声明的一次 LLM 调用。
19
+ - `Generation Site`:由 `generate({ input, limit, attempts, debug }) -> shape` 声明的一次 LLM 调用。
20
20
  - `Boundary`:由 Agent、function 和 block scope 形成的 context 可见性边界。
21
21
 
22
22
  ## `use` 的语义
@@ -32,15 +32,13 @@ func answer(question, scratch) {
32
32
  use question
33
33
  use scratch.summary < 2k
34
34
 
35
- return generate({
35
+ generate({
36
36
  input: "Answer the question using collected facts"
37
37
  limit: 800
38
- }) {
39
- return {
40
- ok boolean
41
- text string
42
- error string
43
- }
38
+ }) -> {
39
+ ok boolean
40
+ text string
41
+ error string
44
42
  }
45
43
  }
46
44
  ```
@@ -63,10 +61,8 @@ main func(input) {
63
61
  scratch.add({ fact: "A" })
64
62
  scratch.add({ fact: "B" })
65
63
 
66
- return generate({ input: "Answer from scratch" }) {
67
- return {
68
- text string
69
- }
64
+ generate({ input: "Answer from scratch" }) -> {
65
+ text string
70
66
  }
71
67
  }
72
68
  ```
@@ -123,15 +119,13 @@ AgentScript 中的作用域不仅是变量可见性规则,也是 prompt contex
123
119
  ```agentscript
124
120
  func caller(input) {
125
121
  use input.goal
126
- return helper(input)
122
+ helper(input)
127
123
  }
128
124
 
129
125
  func helper(input) {
130
126
  use input.detail
131
- return generate({ input: "Work on detail" }) {
132
- return {
133
- ok boolean
134
- }
127
+ generate({ input: "Work on detail" }) -> {
128
+ ok boolean
135
129
  }
136
130
  }
137
131
  ```
@@ -171,10 +165,8 @@ result = Worker({
171
165
  if condition {
172
166
  temp = compute(input)
173
167
  use temp
174
- result = generate({ input: "Use temp" }) {
175
- return {
176
- ok boolean
177
- }
168
+ result = generate({ input: "Use temp" }) -> {
169
+ ok boolean
178
170
  }
179
171
  }
180
172
  ```
@@ -231,11 +223,9 @@ source label 有助于模型理解 context,也有助于人类审计。
231
223
  Instruction 层来自 `generate(...)` 参数对象中的 `input` 字段。
232
224
 
233
225
  ```agentscript
234
- generate({ input: "Answer the question using only collected facts" }) {
235
- return {
236
- ok boolean
237
- text string
238
- }
226
+ generate({ input: "Answer the question using only collected facts" }) -> {
227
+ ok boolean
228
+ text string
239
229
  }
240
230
  ```
241
231
 
@@ -245,10 +235,10 @@ Instruction 是本次 LLM 调用的局部任务,不应混入长期 context。
245
235
 
246
236
  ### Output contract
247
237
 
248
- Output contract 来自 `return shape`。
238
+ Output contract 来自 `generate` 表达式上可选的 `-> { ... }` shape
249
239
 
250
240
  ```agentscript
251
- return {
241
+ generate({ input: "Answer" }) -> {
252
242
  ok boolean
253
243
  text string
254
244
  error string
@@ -270,13 +260,11 @@ use scratch.summary < 2k
270
260
  `generate({ limit: budget }) { ... }` 是 generation budget。
271
261
 
272
262
  ```agentscript
273
- return generate({
263
+ generate({
274
264
  input: "Summarize"
275
265
  limit: 500
276
- }) {
277
- return {
278
- text string
279
- }
266
+ }) -> {
267
+ text string
280
268
  }
281
269
  ```
282
270