@rong/agentscript 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +22 -0
- package/INSTALL.md +92 -0
- package/LICENSE +21 -0
- package/README.md +246 -0
- package/dist/ast/constants.js +1 -0
- package/dist/ast/format.js +41 -0
- package/dist/ast/types.js +1 -0
- package/dist/bin/agentscript.js +234 -0
- package/dist/bin/input.js +19 -0
- package/dist/bin/repl.js +290 -0
- package/dist/index.js +26 -0
- package/dist/parser/errors.js +8 -0
- package/dist/parser/parser.js +661 -0
- package/dist/parser/tokenizer.js +246 -0
- package/dist/providers/llm/anthropic.js +36 -0
- package/dist/providers/llm/index.js +3 -0
- package/dist/providers/llm/ollama.js +19 -0
- package/dist/providers/llm/openai.js +31 -0
- package/dist/providers/llm/protocol.js +45 -0
- package/dist/providers/llm/shared.js +147 -0
- package/dist/providers/llm/types.js +1 -0
- package/dist/providers/llm/uri.js +24 -0
- package/dist/providers/memory/file.js +44 -0
- package/dist/providers/memory/host.js +66 -0
- package/dist/providers/memory/index.js +1 -0
- package/dist/providers/memory/shared.js +56 -0
- package/dist/providers/memory/sqlite.js +98 -0
- package/dist/providers/mock/index.js +32 -0
- package/dist/providers/tools/env.js +11 -0
- package/dist/providers/tools/file.js +99 -0
- package/dist/providers/tools/host.js +34 -0
- package/dist/providers/tools/http.js +40 -0
- package/dist/providers/tools/index.js +2 -0
- package/dist/providers/tools/scheme.js +16 -0
- package/dist/providers/tools/shared.js +92 -0
- package/dist/providers/tools/shell.js +80 -0
- package/dist/runtime/context.js +160 -0
- package/dist/runtime/errors.js +14 -0
- package/dist/runtime/evaluator.js +276 -0
- package/dist/runtime/generate.js +175 -0
- package/dist/runtime/guards.js +39 -0
- package/dist/runtime/input.js +38 -0
- package/dist/runtime/interpreter.js +314 -0
- package/dist/runtime/json.js +59 -0
- package/dist/runtime/loader.js +146 -0
- package/dist/runtime/scope.js +47 -0
- package/dist/runtime/shape.js +132 -0
- package/dist/runtime/trace.js +54 -0
- package/dist/runtime/truth.js +13 -0
- package/dist/runtime/types.js +1 -0
- package/dist/runtime/uri.js +10 -0
- package/dist/semantic/analyzer.js +519 -0
- package/dist/semantic/diagnostics.js +16 -0
- package/dist/utils/assert.js +3 -0
- package/docs/cn/context-engineering.md +389 -0
- package/docs/cn/language.md +478 -0
- package/docs/design-history/v0-design.md +365 -0
- package/docs/design-history/v0-implement.md +274 -0
- package/docs/design-history/v1-design.md +323 -0
- package/docs/design-history/v1-implement.md +267 -0
- package/docs/design-history/v2-design.md +387 -0
- package/docs/design-history/v2-implement.md +399 -0
- package/docs/en/context-engineering.md +332 -0
- package/docs/en/language.md +478 -0
- package/examples/changelog.as +29 -0
- package/examples/extract.as +29 -0
- package/examples/review.as +38 -0
- package/examples/summarize.as +28 -0
- package/examples/translate.as +33 -0
- package/package.json +59 -0
- package/tutorials/cli.as +22 -0
- package/tutorials/helloworld.as +14 -0
- package/tutorials/memory.as +19 -0
- package/tutorials/plan-execute.as +155 -0
- package/tutorials/react.as +98 -0
- package/tutorials/repl.as +31 -0
- package/tutorials/self-improve.as +60 -0
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { RuntimeError } from "./errors.js";
|
|
2
|
+
import { isObject } from "./guards.js";
|
|
3
|
+
import { sanitizeForJson } from "./json.js";
|
|
4
|
+
import { validateValueAgainstShapeType } from "./shape.js";
|
|
5
|
+
export async function prepareEntryInput(input, entry, inputProvider, trace) {
|
|
6
|
+
const inputParam = entry.params[0];
|
|
7
|
+
if (!inputParam?.shape) {
|
|
8
|
+
return input;
|
|
9
|
+
}
|
|
10
|
+
if (!isObject(input)) {
|
|
11
|
+
throw new RuntimeError("Entry input must be a JSON object", inputParam.range);
|
|
12
|
+
}
|
|
13
|
+
await fillShape(input, inputParam.shape, ["input"], inputProvider, trace);
|
|
14
|
+
return input;
|
|
15
|
+
}
|
|
16
|
+
async function fillShape(target, shape, path, inputProvider, trace) {
|
|
17
|
+
for (const field of shape.fields) {
|
|
18
|
+
const fieldPath = [...path, field.name];
|
|
19
|
+
const value = target[field.name];
|
|
20
|
+
if (value === undefined || value === null) {
|
|
21
|
+
if (!inputProvider) {
|
|
22
|
+
throw new RuntimeError(`Missing input '${fieldPath.join(".")}' and no interactive input provider is available`, field.range);
|
|
23
|
+
}
|
|
24
|
+
const filled = await inputProvider.read({ name: field.name, path: fieldPath });
|
|
25
|
+
validateInputField(filled, field);
|
|
26
|
+
target[field.name] = filled;
|
|
27
|
+
trace.push({
|
|
28
|
+
kind: "input",
|
|
29
|
+
data: { path: fieldPath.join("."), value: sanitizeForJson(filled) },
|
|
30
|
+
});
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
validateInputField(value, field);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
function validateInputField(value, field) {
|
|
37
|
+
validateValueAgainstShapeType(value, field.type, field.range, "Input field");
|
|
38
|
+
}
|
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { dirname, extname, isAbsolute, resolve } from "node:path";
|
|
3
|
+
import { formatExpressionSource } from "../ast/format.js";
|
|
4
|
+
import { assertSemanticallyValid } from "../semantic/analyzer.js";
|
|
5
|
+
import { Evaluator } from "./evaluator.js";
|
|
6
|
+
import { RuntimeError } from "./errors.js";
|
|
7
|
+
import { GenerateRuntime } from "./generate.js";
|
|
8
|
+
import { isObject } from "./guards.js";
|
|
9
|
+
import { budgetToJson, sanitizeForJson } from "./json.js";
|
|
10
|
+
import { prepareEntryInput } from "./input.js";
|
|
11
|
+
import { RuntimeScope } from "./scope.js";
|
|
12
|
+
import { assertNever } from "../utils/assert.js";
|
|
13
|
+
import { isTruthy } from "./truth.js";
|
|
14
|
+
import { createDefaultMemoryProvider } from "../providers/memory/index.js";
|
|
15
|
+
import { MockLlmProvider } from "../providers/mock/index.js";
|
|
16
|
+
import { createDefaultToolProvider } from "../providers/tools/index.js";
|
|
17
|
+
const MAX_CALL_DEPTH = 1000;
|
|
18
|
+
export async function executeAgent(program, input, options = {}) {
|
|
19
|
+
assertSemanticallyValid(program);
|
|
20
|
+
const interpreter = new Interpreter(program, options);
|
|
21
|
+
const value = await interpreter.execute(input);
|
|
22
|
+
return { value, trace: interpreter.trace };
|
|
23
|
+
}
|
|
24
|
+
class Interpreter {
|
|
25
|
+
program;
|
|
26
|
+
options;
|
|
27
|
+
trace = [];
|
|
28
|
+
llmProvider;
|
|
29
|
+
inputProvider;
|
|
30
|
+
toolProvider;
|
|
31
|
+
memoryProvider;
|
|
32
|
+
evaluator;
|
|
33
|
+
entryFunction;
|
|
34
|
+
agent;
|
|
35
|
+
currentAgent;
|
|
36
|
+
agents = new Map();
|
|
37
|
+
files = new Map();
|
|
38
|
+
tools = new Map();
|
|
39
|
+
llms = new Map();
|
|
40
|
+
memories = new Map();
|
|
41
|
+
callDepth = 0;
|
|
42
|
+
constructor(program, options) {
|
|
43
|
+
this.program = program;
|
|
44
|
+
this.options = options;
|
|
45
|
+
this.llmProvider = options.llmProvider ?? new MockLlmProvider();
|
|
46
|
+
this.inputProvider = options.inputProvider;
|
|
47
|
+
this.toolProvider = options.toolProvider ?? createDefaultToolProvider(options.workspaceRoot);
|
|
48
|
+
this.memoryProvider = options.memoryProvider ?? createDefaultMemoryProvider({
|
|
49
|
+
baseDir: this.programSourceDir(),
|
|
50
|
+
workspaceRoot: options.workspaceRoot ?? this.programSourceDir(),
|
|
51
|
+
});
|
|
52
|
+
const generateRuntime = new GenerateRuntime(this.llmProvider, this.trace, {
|
|
53
|
+
currentAgent: () => this.currentAgent,
|
|
54
|
+
evaluate: (expr, scope) => this.evaluator.evaluate(expr, scope),
|
|
55
|
+
resolveContextUses: (scope) => this.evaluator.resolveContextUses(scope),
|
|
56
|
+
});
|
|
57
|
+
this.evaluator = new Evaluator(this.toolProvider, this.memoryProvider, this.trace, generateRuntime, {
|
|
58
|
+
callAgent: (agentName, functionName, args, range) => this.callAgent(agentName, functionName, args, range),
|
|
59
|
+
callFunction: (agent, name, args, range) => this.callFunction(agent, name, args, range),
|
|
60
|
+
requireAgent: (name, range) => this.requireAgent(name, range),
|
|
61
|
+
resolveMainFunction: (agent) => this.resolveMainFunction(agent),
|
|
62
|
+
});
|
|
63
|
+
for (const agent of program.agents) {
|
|
64
|
+
this.agents.set(agent.name, agent);
|
|
65
|
+
}
|
|
66
|
+
this.agent = this.resolveAgent(options.agentName);
|
|
67
|
+
this.currentAgent = this.agent;
|
|
68
|
+
this.entryFunction = options.functionName ?? this.resolveMainFunction(this.agent).name;
|
|
69
|
+
this.registerImports(program);
|
|
70
|
+
}
|
|
71
|
+
registerImports(program) {
|
|
72
|
+
for (const imported of program.imports) {
|
|
73
|
+
switch (imported.resourceKind) {
|
|
74
|
+
case "tool":
|
|
75
|
+
this.tools.set(imported.name, { __agentScriptResource: "tool", name: imported.name, uri: imported.uri });
|
|
76
|
+
break;
|
|
77
|
+
case "llm":
|
|
78
|
+
this.llms.set(imported.name, { __agentScriptResource: "llm", name: imported.name, uri: imported.uri });
|
|
79
|
+
break;
|
|
80
|
+
case "file":
|
|
81
|
+
this.files.set(imported.name, this.loadImportedFile(imported.uri));
|
|
82
|
+
break;
|
|
83
|
+
case "memory":
|
|
84
|
+
this.memories.set(imported.name, { __agentScriptResource: "memory", name: imported.name, uri: imported.uri });
|
|
85
|
+
break;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
loadImportedFile(uri) {
|
|
90
|
+
const path = this.resolveImportPath(uri);
|
|
91
|
+
const content = readFileSync(path, "utf8");
|
|
92
|
+
if (extname(path).toLowerCase() === ".json") {
|
|
93
|
+
return JSON.parse(content);
|
|
94
|
+
}
|
|
95
|
+
return content;
|
|
96
|
+
}
|
|
97
|
+
resolveImportPath(uri) {
|
|
98
|
+
if (uri.startsWith("file://")) {
|
|
99
|
+
return new URL(uri).pathname;
|
|
100
|
+
}
|
|
101
|
+
if (isAbsolute(uri)) {
|
|
102
|
+
return uri;
|
|
103
|
+
}
|
|
104
|
+
return resolve(this.programSourceDir(), uri);
|
|
105
|
+
}
|
|
106
|
+
programSourceDir() {
|
|
107
|
+
return this.options.sourcePath ? dirname(resolve(this.options.sourcePath)) : process.cwd();
|
|
108
|
+
}
|
|
109
|
+
async execute(input) {
|
|
110
|
+
const entry = this.findFunction(this.agent, this.entryFunction);
|
|
111
|
+
if (!entry) {
|
|
112
|
+
throw new RuntimeError(`Unknown function '${this.entryFunction}'`);
|
|
113
|
+
}
|
|
114
|
+
const args = entry.params.length === 0 ? [] : [await prepareEntryInput(input, entry, this.inputProvider, this.trace)];
|
|
115
|
+
return this.callFunction(this.agent, entry.name, args, entry.range);
|
|
116
|
+
}
|
|
117
|
+
resolveAgent(agentName) {
|
|
118
|
+
if (agentName) {
|
|
119
|
+
const agent = this.program.agents.find((item) => item.name === agentName);
|
|
120
|
+
if (!agent)
|
|
121
|
+
throw new RuntimeError(`Unknown agent '${agentName}'`);
|
|
122
|
+
return agent;
|
|
123
|
+
}
|
|
124
|
+
const main = this.program.agents.find((item) => item.isMain);
|
|
125
|
+
if (main)
|
|
126
|
+
return main;
|
|
127
|
+
if (this.program.agents.length !== 1) {
|
|
128
|
+
throw new RuntimeError("main agent is required when a program contains multiple agents");
|
|
129
|
+
}
|
|
130
|
+
return this.program.agents[0];
|
|
131
|
+
}
|
|
132
|
+
resolveMainFunction(agent) {
|
|
133
|
+
const main = agent.functions.find((fn) => fn.isMain);
|
|
134
|
+
if (main)
|
|
135
|
+
return main;
|
|
136
|
+
throw new RuntimeError(`Agent '${agent.name}' has no main func`);
|
|
137
|
+
}
|
|
138
|
+
async callFunction(agent, name, args, range) {
|
|
139
|
+
const fn = this.findFunction(agent, name);
|
|
140
|
+
if (!fn) {
|
|
141
|
+
throw new RuntimeError(`Unknown function '${agent.name}.${name}'`, range);
|
|
142
|
+
}
|
|
143
|
+
if (args.length !== fn.params.length) {
|
|
144
|
+
throw new RuntimeError(`Function '${agent.name}.${name}' expects ${fn.params.length} argument(s), got ${args.length}`, fn.range);
|
|
145
|
+
}
|
|
146
|
+
if (this.callDepth >= MAX_CALL_DEPTH) {
|
|
147
|
+
throw new RuntimeError(`Maximum call depth of ${MAX_CALL_DEPTH} exceeded`, range ?? fn.range);
|
|
148
|
+
}
|
|
149
|
+
const previousAgent = this.currentAgent;
|
|
150
|
+
this.currentAgent = agent;
|
|
151
|
+
this.callDepth += 1;
|
|
152
|
+
const scope = await this.buildFunctionScope(agent, fn, args);
|
|
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;
|
|
159
|
+
}
|
|
160
|
+
finally {
|
|
161
|
+
this.callDepth -= 1;
|
|
162
|
+
this.currentAgent = previousAgent;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
async buildFunctionScope(agent, fn, args) {
|
|
166
|
+
const scope = new RuntimeScope();
|
|
167
|
+
for (const agentName of this.agents.keys()) {
|
|
168
|
+
scope.define(agentName, { __agentScriptResource: "agent", name: agentName }, "agent");
|
|
169
|
+
}
|
|
170
|
+
for (const [toolName, tool] of this.tools) {
|
|
171
|
+
scope.define(toolName, tool, "tool");
|
|
172
|
+
}
|
|
173
|
+
for (const [llmName, llm] of this.llms) {
|
|
174
|
+
scope.define(llmName, llm, "llm");
|
|
175
|
+
}
|
|
176
|
+
for (const [fileName, file] of this.files) {
|
|
177
|
+
scope.define(fileName, file, "file");
|
|
178
|
+
}
|
|
179
|
+
for (const [memoryName, memory] of this.memories) {
|
|
180
|
+
scope.define(memoryName, memory, "memory");
|
|
181
|
+
}
|
|
182
|
+
for (const func of agent.functions) {
|
|
183
|
+
scope.define(func.name, { __agentScriptResource: "function", agentName: agent.name, name: func.name }, "function");
|
|
184
|
+
}
|
|
185
|
+
for (const config of agent.config) {
|
|
186
|
+
const configValue = await this.evaluator.evaluateConfig(config, scope);
|
|
187
|
+
scope.setConfig(config.key, configValue);
|
|
188
|
+
}
|
|
189
|
+
for (const [index, param] of fn.params.entries()) {
|
|
190
|
+
scope.define(param.name, args[index] ?? null, "param");
|
|
191
|
+
}
|
|
192
|
+
return scope;
|
|
193
|
+
}
|
|
194
|
+
findFunction(agent, name) {
|
|
195
|
+
return agent.functions.find((fn) => fn.name === name);
|
|
196
|
+
}
|
|
197
|
+
async executeBlock(statements, scope) {
|
|
198
|
+
for (const stmt of statements) {
|
|
199
|
+
const result = await this.executeStatement(stmt, scope);
|
|
200
|
+
if (result)
|
|
201
|
+
return result;
|
|
202
|
+
}
|
|
203
|
+
return undefined;
|
|
204
|
+
}
|
|
205
|
+
async executeStatement(stmt, scope) {
|
|
206
|
+
switch (stmt.kind) {
|
|
207
|
+
case "ConfigStmt":
|
|
208
|
+
scope.setConfig(stmt.key, await this.evaluator.evaluateConfig(stmt, scope));
|
|
209
|
+
return undefined;
|
|
210
|
+
case "UseStmt": {
|
|
211
|
+
const source = formatExpressionSource(stmt.value);
|
|
212
|
+
scope.addUse(stmt.value, source, stmt.budget);
|
|
213
|
+
this.trace.push({ kind: "use", data: { source, budget: budgetToJson(stmt.budget) } });
|
|
214
|
+
return undefined;
|
|
215
|
+
}
|
|
216
|
+
case "AssignStmt": {
|
|
217
|
+
const value = await this.evaluator.evaluate(stmt.value, scope);
|
|
218
|
+
if (stmt.target.kind === "IdentifierExpr") {
|
|
219
|
+
scope.set(stmt.target.name, value);
|
|
220
|
+
return undefined;
|
|
221
|
+
}
|
|
222
|
+
if (stmt.target.kind === "MemberExpr") {
|
|
223
|
+
const object = await this.evaluator.evaluate(stmt.target.object, scope);
|
|
224
|
+
if (Array.isArray(object)) {
|
|
225
|
+
throw new RuntimeError("Cannot assign a property on a list value", stmt.target.range);
|
|
226
|
+
}
|
|
227
|
+
if (!isObject(object)) {
|
|
228
|
+
throw new RuntimeError("Cannot assign a property on a non-object value", stmt.target.range);
|
|
229
|
+
}
|
|
230
|
+
object[stmt.target.property] = value;
|
|
231
|
+
return undefined;
|
|
232
|
+
}
|
|
233
|
+
throw new RuntimeError("Invalid assignment target", stmt.target.range);
|
|
234
|
+
}
|
|
235
|
+
case "ExprStmt":
|
|
236
|
+
await this.evaluator.evaluate(stmt.expr, scope);
|
|
237
|
+
return undefined;
|
|
238
|
+
case "IfStmt": {
|
|
239
|
+
if (isTruthy(await this.evaluator.evaluate(stmt.condition, scope))) {
|
|
240
|
+
return this.executeBlock(stmt.thenBody, scope.child());
|
|
241
|
+
}
|
|
242
|
+
if (stmt.elseBody) {
|
|
243
|
+
return this.executeBlock(stmt.elseBody, scope.child());
|
|
244
|
+
}
|
|
245
|
+
return undefined;
|
|
246
|
+
}
|
|
247
|
+
case "ForInStmt": {
|
|
248
|
+
const iterable = await this.evaluator.evaluate(stmt.iterable, scope);
|
|
249
|
+
if (!Array.isArray(iterable)) {
|
|
250
|
+
throw new RuntimeError("for loop requires a list value", stmt.iterable.range);
|
|
251
|
+
}
|
|
252
|
+
const iterations = Math.min(stmt.maxIterations, iterable.length);
|
|
253
|
+
for (let index = 0; index < iterations; index += 1) {
|
|
254
|
+
const item = iterable[index];
|
|
255
|
+
this.trace.push({
|
|
256
|
+
kind: "for",
|
|
257
|
+
data: { item: stmt.itemName, index, value: sanitizeForJson(item) }
|
|
258
|
+
});
|
|
259
|
+
const child = scope.child();
|
|
260
|
+
child.define(stmt.itemName, item);
|
|
261
|
+
const result = await this.executeBlock(stmt.body, child);
|
|
262
|
+
if (result)
|
|
263
|
+
return result;
|
|
264
|
+
}
|
|
265
|
+
return undefined;
|
|
266
|
+
}
|
|
267
|
+
case "LoopUntilStmt": {
|
|
268
|
+
for (let index = 0; index < stmt.maxIterations; index += 1) {
|
|
269
|
+
if (isTruthy(await this.evaluator.evaluate(stmt.condition, scope))) {
|
|
270
|
+
return undefined;
|
|
271
|
+
}
|
|
272
|
+
const result = await this.executeBlock(stmt.body, scope.child());
|
|
273
|
+
if (result)
|
|
274
|
+
return result;
|
|
275
|
+
}
|
|
276
|
+
return undefined;
|
|
277
|
+
}
|
|
278
|
+
case "RepeatStmt": {
|
|
279
|
+
for (let index = 0; index < stmt.maxAttempts; index += 1) {
|
|
280
|
+
const result = await this.executeBlock(stmt.body, scope.child());
|
|
281
|
+
if (result)
|
|
282
|
+
return result;
|
|
283
|
+
}
|
|
284
|
+
return undefined;
|
|
285
|
+
}
|
|
286
|
+
case "ReturnStmt":
|
|
287
|
+
return { kind: "return", value: await this.evaluator.evaluate(stmt.value, scope) };
|
|
288
|
+
default:
|
|
289
|
+
assertNever(stmt);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
async callAgent(agentName, functionName, args, range) {
|
|
293
|
+
const traceStart = this.trace.length;
|
|
294
|
+
const result = await this.callFunction(this.requireAgent(agentName, range), functionName, args, range);
|
|
295
|
+
const childTrace = this.trace.splice(traceStart);
|
|
296
|
+
this.trace.push({
|
|
297
|
+
kind: "agent",
|
|
298
|
+
data: {
|
|
299
|
+
agent: agentName,
|
|
300
|
+
function: functionName,
|
|
301
|
+
args: sanitizeForJson(args),
|
|
302
|
+
result: sanitizeForJson(result),
|
|
303
|
+
trace: childTrace.map((event) => sanitizeForJson(event)),
|
|
304
|
+
},
|
|
305
|
+
});
|
|
306
|
+
return result;
|
|
307
|
+
}
|
|
308
|
+
requireAgent(name, range) {
|
|
309
|
+
const agent = this.agents.get(name);
|
|
310
|
+
if (!agent)
|
|
311
|
+
throw new RuntimeError(`Unknown agent '${name}'`, range);
|
|
312
|
+
return agent;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { assertNever } from "../utils/assert.js";
|
|
2
|
+
import { isRuntimeResource } from "./guards.js";
|
|
3
|
+
export function sanitizeForJson(value) {
|
|
4
|
+
return sanitizeForJsonValue(value, new WeakSet());
|
|
5
|
+
}
|
|
6
|
+
export function runtimeValuesEqual(left, right) {
|
|
7
|
+
return renderJsonForComparison(left ?? null) === renderJsonForComparison(right ?? null);
|
|
8
|
+
}
|
|
9
|
+
function sanitizeForJsonValue(value, seen) {
|
|
10
|
+
if (value === null || typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
|
|
11
|
+
return value;
|
|
12
|
+
}
|
|
13
|
+
if (isRuntimeResource(value)) {
|
|
14
|
+
return runtimeResourceToJson(value);
|
|
15
|
+
}
|
|
16
|
+
if (seen.has(value)) {
|
|
17
|
+
return "[Circular]";
|
|
18
|
+
}
|
|
19
|
+
seen.add(value);
|
|
20
|
+
if (Array.isArray(value)) {
|
|
21
|
+
const result = value.map((item) => sanitizeForJsonValue(item, seen));
|
|
22
|
+
seen.delete(value);
|
|
23
|
+
return result;
|
|
24
|
+
}
|
|
25
|
+
const result = {};
|
|
26
|
+
for (const [key, item] of Object.entries(value)) {
|
|
27
|
+
result[key] = sanitizeForJsonValue(item, seen);
|
|
28
|
+
}
|
|
29
|
+
seen.delete(value);
|
|
30
|
+
return result;
|
|
31
|
+
}
|
|
32
|
+
function renderJsonForComparison(value) {
|
|
33
|
+
return JSON.stringify(sanitizeForJson(value));
|
|
34
|
+
}
|
|
35
|
+
function runtimeResourceToJson(value) {
|
|
36
|
+
switch (value.__agentScriptResource) {
|
|
37
|
+
case "tool":
|
|
38
|
+
return { tool: value.name, uri: value.uri };
|
|
39
|
+
case "llm":
|
|
40
|
+
return { llm: value.name, uri: value.uri };
|
|
41
|
+
case "function":
|
|
42
|
+
return { function: value.name, agent: value.agentName };
|
|
43
|
+
case "agent":
|
|
44
|
+
return { agent: value.name };
|
|
45
|
+
case "memory":
|
|
46
|
+
return { memory: value.name, uri: value.uri };
|
|
47
|
+
default:
|
|
48
|
+
assertNever(value);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
export function budgetToJson(budget) {
|
|
52
|
+
if (!budget) {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
return {
|
|
56
|
+
amount: budget.amount,
|
|
57
|
+
unit: budget.unit ?? null,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { dirname, isAbsolute, resolve } from "node:path";
|
|
3
|
+
import { parse } from "../parser/parser.js";
|
|
4
|
+
export function loadProgram(path) {
|
|
5
|
+
const entryPath = resolve(path);
|
|
6
|
+
const state = createLoadState();
|
|
7
|
+
const program = readProgram(entryPath, state);
|
|
8
|
+
mergeEntryProgram(program, dirname(entryPath), state);
|
|
9
|
+
return loadedProgram(program, state);
|
|
10
|
+
}
|
|
11
|
+
export function loadProgramSource(source, options = {}) {
|
|
12
|
+
const sourcePath = options.sourcePath ? resolve(options.sourcePath) : undefined;
|
|
13
|
+
const state = createLoadState();
|
|
14
|
+
const program = parse(source);
|
|
15
|
+
const baseDir = sourceBaseDir(program, sourcePath);
|
|
16
|
+
mergeEntryProgram(program, baseDir, state);
|
|
17
|
+
return loadedProgram(program, state);
|
|
18
|
+
}
|
|
19
|
+
function sourceBaseDir(program, sourcePath) {
|
|
20
|
+
if (sourcePath) {
|
|
21
|
+
return dirname(sourcePath);
|
|
22
|
+
}
|
|
23
|
+
ensureNoRelativeImports(program);
|
|
24
|
+
return process.cwd();
|
|
25
|
+
}
|
|
26
|
+
function ensureNoRelativeImports(program) {
|
|
27
|
+
for (const imported of program.imports) {
|
|
28
|
+
if (requiresFileResolution(imported) && isRelativeImport(imported.uri)) {
|
|
29
|
+
throw new Error(`sourcePath is required to resolve relative ${imported.resourceKind} import '${imported.uri}'`);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
function createLoadState() {
|
|
34
|
+
return {
|
|
35
|
+
agentImportKeys: new Set(),
|
|
36
|
+
imports: [],
|
|
37
|
+
importKeys: new Set(),
|
|
38
|
+
loadingFiles: new Set(),
|
|
39
|
+
programs: new Map(),
|
|
40
|
+
agents: [],
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
function loadedProgram(program, state) {
|
|
44
|
+
return {
|
|
45
|
+
...program,
|
|
46
|
+
imports: state.imports,
|
|
47
|
+
agents: state.agents,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
function readProgram(path, state) {
|
|
51
|
+
const cached = state.programs.get(path);
|
|
52
|
+
if (cached) {
|
|
53
|
+
return cached;
|
|
54
|
+
}
|
|
55
|
+
const program = parse(readFileSync(path, "utf8"));
|
|
56
|
+
state.programs.set(path, program);
|
|
57
|
+
return program;
|
|
58
|
+
}
|
|
59
|
+
function mergeEntryProgram(program, baseDir, state) {
|
|
60
|
+
mergeImports(program.imports, baseDir, state);
|
|
61
|
+
state.agents.push(...program.agents);
|
|
62
|
+
}
|
|
63
|
+
function mergeImports(imports, baseDir, state) {
|
|
64
|
+
for (const imported of imports) {
|
|
65
|
+
if (imported.resourceKind === "agent") {
|
|
66
|
+
loadAgentImport(imported, baseDir, state);
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
addImport(normalizeImport(imported, baseDir), state);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
function loadAgentImport(imported, baseDir, state) {
|
|
74
|
+
const path = resolveImportPath(imported.uri, baseDir);
|
|
75
|
+
const importKey = `${path}#${imported.name}`;
|
|
76
|
+
if (state.agentImportKeys.has(importKey)) {
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
if (state.loadingFiles.has(path)) {
|
|
80
|
+
throw new Error(`Circular agent import detected at ${imported.uri}`);
|
|
81
|
+
}
|
|
82
|
+
state.agentImportKeys.add(importKey);
|
|
83
|
+
state.loadingFiles.add(path);
|
|
84
|
+
try {
|
|
85
|
+
const program = readProgram(path, state);
|
|
86
|
+
mergeImports(program.imports, dirname(path), state);
|
|
87
|
+
const agent = program.agents.find((item) => item.name === imported.name);
|
|
88
|
+
if (!agent) {
|
|
89
|
+
throw new Error(`Imported agent '${imported.name}' was not found in ${imported.uri}`);
|
|
90
|
+
}
|
|
91
|
+
state.agents.push({
|
|
92
|
+
...agent,
|
|
93
|
+
isMain: false,
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
catch (error) {
|
|
97
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
98
|
+
throw new Error(`Failed to load agent import '${imported.name}' from ${path}: ${message}`);
|
|
99
|
+
}
|
|
100
|
+
finally {
|
|
101
|
+
state.loadingFiles.delete(path);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
function addImport(imported, state) {
|
|
105
|
+
const key = `${imported.resourceKind}:${imported.name}:${imported.uri}`;
|
|
106
|
+
if (state.importKeys.has(key)) {
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
state.importKeys.add(key);
|
|
110
|
+
state.imports.push(imported);
|
|
111
|
+
}
|
|
112
|
+
function normalizeImport(imported, baseDir) {
|
|
113
|
+
if (imported.resourceKind !== "file" && imported.resourceKind !== "memory") {
|
|
114
|
+
return imported;
|
|
115
|
+
}
|
|
116
|
+
if (imported.resourceKind === "memory" && !imported.uri.startsWith("file://")) {
|
|
117
|
+
return imported;
|
|
118
|
+
}
|
|
119
|
+
const path = resolveImportPath(imported.uri, baseDir);
|
|
120
|
+
return {
|
|
121
|
+
...imported,
|
|
122
|
+
uri: imported.resourceKind === "memory" ? `file://${path}` : path,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
function resolveImportPath(uri, baseDir) {
|
|
126
|
+
if (uri.startsWith("file://")) {
|
|
127
|
+
const path = decodeURIComponent(uri.slice("file://".length));
|
|
128
|
+
return isAbsolute(path) ? path : resolve(baseDir, path);
|
|
129
|
+
}
|
|
130
|
+
if (isAbsolute(uri)) {
|
|
131
|
+
return uri;
|
|
132
|
+
}
|
|
133
|
+
return resolve(baseDir, uri);
|
|
134
|
+
}
|
|
135
|
+
function isRelativeImport(uri) {
|
|
136
|
+
return !uri.startsWith("file://") && !isAbsolute(uri) && (uri.startsWith("./") || uri.startsWith("../"));
|
|
137
|
+
}
|
|
138
|
+
const FILE_RESOLUTION_KINDS = new Set(["agent", "file"]);
|
|
139
|
+
function requiresFileResolution(imported) {
|
|
140
|
+
if (FILE_RESOLUTION_KINDS.has(imported.resourceKind)) {
|
|
141
|
+
return true;
|
|
142
|
+
}
|
|
143
|
+
return (imported.resourceKind === "memory" &&
|
|
144
|
+
imported.uri.startsWith("file://") &&
|
|
145
|
+
isRelativeImport(imported.uri.slice("file://".length)));
|
|
146
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { RuntimeError } from "./errors.js";
|
|
2
|
+
const MUTABLE_BINDING_KINDS = new Set(["local", "param"]);
|
|
3
|
+
export class RuntimeScope {
|
|
4
|
+
parent;
|
|
5
|
+
bindings = new Map();
|
|
6
|
+
config = new Map();
|
|
7
|
+
uses = [];
|
|
8
|
+
constructor(parent) {
|
|
9
|
+
this.parent = parent;
|
|
10
|
+
}
|
|
11
|
+
child() {
|
|
12
|
+
return new RuntimeScope(this);
|
|
13
|
+
}
|
|
14
|
+
define(name, value, kind = "local") {
|
|
15
|
+
this.bindings.set(name, { value, kind });
|
|
16
|
+
}
|
|
17
|
+
get(name, range) {
|
|
18
|
+
const binding = this.resolveBinding(name);
|
|
19
|
+
if (!binding) {
|
|
20
|
+
throw new RuntimeError(`Unknown variable '${name}'`, range);
|
|
21
|
+
}
|
|
22
|
+
return binding.value;
|
|
23
|
+
}
|
|
24
|
+
set(name, value) {
|
|
25
|
+
const binding = this.resolveBinding(name);
|
|
26
|
+
if (binding && MUTABLE_BINDING_KINDS.has(binding.kind)) {
|
|
27
|
+
binding.value = value;
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
this.define(name, value);
|
|
31
|
+
}
|
|
32
|
+
addUse(expr, source, budget) {
|
|
33
|
+
this.uses.push({ expr, source, budget, scope: this });
|
|
34
|
+
}
|
|
35
|
+
setConfig(name, value) {
|
|
36
|
+
this.config.set(name, value);
|
|
37
|
+
}
|
|
38
|
+
getConfig(name) {
|
|
39
|
+
return this.config.get(name) ?? this.parent?.getConfig(name);
|
|
40
|
+
}
|
|
41
|
+
visibleUses() {
|
|
42
|
+
return [...(this.parent?.visibleUses() ?? []), ...this.uses];
|
|
43
|
+
}
|
|
44
|
+
resolveBinding(name) {
|
|
45
|
+
return this.bindings.get(name) ?? this.parent?.resolveBinding(name);
|
|
46
|
+
}
|
|
47
|
+
}
|