@rong/agentscript 0.1.3 → 0.1.5

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/CHANGELOG.md CHANGED
@@ -2,6 +2,30 @@
2
2
 
3
3
  All notable changes to AgentScript will be documented in this file.
4
4
 
5
+ ## 0.1.5 - 2026-05-09
6
+
7
+ ### Changed
8
+
9
+ - Switched context budgets from `use expr < budget` to `use expr max budget`.
10
+ - Switched loop and for-in iteration limits to `max` syntax.
11
+ - Restored `<` as an ordinary numeric comparison operator.
12
+ - Required comma separators between object literal fields.
13
+ - Updated current docs, examples, tutorials, fixtures, and tests for the new syntax.
14
+
15
+ ## 0.1.4 - 2026-05-08
16
+
17
+ ### Added
18
+
19
+ - Added `max_output` as the explicit `generate` output budget.
20
+ - Added `temperature`, `think`, and `strict` generate options.
21
+ - Added provider request mapping for generate provider hints.
22
+
23
+ ### Changed
24
+
25
+ - Replaced generate `limit` usage with `max_output` in current docs, examples, tutorials, and fixtures.
26
+ - Expanded English and Chinese `generate` design docs with configuration semantics.
27
+ - Updated shape validation so `strict: true` disables coercion and rejects extra fields.
28
+
5
29
  ## 0.1.3 - 2026-05-08
6
30
 
7
31
  ### Added
package/README.md CHANGED
@@ -6,8 +6,10 @@
6
6
  > Zero runtime dependencies. TypeScript-powered.
7
7
 
8
8
  ```agentscript
9
- use scratch.summary < 2k as observations
10
- generate({ input: "Answer from observations" }) -> {
9
+ use scratch.summary max 2k as observations
10
+ generate({
11
+ input: "Answer from observations"
12
+ }) -> {
11
13
  ok boolean
12
14
  text string
13
15
  }
@@ -60,13 +62,15 @@ main agent FileSummarizer {
60
62
  description "Read one local file and produce a useful structured summary."
61
63
 
62
64
  main func(input { path string }) {
63
- content = File.read({ path: input.path })
65
+ content = File.read({
66
+ path: input.path
67
+ })
64
68
  use input.path as source path
65
- use content < 8k as file content
69
+ use content max 8k as file content
66
70
 
67
71
  generate({
68
- input: "Summarize the file for a busy teammate"
69
- limit: 1000
72
+ input: "Summarize the file for a busy teammate",
73
+ max_output: 1000
70
74
  }) -> {
71
75
  title string
72
76
  summary string
@@ -181,10 +185,10 @@ main agent ResearchAgent {
181
185
  use input.question as user question
182
186
 
183
187
  scratch = []
184
- use scratch.summary < 2k as observations
188
+ use scratch.summary max 2k as observations
185
189
 
186
190
  done = false
187
- loop until done < 6 {
191
+ loop until done max 6 {
188
192
  thought = reason(input.question, scratch)
189
193
  obs = Search.search(thought.focus)
190
194
  scratch.add(obs)
@@ -196,8 +200,10 @@ main agent ResearchAgent {
196
200
 
197
201
  func answer(question, scratch) {
198
202
  use question as user question
199
- use scratch.summary < 2k as observations
200
- generate({ input: "Answer using only the observations" }) -> {
203
+ use scratch.summary max 2k as observations
204
+ generate({
205
+ input: "Answer using only the observations"
206
+ }) -> {
201
207
  ok boolean
202
208
  text string
203
209
  error string
@@ -254,8 +260,8 @@ agentscript examples/review.as --quiet # value only, no trace
254
260
 
255
261
  | Language | Links |
256
262
  |----------|-------|
257
- | English | [Language Reference](docs/en/language.md) · [Context Engineering](docs/en/context-engineering.md) · [Design History](docs/design-history/) |
258
- | 中文 | [README-CN](./README-CN.md) · [语言参考](docs/cn/language.md) · [Context Engineering](docs/cn/context-engineering.md) |
263
+ | English | [Language Reference](docs/en/language.md) · [Context Engineering](docs/en/context-engineering.md) · [`use ... as ...`](docs/en/use-as.md) · [`generate`](docs/en/generate.md) · [Design History](docs/design-history/) |
264
+ | 中文 | [README-CN](./README-CN.md) · [语言参考](docs/cn/language.md) · [Context Engineering](docs/cn/context-engineering.md) · [`use ... as ...`](docs/cn/use-as.md) · [`generate`](docs/cn/generate.md) |
259
265
 
260
266
  ### Design principles
261
267
 
@@ -281,4 +287,4 @@ Zero runtime dependencies. Built with TypeScript.
281
287
 
282
288
  ## License
283
289
 
284
- MIT
290
+ MIT
@@ -175,7 +175,7 @@ class Parser {
175
175
  parseUse() {
176
176
  const start = this.consume("use").range.start;
177
177
  const value = this.parseLogicalOr();
178
- const budget = this.match("<") ? this.parseBudget() : undefined;
178
+ const budget = this.match("max") ? this.parseBudget() : undefined;
179
179
  const label = this.match("as") ? this.parseUseLabel() : undefined;
180
180
  return {
181
181
  kind: "UseStmt",
@@ -228,7 +228,7 @@ class Parser {
228
228
  const item = this.consumeIdentifier("Expected for item name");
229
229
  this.consume("in");
230
230
  const iterable = this.parseLogicalOr();
231
- this.consume("<");
231
+ this.consume("max");
232
232
  const maxIterations = this.parsePositiveInteger("Expected for iteration count");
233
233
  const body = this.parseBlock();
234
234
  return {
@@ -245,7 +245,7 @@ class Parser {
245
245
  const start = this.consume("loop").range.start;
246
246
  this.consume("until");
247
247
  const condition = this.parseLogicalOr();
248
- this.consume("<");
248
+ this.consume("max");
249
249
  const maxIterations = this.parsePositiveInteger("Expected loop iteration count");
250
250
  const body = this.parseBlock();
251
251
  return {
@@ -299,7 +299,10 @@ class Parser {
299
299
  return this.parseBinaryExpression(() => this.parseEquality(), "and");
300
300
  }
301
301
  parseEquality() {
302
- return this.parseBinaryExpression(() => this.parseUnary(), "==", "!=");
302
+ return this.parseBinaryExpression(() => this.parseComparison(), "==", "!=");
303
+ }
304
+ parseComparison() {
305
+ return this.parseBinaryExpression(() => this.parseUnary(), "<");
303
306
  }
304
307
  parseBinaryExpression(parseOperand, ...operators) {
305
308
  let expr = parseOperand();
@@ -434,16 +437,19 @@ class Parser {
434
437
  const properties = [];
435
438
  let input;
436
439
  let attempts;
437
- let limit;
440
+ let maxOutput;
441
+ let temperature;
442
+ let think;
443
+ let strict;
438
444
  let debug;
439
445
  while (!this.check("}") && !this.isAtEnd()) {
440
446
  const propStart = this.peek().range.start;
441
447
  const key = this.consumeObjectKey();
442
448
  this.consume(":");
443
449
  let value;
444
- if (key === "limit") {
445
- const token = this.consumeKind("number", "Expected generate limit");
446
- limit = this.parseBudgetToken(token);
450
+ if (key === "max_output") {
451
+ const token = this.consumeKind("number", "Expected generate max_output");
452
+ maxOutput = this.parseBudgetToken(token);
447
453
  value = {
448
454
  kind: "NumberExpr",
449
455
  value: Number.parseFloat(token.value),
@@ -466,10 +472,19 @@ class Parser {
466
472
  else if (key === "attempts" && value.kind === "NumberExpr") {
467
473
  attempts = value;
468
474
  }
475
+ else if (key === "temperature" && value.kind === "NumberExpr") {
476
+ temperature = value;
477
+ }
478
+ else if (key === "think" && (value.kind === "BooleanExpr" || value.kind === "StringExpr")) {
479
+ think = value;
480
+ }
481
+ else if (key === "strict" && value.kind === "BooleanExpr") {
482
+ strict = value;
483
+ }
469
484
  else if (key === "debug" && value.kind === "BooleanExpr") {
470
485
  debug = value;
471
486
  }
472
- this.match(",");
487
+ this.consumePropertySeparator("}");
473
488
  }
474
489
  this.consume("}");
475
490
  return {
@@ -477,7 +492,10 @@ class Parser {
477
492
  properties,
478
493
  input,
479
494
  attempts,
480
- limit,
495
+ maxOutput,
496
+ temperature,
497
+ think,
498
+ strict,
481
499
  debug,
482
500
  range: { start, end: this.previous().range.end }
483
501
  };
@@ -496,7 +514,7 @@ class Parser {
496
514
  value,
497
515
  range: { start: propStart, end: value.range.end }
498
516
  });
499
- this.match(",");
517
+ this.consumePropertySeparator("}");
500
518
  }
501
519
  this.consume("}");
502
520
  return {
@@ -505,6 +523,11 @@ class Parser {
505
523
  range: { start, end: this.previous().range.end }
506
524
  };
507
525
  }
526
+ consumePropertySeparator(terminator) {
527
+ if (this.check(terminator))
528
+ return;
529
+ this.consume(",");
530
+ }
508
531
  parseList() {
509
532
  const start = this.consume("[").range.start;
510
533
  const items = this.parseCommaSeparatedUntil("]", () => this.parseExpression());
@@ -25,7 +25,8 @@ const KEYWORDS = new Set([
25
25
  "number",
26
26
  "boolean",
27
27
  "json",
28
- "list"
28
+ "list",
29
+ "max"
29
30
  ]);
30
31
  const SYMBOLS = new Set([
31
32
  "{",
@@ -5,7 +5,7 @@ export async function callAnthropic(request, parsed, options, fetchImpl, timeout
5
5
  if (!apiKey) {
6
6
  throw new RuntimeError("ANTHROPIC_API_KEY is required for anthropic:// models");
7
7
  }
8
- const response = await postJson(fetchImpl, `${baseUrl}/messages`, {
8
+ const body = {
9
9
  model: parsed.model,
10
10
  max_tokens: budgetToTokenLimit(request) ?? 1024,
11
11
  system: request.builtContext.system,
@@ -15,7 +15,11 @@ export async function callAnthropic(request, parsed, options, fetchImpl, timeout
15
15
  content: request.builtContext.finalUserMessage,
16
16
  },
17
17
  ],
18
- }, timeoutMs, {
18
+ };
19
+ if (request.temperature !== undefined) {
20
+ body.temperature = request.temperature;
21
+ }
22
+ const response = await postJson(fetchImpl, `${baseUrl}/messages`, body, timeoutMs, {
19
23
  "x-api-key": apiKey,
20
24
  "anthropic-version": "2023-06-01",
21
25
  });
@@ -3,7 +3,7 @@ export async function callOllama(request, parsed, fetchImpl, timeoutMs, baseUrl)
3
3
  const body = {
4
4
  model: parsed.model,
5
5
  stream: false,
6
- think: false,
6
+ think: request.think ?? false,
7
7
  messages: [
8
8
  { role: "system", content: request.builtContext.system },
9
9
  { role: "user", content: request.builtContext.finalUserMessage },
@@ -13,8 +13,15 @@ export async function callOllama(request, parsed, fetchImpl, timeoutMs, baseUrl)
13
13
  body.format = request.builtContext.returnSchema;
14
14
  }
15
15
  const maxTokens = budgetToTokenLimit(request);
16
+ const options = {};
16
17
  if (maxTokens) {
17
- body.options = { num_predict: maxTokens };
18
+ options.num_predict = maxTokens;
19
+ }
20
+ if (request.temperature !== undefined) {
21
+ options.temperature = request.temperature;
22
+ }
23
+ if (Object.keys(options).length > 0) {
24
+ body.options = options;
18
25
  }
19
26
  const response = await postJson(fetchImpl, `${baseUrl}/api/chat`, body, timeoutMs);
20
27
  const text = readPath(response, ["message", "content"]);
@@ -17,11 +17,18 @@ export async function callOpenAI(request, parsed, options, fetchImpl, timeoutMs,
17
17
  type: "json_schema",
18
18
  json_schema: {
19
19
  name: "agentscript_generate",
20
- strict: true,
20
+ strict: request.strict,
21
21
  schema: request.builtContext.returnSchema,
22
22
  },
23
23
  };
24
24
  }
25
+ if (request.temperature !== undefined) {
26
+ body.temperature = request.temperature;
27
+ }
28
+ const reasoningEffort = openAIReasoningEffort(request.think);
29
+ if (reasoningEffort) {
30
+ body.reasoning_effort = reasoningEffort;
31
+ }
25
32
  const maxTokens = budgetToTokenLimit(request);
26
33
  if (maxTokens) {
27
34
  body.max_completion_tokens = maxTokens;
@@ -32,3 +39,10 @@ export async function callOpenAI(request, parsed, options, fetchImpl, timeoutMs,
32
39
  const text = readPath(response, ["choices", 0, "message", "content"]);
33
40
  return request.returnShape ? parseJsonText(text) : text;
34
41
  }
42
+ function openAIReasoningEffort(think) {
43
+ if (think === true)
44
+ return "medium";
45
+ if (think === "low" || think === "medium" || think === "high")
46
+ return think;
47
+ return undefined;
48
+ }
@@ -1,12 +1,12 @@
1
1
  import { RuntimeError } from "../../runtime/errors.js";
2
2
  export function budgetToTokenLimit(request) {
3
- if (!request.budget) {
3
+ if (!request.maxOutput) {
4
4
  return undefined;
5
5
  }
6
- if (request.budget.unit === "k") {
7
- return Math.max(1, Math.floor(request.budget.amount * 1000));
6
+ if (request.maxOutput.unit === "k") {
7
+ return Math.max(1, Math.floor(request.maxOutput.amount * 1000));
8
8
  }
9
- return Math.max(1, Math.floor(request.budget.amount));
9
+ return Math.max(1, Math.floor(request.maxOutput.amount));
10
10
  }
11
11
  export async function postJson(fetchImpl, url, body, timeoutMs, headers = {}) {
12
12
  const controller = new AbortController();
@@ -14,7 +14,7 @@ export function buildContext(input) {
14
14
  instruction,
15
15
  instructionText,
16
16
  returnSchema,
17
- budget: input.budget,
17
+ maxOutput: input.maxOutput,
18
18
  finalUserMessage: buildFinalUserMessage(context, instructionText, returnSchema),
19
19
  };
20
20
  }
@@ -166,7 +166,7 @@ export function builtContextToJson(context) {
166
166
  })),
167
167
  instruction: context.instruction,
168
168
  returnSchema: context.returnSchema ?? null,
169
- budget: budgetToJson(context.budget),
169
+ maxOutput: budgetToJson(context.maxOutput),
170
170
  finalUserMessage: context.finalUserMessage,
171
171
  };
172
172
  }
@@ -97,8 +97,16 @@ export class Evaluator {
97
97
  return this.valuesEqual(await this.evaluate(expr.left, scope), await this.evaluate(expr.right, scope));
98
98
  case "!=":
99
99
  return !this.valuesEqual(await this.evaluate(expr.left, scope), await this.evaluate(expr.right, scope));
100
+ case "<":
101
+ return this.evaluateLessThan(await this.evaluate(expr.left, scope), await this.evaluate(expr.right, scope), expr.range);
100
102
  }
101
103
  }
104
+ evaluateLessThan(left, right, range) {
105
+ if (typeof left !== "number" || typeof right !== "number") {
106
+ throw new RuntimeError("operator '<' requires number operands", range);
107
+ }
108
+ return left < right;
109
+ }
102
110
  valuesEqual(left, right) {
103
111
  return runtimeValuesEqual(left, right);
104
112
  }
@@ -29,7 +29,7 @@ export class GenerateRuntime {
29
29
  instruction,
30
30
  returnShape: expr.returnShape,
31
31
  uses: context,
32
- budget: options.limit,
32
+ maxOutput: options.maxOutput,
33
33
  });
34
34
  if (options.debug) {
35
35
  writeDebugPrompt(agent.name, attempt, builtContext);
@@ -44,7 +44,11 @@ export class GenerateRuntime {
44
44
  returnShape: expr.returnShape,
45
45
  context,
46
46
  builtContext,
47
- budget: options.limit,
47
+ maxOutput: options.maxOutput,
48
+ temperature: options.temperature,
49
+ think: options.think,
50
+ strict: options.strict,
51
+ debug: options.debug,
48
52
  });
49
53
  }
50
54
  catch (error) {
@@ -58,18 +62,27 @@ export class GenerateRuntime {
58
62
  continue;
59
63
  }
60
64
  try {
61
- const result = expr.returnShape ? coerceValueToShape(rawResult, expr.returnShape) : rawResult;
65
+ const result = expr.returnShape && !options.strict ? coerceValueToShape(rawResult, expr.returnShape) : rawResult;
62
66
  if (expr.returnShape) {
63
- validateValueAgainstShape(result, expr.returnShape, expr.range);
67
+ validateValueAgainstShape(result, expr.returnShape, expr.range, { rejectExtraFields: options.strict });
64
68
  }
65
69
  this.trace.push({
66
70
  kind: "generate",
67
71
  data: {
68
72
  instruction: sanitizeForJson(options.input),
73
+ config: {
74
+ maxOutput: budgetToJson(options.maxOutput),
75
+ attempts: options.attempts,
76
+ temperature: options.temperature ?? null,
77
+ think: options.think ?? false,
78
+ strict: options.strict,
79
+ debug: options.debug,
80
+ },
69
81
  attempts: attempt,
70
- budget: budgetToJson(options.limit),
82
+ maxOutput: budgetToJson(options.maxOutput),
71
83
  debug: options.debug,
72
84
  context: builtContextToJson(builtContext),
85
+ validation: expr.returnShape ? { ok: true, strict: options.strict } : null,
73
86
  result: sanitizeForJson(result),
74
87
  },
75
88
  });
@@ -99,7 +112,10 @@ export class GenerateRuntime {
99
112
  return {
100
113
  input: await this.host.evaluate(expr.options.input, scope),
101
114
  attempts,
102
- limit: expr.options.limit,
115
+ maxOutput: expr.options.maxOutput,
116
+ temperature: expr.options.temperature?.value,
117
+ think: expr.options.think?.value,
118
+ strict: expr.options.strict?.value ?? false,
103
119
  debug: expr.options.debug?.value ?? false,
104
120
  };
105
121
  }
@@ -7,14 +7,16 @@ export function buildValueFromShape(shape) {
7
7
  }
8
8
  return result;
9
9
  }
10
- export function validateValueAgainstShape(value, shape, range) {
10
+ export function validateValueAgainstShape(value, shape, range, options = {}) {
11
11
  if (!isObject(value)) {
12
12
  throw new RuntimeError("LLM result must be an object matching the generate return shape", range);
13
13
  }
14
14
  const allowedFields = new Set(shape.fields.map((field) => field.name));
15
- for (const key of Object.keys(value)) {
16
- if (!allowedFields.has(key)) {
17
- throw new RuntimeError(`LLM result contains unexpected field '${key}'`, range);
15
+ if (options.rejectExtraFields) {
16
+ for (const key of Object.keys(value)) {
17
+ if (!allowedFields.has(key)) {
18
+ throw new RuntimeError(`LLM result contains unexpected field '${key}'`, range);
19
+ }
18
20
  }
19
21
  }
20
22
  for (const field of shape.fields) {
@@ -395,9 +395,26 @@ class Analyzer {
395
395
  this.error("INVALID_GENERATE_ATTEMPTS", "generate attempts must be a positive integer", property.value.range);
396
396
  }
397
397
  }
398
- else if (property.key === "limit") {
399
- if (!expr.options.limit || expr.options.limit.amount <= 0) {
400
- this.error("INVALID_GENERATE_LIMIT", "generate limit must be a positive budget", property.value.range);
398
+ else if (property.key === "max_output") {
399
+ if (!expr.options.maxOutput || expr.options.maxOutput.amount <= 0) {
400
+ this.error("INVALID_GENERATE_MAX_OUTPUT", "generate max_output must be a positive budget", property.value.range);
401
+ }
402
+ }
403
+ else if (property.key === "temperature") {
404
+ if (property.value.kind !== "NumberExpr") {
405
+ this.error("INVALID_GENERATE_TEMPERATURE", "generate temperature must be a number", property.value.range);
406
+ }
407
+ }
408
+ else if (property.key === "think") {
409
+ const validThinkString = property.value.kind === "StringExpr" &&
410
+ ["auto", "low", "medium", "high"].includes(property.value.value);
411
+ if (property.value.kind !== "BooleanExpr" && !validThinkString) {
412
+ this.error("INVALID_GENERATE_THINK", "generate think must be a boolean or one of auto, low, medium, high", property.value.range);
413
+ }
414
+ }
415
+ else if (property.key === "strict") {
416
+ if (property.value.kind !== "BooleanExpr") {
417
+ this.error("INVALID_GENERATE_STRICT", "generate strict must be a boolean", property.value.range);
401
418
  }
402
419
  }
403
420
  else if (property.key === "debug") {