@rong/agentscript 0.1.1 → 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/README.md CHANGED
@@ -2,12 +2,12 @@
2
2
 
3
3
  > **Agent context as code.**
4
4
  > `use` declares what the model can see.
5
- > `generate` defines the only LLM call site and its return shape.
5
+ > `generate` defines the only LLM call site and optional output shape.
6
6
  > Zero runtime dependencies. TypeScript-powered.
7
7
 
8
8
  ```agentscript
9
9
  use scratch.summary < 2k
10
- return generate({ input: "Answer from observations" }) -> {
10
+ generate({ input: "Answer from observations" }) -> {
11
11
  ok boolean
12
12
  text string
13
13
  }
@@ -64,7 +64,7 @@ main agent FileSummarizer {
64
64
  use input.path
65
65
  use content < 8k
66
66
 
67
- return generate({
67
+ generate({
68
68
  input: "Summarize the file for a busy teammate"
69
69
  limit: 1000
70
70
  }) -> {
@@ -93,7 +93,7 @@ Expected output (with mock LLM):
93
93
 
94
94
  With `--real-llm`, the fields are populated by the model.
95
95
 
96
- The block after `generate` is a return schema, not ordinary object construction.
96
+ The optional block after `generate` is an output schema, not ordinary object construction.
97
97
 
98
98
  ## What problem it solves
99
99
 
@@ -113,7 +113,7 @@ It is a small language for one thing:
113
113
 
114
114
  > making LLM prompt context explicit, scoped, typed, traceable, and compilable.
115
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 *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.
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
117
 
118
118
  ## How it works
119
119
 
@@ -161,7 +161,7 @@ AgentScript doesn't hardcode agent patterns as keywords. You compose them from t
161
161
  | **Reflection / Self-Improvement** | `tutorials/self-improve.as` | Query past lessons → generate → reflect → persist new lessons |
162
162
  | **Multi-Agent** | `tutorials/plan-execute.as` | Independent agents with isolated context boundaries |
163
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.
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
165
 
166
166
  ## Language at a glance
167
167
 
@@ -191,13 +191,13 @@ main agent ResearchAgent {
191
191
  done = enough(input.question, scratch)
192
192
  }
193
193
 
194
- return answer(input.question, scratch)
194
+ answer(input.question, scratch)
195
195
  }
196
196
 
197
197
  func answer(question, scratch) {
198
198
  use question
199
199
  use scratch.summary < 2k
200
- return generate({ input: "Answer using only the observations" }) -> {
200
+ generate({ input: "Answer using only the observations" }) -> {
201
201
  ok boolean
202
202
  text string
203
203
  error string
@@ -209,10 +209,11 @@ main agent ResearchAgent {
209
209
  ## Key ideas
210
210
 
211
211
  1. **`use` is explicit context** — nothing enters the LLM prompt unless `use`d
212
- 2. **`generate` is the only LLM call site** — with a required input instruction and a return shape
213
- 3. **Scope is context boundary** — functions, agents, and blocks isolate prompt visibility
214
- 4. **Tools, memory, and files are imported resources** with auditable access
215
- 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
216
217
 
217
218
  ## Why not just Python or TypeScript?
218
219
 
@@ -408,8 +408,7 @@ class Parser {
408
408
  this.consume("(");
409
409
  const options = this.parseGenerateOptions();
410
410
  this.consume(")");
411
- this.consume("->");
412
- const returnShape = this.parseShapeObject();
411
+ const returnShape = this.match("->") ? this.parseShapeObject() : undefined;
413
412
  return {
414
413
  kind: "GenerateExpr",
415
414
  options,
@@ -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);
@@ -32,7 +32,7 @@ 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
38
  }) -> {
@@ -61,7 +61,7 @@ main func(input) {
61
61
  scratch.add({ fact: "A" })
62
62
  scratch.add({ fact: "B" })
63
63
 
64
- return generate({ input: "Answer from scratch" }) -> {
64
+ generate({ input: "Answer from scratch" }) -> {
65
65
  text string
66
66
  }
67
67
  }
@@ -119,12 +119,12 @@ AgentScript 中的作用域不仅是变量可见性规则,也是 prompt contex
119
119
  ```agentscript
120
120
  func caller(input) {
121
121
  use input.goal
122
- return helper(input)
122
+ helper(input)
123
123
  }
124
124
 
125
125
  func helper(input) {
126
126
  use input.detail
127
- return generate({ input: "Work on detail" }) -> {
127
+ generate({ input: "Work on detail" }) -> {
128
128
  ok boolean
129
129
  }
130
130
  }
@@ -235,10 +235,10 @@ Instruction 是本次 LLM 调用的局部任务,不应混入长期 context。
235
235
 
236
236
  ### Output contract
237
237
 
238
- Output contract 来自 `return shape`。
238
+ Output contract 来自 `generate` 表达式上可选的 `-> { ... }` shape
239
239
 
240
240
  ```agentscript
241
- return {
241
+ generate({ input: "Answer" }) -> {
242
242
  ok boolean
243
243
  text string
244
244
  error string
@@ -260,7 +260,7 @@ use scratch.summary < 2k
260
260
  `generate({ limit: budget }) { ... }` 是 generation budget。
261
261
 
262
262
  ```agentscript
263
- return generate({
263
+ generate({
264
264
  input: "Summarize"
265
265
  limit: 500
266
266
  }) -> {
@@ -0,0 +1,197 @@
1
+ # AgentScript 隐式返回规范草案
2
+
3
+ 本文档描述 AgentScript 函数的隐式返回规则草案。
4
+
5
+ ## 1. 基本规则
6
+
7
+ 函数体最后一个顶层表达式可以作为函数返回值。
8
+
9
+ ```agentscript
10
+ func answer(input) {
11
+ use input.question
12
+
13
+ generate({ input: "Answer the question" }) -> {
14
+ ok boolean
15
+ answer string
16
+ }
17
+ }
18
+ ```
19
+
20
+ 等价于:
21
+
22
+ ```agentscript
23
+ func answer(input) {
24
+ use input.question
25
+
26
+ return generate({ input: "Answer the question" }) -> {
27
+ ok boolean
28
+ answer string
29
+ }
30
+ }
31
+ ```
32
+
33
+ 这个规则称为:
34
+
35
+ ```text
36
+ final expression return
37
+ ```
38
+
39
+ ## 2. 可隐式返回的表达式
40
+
41
+ 允许最后一行隐式返回以下表达式形式:
42
+
43
+ ```text
44
+ generate(...) -> shape
45
+ 普通函数调用
46
+ agent 调用
47
+ 变量引用
48
+ 字段访问
49
+ 索引访问
50
+ object literal
51
+ list literal
52
+ ```
53
+
54
+ 例如:
55
+
56
+ ```agentscript
57
+ func run(input) {
58
+ Worker(input)
59
+ }
60
+ ```
61
+
62
+ ```agentscript
63
+ func get_result(result) {
64
+ result.value
65
+ }
66
+ ```
67
+
68
+ ```agentscript
69
+ func observe(action) {
70
+ {
71
+ facts: [action.summary],
72
+ source: action.source
73
+ }
74
+ }
75
+ ```
76
+
77
+ ## 3. 不可隐式返回的语句
78
+
79
+ 下面这些不是表达式,不参与隐式返回:
80
+
81
+ ```text
82
+ use 声明
83
+ 赋值语句
84
+ import
85
+ loop
86
+ repeat
87
+ for
88
+ if/else,早期版本可先不表达式化
89
+ ```
90
+
91
+ 例如:
92
+
93
+ ```agentscript
94
+ func bad(input) {
95
+ use input.question
96
+ }
97
+ ```
98
+
99
+ 这里没有返回表达式,函数返回 `none`,或由 semantic analyzer 给出 warning。
100
+
101
+ 赋值也不返回:
102
+
103
+ ```agentscript
104
+ func f() {
105
+ x = answer()
106
+ }
107
+ ```
108
+
109
+ 如果要返回,需要写:
110
+
111
+ ```agentscript
112
+ func f() {
113
+ x = answer()
114
+ x
115
+ }
116
+ ```
117
+
118
+ ## 4. 显式 `return` 优先
119
+
120
+ 显式 `return` 仍然合法,并且在复杂控制流中推荐使用。
121
+
122
+ ```agentscript
123
+ func answer(input) {
124
+ if input.dry_run {
125
+ return {
126
+ ok: false,
127
+ answer: "dry run"
128
+ }
129
+ }
130
+
131
+ generate({ input: "Answer" }) -> {
132
+ ok boolean
133
+ answer string
134
+ }
135
+ }
136
+ ```
137
+
138
+ ## 5. 无返回值
139
+
140
+ 如果函数没有显式 `return`,最后一行也不是可返回表达式,则返回:
141
+
142
+ ```agentscript
143
+ none
144
+ ```
145
+
146
+ 可以显式写:
147
+
148
+ ```agentscript
149
+ return none
150
+ ```
151
+
152
+ 用于表达“此函数只产生副作用”。
153
+
154
+ ## 6. 推荐风格
155
+
156
+ 对于典型 LLM 调用,推荐省略 `return`:
157
+
158
+ ```agentscript
159
+ func summarize(content) {
160
+ use content < 8k
161
+
162
+ generate({
163
+ input: "Summarize the content"
164
+ limit: 1000
165
+ }) -> {
166
+ title string
167
+ summary string
168
+ key_points list[string]
169
+ }
170
+ }
171
+ ```
172
+
173
+ 对于分支、提前退出、错误处理,推荐使用显式 `return`:
174
+
175
+ ```agentscript
176
+ func answer(input) {
177
+ if not input.question {
178
+ return {
179
+ ok: false,
180
+ answer: ""
181
+ }
182
+ }
183
+
184
+ use input.question
185
+
186
+ generate({ input: "Answer the question" }) -> {
187
+ ok boolean
188
+ answer string
189
+ }
190
+ }
191
+ ```
192
+
193
+ ## 一句话定义
194
+
195
+ ```text
196
+ A function returns the value of its final top-level expression when no explicit return is reached.
197
+ ```
@@ -27,7 +27,7 @@ main agent Assistant {
27
27
  question string
28
28
  }) {
29
29
  use input.question
30
- return generate({ input: "Answer the question" }) -> {
30
+ generate({ input: "Answer the question" }) -> {
31
31
  ok boolean
32
32
  answer string
33
33
  }
@@ -138,7 +138,7 @@ func careful(input) {
138
138
  Shape 用于输入校验和 `generate` 输出校验:
139
139
 
140
140
  ```agentscript
141
- return generate({ input: "Extract facts" }) -> {
141
+ generate({ input: "Extract facts" }) -> {
142
142
  ok boolean
143
143
  title string
144
144
  items list[json]
@@ -177,7 +177,7 @@ use past_lessons < 2k
177
177
 
178
178
  ## Generate
179
179
 
180
- `generate` 调用当前模型,需要 `input` 指令和返回 shape
180
+ `generate` 调用当前模型,需要 `input` 指令。返回 shape 是可选的。
181
181
 
182
182
  ```agentscript
183
183
  answer = generate({
@@ -198,7 +198,8 @@ answer = generate({
198
198
  - `limit`:生成预算(数字或 `2k` 格式)。可选。
199
199
  - `attempts`:JSON 解析失败或 shape 不匹配时的重试次数。可选,默认 1。
200
200
  - `debug`:将完整 prompt 打印到 stderr。可选,默认 false。
201
- - `-> { ... }` 块声明期望的输出 shape。
201
+ - 可选的 `-> { ... }` 块声明期望的输出 shape。
202
+ - 不写 `-> { ... }` 时,`generate` 输出无约束:AgentScript 不会在 prompt 中加入返回 schema,不会要求 provider 使用结构化输出,也不会对返回值做类型强制转换或 shape 校验。
202
203
  - Provider 错误(认证、网络、超时、模型不存在)直接失败,不做重试。
203
204
  - Shape 校验包含类型强制转换(如 `"true"` -> `true`,`"42"` -> `42`)。
204
205
 
@@ -365,7 +366,7 @@ import file Config from "./config.json"
365
366
  func answer(input) {
366
367
  use Requirements < 4k
367
368
  use Config
368
- return generate({ input: "Answer from the referenced file." }) -> {
369
+ generate({ input: "Answer from the referenced file." }) -> {
369
370
  ok boolean
370
371
  answer string
371
372
  }
@@ -417,7 +418,7 @@ main agent Controller {
417
418
  result = Executor({ goal: input.goal, step: step })
418
419
  results.add(result)
419
420
  }
420
- return results.summary
421
+ results.summary
421
422
  }
422
423
  }
423
424
  ```
@@ -50,13 +50,13 @@ main agent ResearchAgent {
50
50
  question string
51
51
  }) {
52
52
  use input.question
53
- return answer(input.question)
53
+ answer(input.question)
54
54
  }
55
55
 
56
56
  func answer(question) {
57
57
  use question
58
58
 
59
- return generate({ input: "Answer the question" }) -> {
59
+ generate({ input: "Answer the question" }) -> {
60
60
  ok boolean
61
61
  text string
62
62
  }
@@ -103,7 +103,7 @@ agent A {
103
103
  model Strong
104
104
  description "Use a stronger model for this function."
105
105
 
106
- return generate({ input: "Answer carefully" }) -> {
106
+ generate({ input: "Answer carefully" }) -> {
107
107
  text string
108
108
  }
109
109
  }
@@ -130,7 +130,7 @@ V0 运行时数据以 JSON 为核心:
130
130
  `generate` 返回 shape 使用轻量标注:
131
131
 
132
132
  ```agentscript
133
- return generate({ input: "Extract facts" }) -> {
133
+ generate({ input: "Extract facts" }) -> {
134
134
  facts list[string]
135
135
  source string
136
136
  meta json
@@ -149,7 +149,7 @@ func compose(question, scratch) {
149
149
  use question
150
150
  use scratch.summary < 2k
151
151
 
152
- return generate({ input: "Answer using only the context" }) -> {
152
+ generate({ input: "Answer using only the context" }) -> {
153
153
  ok boolean
154
154
  text string
155
155
  }
@@ -113,7 +113,7 @@ for item in list < n {
113
113
  V1 不需要专门的 `plan` 类型。推荐结构:
114
114
 
115
115
  ```agentscript
116
- return generate({ input: "Create a short executable plan" }) -> {
116
+ generate({ input: "Create a short executable plan" }) -> {
117
117
  steps list[json]
118
118
  }
119
119
  ```
@@ -278,7 +278,7 @@ func answer(input) {
278
278
  use Requirements < 4k
279
279
  use input.question
280
280
 
281
- return generate({ input: "Answer from the referenced file" }) -> {
281
+ generate({ input: "Answer from the referenced file" }) -> {
282
282
  ok boolean
283
283
  text string
284
284
  }
@@ -210,7 +210,7 @@ agent Reflector {
210
210
  main func(input) {
211
211
  use input
212
212
 
213
- return generate({
213
+ generate({
214
214
  input: "Extract one reusable lesson from this run.",
215
215
  attempts: 3
216
216
  }) -> {
@@ -286,13 +286,13 @@ main agent Learner {
286
286
  ok: result.ok
287
287
  })
288
288
 
289
- return result
289
+ result
290
290
  }
291
291
 
292
292
  func reflect(run) {
293
293
  use run
294
294
 
295
- return generate({
295
+ generate({
296
296
  input: "Extract one reusable lesson from this run.",
297
297
  attempts: 3
298
298
  }) -> {
@@ -30,7 +30,7 @@ func answer(question, scratch) {
30
30
  use question
31
31
  use scratch.summary < 2k
32
32
 
33
- return generate({
33
+ generate({
34
34
  input: "Answer the question using collected facts"
35
35
  limit: 800
36
36
  }) -> {
@@ -55,7 +55,7 @@ main func(input) {
55
55
  scratch.add({ fact: "A" })
56
56
  scratch.add({ fact: "B" })
57
57
 
58
- return generate({ input: "Answer from scratch" }) -> {
58
+ generate({ input: "Answer from scratch" }) -> {
59
59
  text string
60
60
  }
61
61
  }
@@ -99,12 +99,12 @@ Each function call creates an independent context boundary.
99
99
  ```agentscript
100
100
  func caller(input) {
101
101
  use input.goal
102
- return helper(input)
102
+ helper(input)
103
103
  }
104
104
 
105
105
  func helper(input) {
106
106
  use input.detail
107
- return generate({ input: "Work on detail" }) -> {
107
+ generate({ input: "Work on detail" }) -> {
108
108
  ok boolean
109
109
  }
110
110
  }
@@ -207,10 +207,10 @@ The instruction is the per-call task. `limit`, `attempts`, and `debug` are local
207
207
 
208
208
  ### Output contract
209
209
 
210
- Comes from `return { ... }` shape.
210
+ Comes from the optional `-> { ... }` shape on a `generate` expression.
211
211
 
212
212
  ```agentscript
213
- return {
213
+ generate({ input: "Answer" }) -> {
214
214
  ok boolean
215
215
  text string
216
216
  error string
@@ -0,0 +1,197 @@
1
+ # AgentScript Final Expression Return
2
+
3
+ This document describes the implicit return rule for AgentScript functions.
4
+
5
+ ## 1. Basic rule
6
+
7
+ The final top-level expression in a function body can be used as the function return value.
8
+
9
+ ```agentscript
10
+ func answer(input) {
11
+ use input.question
12
+
13
+ generate({ input: "Answer the question" }) -> {
14
+ ok boolean
15
+ answer string
16
+ }
17
+ }
18
+ ```
19
+
20
+ This is equivalent to:
21
+
22
+ ```agentscript
23
+ func answer(input) {
24
+ use input.question
25
+
26
+ return generate({ input: "Answer the question" }) -> {
27
+ ok boolean
28
+ answer string
29
+ }
30
+ }
31
+ ```
32
+
33
+ This rule is called:
34
+
35
+ ```text
36
+ final expression return
37
+ ```
38
+
39
+ ## 2. Expressions that can be implicitly returned
40
+
41
+ The final line can implicitly return these expression forms:
42
+
43
+ ```text
44
+ generate(...) -> shape
45
+ regular function call
46
+ agent call
47
+ variable reference
48
+ field access
49
+ index access
50
+ object literal
51
+ list literal
52
+ ```
53
+
54
+ Examples:
55
+
56
+ ```agentscript
57
+ func run(input) {
58
+ Worker(input)
59
+ }
60
+ ```
61
+
62
+ ```agentscript
63
+ func get_result(result) {
64
+ result.value
65
+ }
66
+ ```
67
+
68
+ ```agentscript
69
+ func observe(action) {
70
+ {
71
+ facts: [action.summary],
72
+ source: action.source
73
+ }
74
+ }
75
+ ```
76
+
77
+ ## 3. Statements that cannot be implicitly returned
78
+
79
+ The following constructs are not expressions and do not participate in final expression return:
80
+
81
+ ```text
82
+ use declaration
83
+ assignment statement
84
+ import
85
+ loop
86
+ repeat
87
+ for
88
+ if/else, which can remain non-expression syntax in early versions
89
+ ```
90
+
91
+ Example:
92
+
93
+ ```agentscript
94
+ func bad(input) {
95
+ use input.question
96
+ }
97
+ ```
98
+
99
+ This function has no return expression. It returns `none`, or the semantic analyzer may report a warning.
100
+
101
+ Assignment also does not return a value:
102
+
103
+ ```agentscript
104
+ func f() {
105
+ x = answer()
106
+ }
107
+ ```
108
+
109
+ To return the assigned value, write:
110
+
111
+ ```agentscript
112
+ func f() {
113
+ x = answer()
114
+ x
115
+ }
116
+ ```
117
+
118
+ ## 4. Explicit `return` takes priority
119
+
120
+ Explicit `return` remains valid and is recommended for complex control flow.
121
+
122
+ ```agentscript
123
+ func answer(input) {
124
+ if input.dry_run {
125
+ return {
126
+ ok: false,
127
+ answer: "dry run"
128
+ }
129
+ }
130
+
131
+ generate({ input: "Answer" }) -> {
132
+ ok boolean
133
+ answer string
134
+ }
135
+ }
136
+ ```
137
+
138
+ ## 5. No return value
139
+
140
+ If a function has no explicit `return` and its final line is not a returnable expression, it returns:
141
+
142
+ ```agentscript
143
+ none
144
+ ```
145
+
146
+ This can also be written explicitly:
147
+
148
+ ```agentscript
149
+ return none
150
+ ```
151
+
152
+ Use this form to express that a function only produces side effects.
153
+
154
+ ## 6. Recommended style
155
+
156
+ For typical LLM calls, omit `return`:
157
+
158
+ ```agentscript
159
+ func summarize(content) {
160
+ use content < 8k
161
+
162
+ generate({
163
+ input: "Summarize the content"
164
+ limit: 1000
165
+ }) -> {
166
+ title string
167
+ summary string
168
+ key_points list[string]
169
+ }
170
+ }
171
+ ```
172
+
173
+ For branches, early exits, and error handling, use explicit `return`:
174
+
175
+ ```agentscript
176
+ func answer(input) {
177
+ if not input.question {
178
+ return {
179
+ ok: false,
180
+ answer: ""
181
+ }
182
+ }
183
+
184
+ use input.question
185
+
186
+ generate({ input: "Answer the question" }) -> {
187
+ ok boolean
188
+ answer string
189
+ }
190
+ }
191
+ ```
192
+
193
+ ## One-sentence definition
194
+
195
+ ```text
196
+ A function returns the value of its final top-level expression when no explicit return is reached.
197
+ ```
@@ -27,7 +27,7 @@ main agent Assistant {
27
27
  question string
28
28
  }) {
29
29
  use input.question
30
- return generate({ input: "Answer the question" }) -> {
30
+ generate({ input: "Answer the question" }) -> {
31
31
  ok boolean
32
32
  answer string
33
33
  }
@@ -138,7 +138,7 @@ Runtime values are JSON-oriented:
138
138
  Shapes are used for input validation and `generate` output validation:
139
139
 
140
140
  ```agentscript
141
- return generate({ input: "Extract facts" }) -> {
141
+ generate({ input: "Extract facts" }) -> {
142
142
  ok boolean
143
143
  title string
144
144
  items list[json]
@@ -177,7 +177,7 @@ use past_lessons < 2k
177
177
 
178
178
  ## Generate
179
179
 
180
- `generate` calls the current model and requires an `input` instruction plus a return shape.
180
+ `generate` calls the current model and requires an `input` instruction. A return shape is optional.
181
181
 
182
182
  ```agentscript
183
183
  answer = generate({
@@ -198,7 +198,8 @@ answer = generate({
198
198
  - `limit` is the generation budget (number or `2k` style). Optional.
199
199
  - `attempts` controls retry for JSON parse errors or shape mismatch. Optional, defaults to 1.
200
200
  - `debug` prints the full prompt to stderr. Optional, defaults to false.
201
- - The `-> { ... }` block declares the expected output shape.
201
+ - The optional `-> { ... }` block declares the expected output shape.
202
+ - Without `-> { ... }`, the generate output is unconstrained: AgentScript does not add a return schema to the prompt, does not request provider structured output, and does not coerce or validate the returned value.
202
203
  - Provider errors (auth, network, timeout, missing model) fail directly without retry.
203
204
  - Shape validation includes coercion (e.g. `"true"` -> `true`, `"42"` -> `42`).
204
205
 
@@ -365,7 +366,7 @@ import file Config from "./config.json"
365
366
  func answer(input) {
366
367
  use Requirements < 4k
367
368
  use Config
368
- return generate({ input: "Answer from the referenced file." }) -> {
369
+ generate({ input: "Answer from the referenced file." }) -> {
369
370
  ok boolean
370
371
  answer string
371
372
  }
@@ -417,7 +418,7 @@ main agent Controller {
417
418
  result = Executor({ goal: input.goal, step: step })
418
419
  results.add(result)
419
420
  }
420
- return results.summary
421
+ results.summary
421
422
  }
422
423
  }
423
424
  ```
@@ -16,7 +16,7 @@ main agent ChangelogWriter {
16
16
  use input.diff_path
17
17
  use diff < 10k
18
18
 
19
- return generate({ input: "Write a changelog from this git diff", limit: 1200 }) -> {
19
+ generate({ input: "Write a changelog from this git diff", limit: 1200 }) -> {
20
20
  title string
21
21
  highlights list[string]
22
22
  breaking_changes list[string]
@@ -17,7 +17,7 @@ main agent ApiExtractor {
17
17
  use input.url
18
18
  use response < 8k
19
19
 
20
- return generate({ input: "Extract normalized data from the API response", limit: 1200 }) -> {
20
+ generate({ input: "Extract normalized data from the API response", limit: 1200 }) -> {
21
21
  records list[json]
22
22
  fields list[string]
23
23
  warnings list[string]
@@ -26,7 +26,7 @@ main agent CodeReviewAssistant {
26
26
  use todos < 4k
27
27
  use fixmes < 4k
28
28
 
29
- return generate({ input: "Turn TODO and FIXME scan results into prioritized repair suggestions", limit: 1200 }) -> {
29
+ generate({ input: "Turn TODO and FIXME scan results into prioritized repair suggestions", limit: 1200 }) -> {
30
30
  summary string
31
31
  findings list[string]
32
32
  suggested_fixes list[string]
@@ -16,7 +16,7 @@ main agent FileSummarizer {
16
16
  use input.path
17
17
  use content < 8k
18
18
 
19
- return generate({ input: "Summarize the file for a busy teammate", limit: 1000 }) -> {
19
+ generate({ input: "Summarize the file for a busy teammate", limit: 1000 }) -> {
20
20
  title string
21
21
  summary string
22
22
  key_points list[string]
@@ -21,7 +21,7 @@ main agent MarkdownTranslator {
21
21
  use input.target_language
22
22
  use files < 4k
23
23
 
24
- return generate({ input: "Create a practical markdown translation plan", limit: 1000 }) -> {
24
+ generate({ input: "Create a practical markdown translation plan", limit: 1000 }) -> {
25
25
  target_language string
26
26
  files list[string]
27
27
  glossary_notes list[string]
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rong/agentscript",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "description": "AgentScript context engineering language runtime",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
package/tutorials/cli.as CHANGED
@@ -12,7 +12,7 @@ main agent {
12
12
  use input.name
13
13
  use input.request
14
14
 
15
- return generate({ input: "Reply to the CLI user by name", limit: 300 }) -> {
15
+ generate({ input: "Reply to the CLI user by name", limit: 300 }) -> {
16
16
  ok boolean
17
17
  message string
18
18
  }
@@ -6,7 +6,7 @@ main agent {
6
6
  description "Return a simple hello world response."
7
7
 
8
8
  main func(input {}) {
9
- return {
9
+ {
10
10
  ok: true,
11
11
  message: "Hello, AgentScript!"
12
12
  }
@@ -10,7 +10,7 @@ main agent MemoryDemo {
10
10
  topic: input.topic
11
11
  })
12
12
 
13
- return Notes.query({
13
+ Notes.query({
14
14
  kind: "note"
15
15
  text: input.topic
16
16
  limit: 3
@@ -44,7 +44,7 @@ main agent PlanAndExecute {
44
44
  }
45
45
  }
46
46
 
47
- return finish(input.goal, results)
47
+ finish(input.goal, results)
48
48
  }
49
49
 
50
50
  func run_step(goal, step, previous) {
@@ -60,7 +60,7 @@ main agent PlanAndExecute {
60
60
  result: result
61
61
  })
62
62
 
63
- return {
63
+ {
64
64
  ok: verdict.ok,
65
65
  reason: verdict.reason,
66
66
  result: {
@@ -75,7 +75,7 @@ main agent PlanAndExecute {
75
75
  use goal
76
76
  use results.summary < 2k
77
77
 
78
- return generate({ input: "Create the final answer from executed steps", limit: 800 }) -> {
78
+ generate({ input: "Create the final answer from executed steps", limit: 800 }) -> {
79
79
  ok boolean
80
80
  text string
81
81
  error string
@@ -93,7 +93,7 @@ agent Planner {
93
93
  use input.problem
94
94
  use input.previous < 1k
95
95
 
96
- return generate({ input: "Create a three step plan", limit: 600 }) -> {
96
+ generate({ input: "Create a three step plan", limit: 600 }) -> {
97
97
  step1 string
98
98
  step2 string
99
99
  step3 string
@@ -121,7 +121,7 @@ agent Executor {
121
121
 
122
122
  use observation
123
123
 
124
- return generate({ input: "Report the result of this step", limit: 500 }) -> {
124
+ generate({ input: "Report the result of this step", limit: 500 }) -> {
125
125
  ok boolean
126
126
  output json
127
127
  error string
@@ -139,7 +139,7 @@ agent Verifier {
139
139
  use input.step
140
140
  use input.result
141
141
 
142
- return generate({ input: "Verify this step result", limit: 300 }) -> {
142
+ generate({ input: "Verify this step result", limit: 300 }) -> {
143
143
  ok boolean
144
144
  reason string
145
145
  }
@@ -25,14 +25,14 @@ main agent ResearchAgent {
25
25
  done = enough(input.question, scratch)
26
26
  }
27
27
 
28
- return answer(input.question, scratch)
28
+ answer(input.question, scratch)
29
29
  }
30
30
 
31
31
  func reason(question, scratch) {
32
32
  use question
33
33
  use scratch.summary < 1k
34
34
 
35
- return generate({ input: "Choose the next search focus", limit: 300 }) -> {
35
+ generate({ input: "Choose the next search focus", limit: 300 }) -> {
36
36
  focus string
37
37
  why string
38
38
  }
@@ -42,7 +42,7 @@ main agent ResearchAgent {
42
42
  raw_query = Search.query(question, thought)
43
43
  raw_result = Search.search(raw_query)
44
44
 
45
- return {
45
+ {
46
46
  query: raw_query.summary,
47
47
  result: {
48
48
  summary: raw_result.summary,
@@ -60,7 +60,7 @@ main agent ResearchAgent {
60
60
 
61
61
  use raw
62
62
 
63
- return generate({ input: "Summarize the useful observation", limit: 400 }) -> {
63
+ generate({ input: "Summarize the useful observation", limit: 400 }) -> {
64
64
  facts list[string]
65
65
  source string
66
66
  }
@@ -74,14 +74,14 @@ main agent ResearchAgent {
74
74
  done boolean
75
75
  }
76
76
 
77
- return verdict.done
77
+ verdict.done
78
78
  }
79
79
 
80
80
  func answer(question, scratch) {
81
81
  use question
82
82
  use scratch.summary < 2k
83
83
 
84
- return generate({ input: "Answer using only the observations", limit: 800 }) -> {
84
+ generate({ input: "Answer using only the observations", limit: 800 }) -> {
85
85
  ok boolean
86
86
  text string
87
87
  error string
package/tutorials/repl.as CHANGED
@@ -22,7 +22,7 @@ main agent ReplGuide {
22
22
  }
23
23
  ]
24
24
 
25
- return {
25
+ {
26
26
  ok: true,
27
27
  title: "AgentScript REPL quick start",
28
28
  commands: commands
@@ -40,13 +40,13 @@ main agent SelfImprover {
40
40
  ok: result.ok
41
41
  })
42
42
 
43
- return result
43
+ result
44
44
  }
45
45
 
46
46
  func reflect(run) {
47
47
  use run
48
48
 
49
- return generate({
49
+ generate({
50
50
  input: "Extract one durable lesson that could improve a future run.",
51
51
  attempts: 3
52
52
  }) -> {