@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.
Files changed (77) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/INSTALL.md +92 -0
  3. package/LICENSE +21 -0
  4. package/README.md +246 -0
  5. package/dist/ast/constants.js +1 -0
  6. package/dist/ast/format.js +41 -0
  7. package/dist/ast/types.js +1 -0
  8. package/dist/bin/agentscript.js +234 -0
  9. package/dist/bin/input.js +19 -0
  10. package/dist/bin/repl.js +290 -0
  11. package/dist/index.js +26 -0
  12. package/dist/parser/errors.js +8 -0
  13. package/dist/parser/parser.js +661 -0
  14. package/dist/parser/tokenizer.js +246 -0
  15. package/dist/providers/llm/anthropic.js +36 -0
  16. package/dist/providers/llm/index.js +3 -0
  17. package/dist/providers/llm/ollama.js +19 -0
  18. package/dist/providers/llm/openai.js +31 -0
  19. package/dist/providers/llm/protocol.js +45 -0
  20. package/dist/providers/llm/shared.js +147 -0
  21. package/dist/providers/llm/types.js +1 -0
  22. package/dist/providers/llm/uri.js +24 -0
  23. package/dist/providers/memory/file.js +44 -0
  24. package/dist/providers/memory/host.js +66 -0
  25. package/dist/providers/memory/index.js +1 -0
  26. package/dist/providers/memory/shared.js +56 -0
  27. package/dist/providers/memory/sqlite.js +98 -0
  28. package/dist/providers/mock/index.js +32 -0
  29. package/dist/providers/tools/env.js +11 -0
  30. package/dist/providers/tools/file.js +99 -0
  31. package/dist/providers/tools/host.js +34 -0
  32. package/dist/providers/tools/http.js +40 -0
  33. package/dist/providers/tools/index.js +2 -0
  34. package/dist/providers/tools/scheme.js +16 -0
  35. package/dist/providers/tools/shared.js +92 -0
  36. package/dist/providers/tools/shell.js +80 -0
  37. package/dist/runtime/context.js +160 -0
  38. package/dist/runtime/errors.js +14 -0
  39. package/dist/runtime/evaluator.js +276 -0
  40. package/dist/runtime/generate.js +175 -0
  41. package/dist/runtime/guards.js +39 -0
  42. package/dist/runtime/input.js +38 -0
  43. package/dist/runtime/interpreter.js +314 -0
  44. package/dist/runtime/json.js +59 -0
  45. package/dist/runtime/loader.js +146 -0
  46. package/dist/runtime/scope.js +47 -0
  47. package/dist/runtime/shape.js +132 -0
  48. package/dist/runtime/trace.js +54 -0
  49. package/dist/runtime/truth.js +13 -0
  50. package/dist/runtime/types.js +1 -0
  51. package/dist/runtime/uri.js +10 -0
  52. package/dist/semantic/analyzer.js +519 -0
  53. package/dist/semantic/diagnostics.js +16 -0
  54. package/dist/utils/assert.js +3 -0
  55. package/docs/cn/context-engineering.md +389 -0
  56. package/docs/cn/language.md +478 -0
  57. package/docs/design-history/v0-design.md +365 -0
  58. package/docs/design-history/v0-implement.md +274 -0
  59. package/docs/design-history/v1-design.md +323 -0
  60. package/docs/design-history/v1-implement.md +267 -0
  61. package/docs/design-history/v2-design.md +387 -0
  62. package/docs/design-history/v2-implement.md +399 -0
  63. package/docs/en/context-engineering.md +332 -0
  64. package/docs/en/language.md +478 -0
  65. package/examples/changelog.as +29 -0
  66. package/examples/extract.as +29 -0
  67. package/examples/review.as +38 -0
  68. package/examples/summarize.as +28 -0
  69. package/examples/translate.as +33 -0
  70. package/package.json +59 -0
  71. package/tutorials/cli.as +22 -0
  72. package/tutorials/helloworld.as +14 -0
  73. package/tutorials/memory.as +19 -0
  74. package/tutorials/plan-execute.as +155 -0
  75. package/tutorials/react.as +98 -0
  76. package/tutorials/repl.as +31 -0
  77. package/tutorials/self-improve.as +60 -0
@@ -0,0 +1,246 @@
1
+ import { ParseError } from "./errors.js";
2
+ const KEYWORDS = new Set([
3
+ "import",
4
+ "main",
5
+ "agent",
6
+ "func",
7
+ "use",
8
+ "loop",
9
+ "until",
10
+ "repeat",
11
+ "for",
12
+ "in",
13
+ "return",
14
+ "if",
15
+ "else",
16
+ "and",
17
+ "or",
18
+ "not",
19
+ "generate",
20
+ "from",
21
+ "true",
22
+ "false",
23
+ "none",
24
+ "string",
25
+ "number",
26
+ "boolean",
27
+ "json",
28
+ "list"
29
+ ]);
30
+ const SYMBOLS = new Set([
31
+ "{",
32
+ "}",
33
+ "(",
34
+ ")",
35
+ "[",
36
+ "]",
37
+ ".",
38
+ ",",
39
+ ":",
40
+ "=",
41
+ "!",
42
+ "<",
43
+ "*"
44
+ ]);
45
+ export function tokenize(source) {
46
+ const scanner = new Scanner(source);
47
+ return scanner.scanAll();
48
+ }
49
+ class Scanner {
50
+ source;
51
+ offset = 0;
52
+ line = 1;
53
+ column = 1;
54
+ constructor(source) {
55
+ this.source = source;
56
+ }
57
+ scanAll() {
58
+ const tokens = [];
59
+ while (!this.isAtEnd()) {
60
+ this.skipWhitespaceAndComments();
61
+ if (this.isAtEnd()) {
62
+ break;
63
+ }
64
+ const start = this.location();
65
+ const char = this.peek();
66
+ if (char === '"' || char === "'") {
67
+ tokens.push(this.scanString());
68
+ continue;
69
+ }
70
+ if (isDigit(char)) {
71
+ tokens.push(this.scanNumber());
72
+ continue;
73
+ }
74
+ if (isIdentifierStart(char)) {
75
+ tokens.push(this.scanIdentifier());
76
+ continue;
77
+ }
78
+ if (SYMBOLS.has(char)) {
79
+ const twoChar = `${char}${this.peekNext()}`;
80
+ if (twoChar === "==" || twoChar === "!=") {
81
+ this.advance();
82
+ this.advance();
83
+ tokens.push({
84
+ kind: "symbol",
85
+ value: twoChar,
86
+ range: { start, end: this.location() }
87
+ });
88
+ continue;
89
+ }
90
+ this.advance();
91
+ tokens.push({
92
+ kind: "symbol",
93
+ value: char,
94
+ range: { start, end: this.location() }
95
+ });
96
+ continue;
97
+ }
98
+ throw new ParseError(`Unexpected character '${char}'`, start);
99
+ }
100
+ const location = this.location();
101
+ tokens.push({
102
+ kind: "eof",
103
+ value: "",
104
+ range: { start: location, end: location }
105
+ });
106
+ return tokens;
107
+ }
108
+ scanString() {
109
+ const quote = this.peek();
110
+ const start = this.location();
111
+ this.advance();
112
+ let value = "";
113
+ while (!this.isAtEnd() && this.peek() !== quote) {
114
+ const char = this.advance();
115
+ if (char === "\\") {
116
+ value += this.scanEscape(start);
117
+ }
118
+ else {
119
+ value += char;
120
+ }
121
+ }
122
+ if (this.isAtEnd()) {
123
+ throw new ParseError("Unterminated string literal", start);
124
+ }
125
+ this.advance();
126
+ return {
127
+ kind: "string",
128
+ value,
129
+ range: { start, end: this.location() }
130
+ };
131
+ }
132
+ scanEscape(start) {
133
+ if (this.isAtEnd()) {
134
+ throw new ParseError("Unterminated escape sequence", start);
135
+ }
136
+ const escaped = this.advance();
137
+ switch (escaped) {
138
+ case "n":
139
+ return "\n";
140
+ case "r":
141
+ return "\r";
142
+ case "t":
143
+ return "\t";
144
+ case "\\":
145
+ return "\\";
146
+ case '"':
147
+ return '"';
148
+ case "'":
149
+ return "'";
150
+ default:
151
+ return escaped;
152
+ }
153
+ }
154
+ scanNumber() {
155
+ const start = this.location();
156
+ let value = "";
157
+ while (!this.isAtEnd() && isDigit(this.peek())) {
158
+ value += this.advance();
159
+ }
160
+ if (!this.isAtEnd() && this.peek() === ".") {
161
+ value += this.advance();
162
+ while (!this.isAtEnd() && isDigit(this.peek())) {
163
+ value += this.advance();
164
+ }
165
+ }
166
+ if (!this.isAtEnd() && isIdentifierStart(this.peek())) {
167
+ while (!this.isAtEnd() && isIdentifierPart(this.peek())) {
168
+ value += this.advance();
169
+ }
170
+ }
171
+ return {
172
+ kind: "number",
173
+ value,
174
+ range: { start, end: this.location() }
175
+ };
176
+ }
177
+ scanIdentifier() {
178
+ const start = this.location();
179
+ let value = "";
180
+ while (!this.isAtEnd() && isIdentifierPart(this.peek())) {
181
+ value += this.advance();
182
+ }
183
+ return {
184
+ kind: KEYWORDS.has(value) ? "keyword" : "identifier",
185
+ value,
186
+ range: { start, end: this.location() }
187
+ };
188
+ }
189
+ skipWhitespaceAndComments() {
190
+ while (!this.isAtEnd()) {
191
+ while (!this.isAtEnd() && isWhitespace(this.peek())) {
192
+ this.advance();
193
+ }
194
+ if (this.peek() === "/" && this.peekNext() === "/") {
195
+ while (!this.isAtEnd() && this.peek() !== "\n") {
196
+ this.advance();
197
+ }
198
+ continue;
199
+ }
200
+ return;
201
+ }
202
+ }
203
+ peek() {
204
+ return this.source[this.offset] ?? "\0";
205
+ }
206
+ peekNext() {
207
+ return this.source[this.offset + 1] ?? "\0";
208
+ }
209
+ advance() {
210
+ const char = this.source[this.offset] ?? "\0";
211
+ this.offset += 1;
212
+ if (char === "\n") {
213
+ this.line += 1;
214
+ this.column = 1;
215
+ }
216
+ else {
217
+ this.column += 1;
218
+ }
219
+ return char;
220
+ }
221
+ isAtEnd() {
222
+ return this.offset >= this.source.length;
223
+ }
224
+ location() {
225
+ return {
226
+ line: this.line,
227
+ column: this.column,
228
+ offset: this.offset
229
+ };
230
+ }
231
+ }
232
+ function isWhitespace(char) {
233
+ return char === " " || char === "\t" || char === "\r" || char === "\n";
234
+ }
235
+ function isDigit(char) {
236
+ const code = char.charCodeAt(0);
237
+ return code >= 48 && code <= 57;
238
+ }
239
+ function isIdentifierStart(char) {
240
+ const code = char.charCodeAt(0);
241
+ return (code >= 65 && code <= 90) || (code >= 97 && code <= 122) || code === 95;
242
+ }
243
+ function isIdentifierPart(char) {
244
+ const code = char.charCodeAt(0);
245
+ return isIdentifierStart(char) || (code >= 48 && code <= 57) || code === 45;
246
+ }
@@ -0,0 +1,36 @@
1
+ import { RuntimeError } from "../../runtime/errors.js";
2
+ import { budgetToTokenLimit, parseJsonText, postJson } from "./shared.js";
3
+ export async function callAnthropic(request, parsed, options, fetchImpl, timeoutMs, baseUrl) {
4
+ const apiKey = options.anthropicApiKey ?? process.env.ANTHROPIC_API_KEY;
5
+ if (!apiKey) {
6
+ throw new RuntimeError("ANTHROPIC_API_KEY is required for anthropic:// models");
7
+ }
8
+ const response = await postJson(fetchImpl, `${baseUrl}/messages`, {
9
+ model: parsed.model,
10
+ max_tokens: budgetToTokenLimit(request) ?? 1024,
11
+ system: request.builtContext.system,
12
+ messages: [
13
+ {
14
+ role: "user",
15
+ content: request.builtContext.finalUserMessage,
16
+ },
17
+ ],
18
+ }, timeoutMs, {
19
+ "x-api-key": apiKey,
20
+ "anthropic-version": "2023-06-01",
21
+ });
22
+ return parseJsonText(readAnthropicText(response));
23
+ }
24
+ function readAnthropicText(value) {
25
+ if (!value || typeof value !== "object" || Array.isArray(value) || !Array.isArray(value.content)) {
26
+ throw new RuntimeError("Anthropic response is missing content");
27
+ }
28
+ return value.content.filter(isAnthropicTextBlock).map((block) => block.text).join("");
29
+ }
30
+ function isAnthropicTextBlock(block) {
31
+ return (block != null &&
32
+ typeof block === "object" &&
33
+ !Array.isArray(block) &&
34
+ block.type === "text" &&
35
+ typeof block.text === "string");
36
+ }
@@ -0,0 +1,3 @@
1
+ export * from "./protocol.js";
2
+ export * from "./types.js";
3
+ export * from "./uri.js";
@@ -0,0 +1,19 @@
1
+ import { budgetToTokenLimit, parseJsonText, postJson, readPath } from "./shared.js";
2
+ export async function callOllama(request, parsed, fetchImpl, timeoutMs, baseUrl) {
3
+ const body = {
4
+ model: parsed.model,
5
+ stream: false,
6
+ think: false,
7
+ format: request.builtContext.returnSchema,
8
+ messages: [
9
+ { role: "system", content: request.builtContext.system },
10
+ { role: "user", content: request.builtContext.finalUserMessage },
11
+ ],
12
+ };
13
+ const maxTokens = budgetToTokenLimit(request);
14
+ if (maxTokens) {
15
+ body.options = { num_predict: maxTokens };
16
+ }
17
+ const response = await postJson(fetchImpl, `${baseUrl}/api/chat`, body, timeoutMs);
18
+ return parseJsonText(readPath(response, ["message", "content"]));
19
+ }
@@ -0,0 +1,31 @@
1
+ import { RuntimeError } from "../../runtime/errors.js";
2
+ import { budgetToTokenLimit, parseJsonText, postJson, readPath } from "./shared.js";
3
+ export async function callOpenAI(request, parsed, options, fetchImpl, timeoutMs, baseUrl) {
4
+ const apiKey = options.openaiApiKey ?? process.env.OPENAI_API_KEY;
5
+ if (!apiKey) {
6
+ throw new RuntimeError("OPENAI_API_KEY is required for openai:// models");
7
+ }
8
+ const body = {
9
+ model: parsed.model,
10
+ messages: [
11
+ { role: "system", content: request.builtContext.system },
12
+ { role: "user", content: request.builtContext.finalUserMessage },
13
+ ],
14
+ response_format: {
15
+ type: "json_schema",
16
+ json_schema: {
17
+ name: "agentscript_generate",
18
+ strict: true,
19
+ schema: request.builtContext.returnSchema,
20
+ },
21
+ },
22
+ };
23
+ const maxTokens = budgetToTokenLimit(request);
24
+ if (maxTokens) {
25
+ body.max_completion_tokens = maxTokens;
26
+ }
27
+ const response = await postJson(fetchImpl, `${baseUrl}/chat/completions`, body, timeoutMs, {
28
+ authorization: `Bearer ${apiKey}`,
29
+ });
30
+ return parseJsonText(readPath(response, ["choices", 0, "message", "content"]));
31
+ }
@@ -0,0 +1,45 @@
1
+ import { RuntimeError } from "../../runtime/errors.js";
2
+ import { callAnthropic } from "./anthropic.js";
3
+ import { callOllama } from "./ollama.js";
4
+ import { callOpenAI } from "./openai.js";
5
+ import { trimTrailingSlash } from "./shared.js";
6
+ import { parseLlmUri } from "./uri.js";
7
+ export class ProtocolLlmProvider {
8
+ options;
9
+ fetchImpl;
10
+ constructor(options = {}) {
11
+ this.options = options;
12
+ this.fetchImpl = options.fetch ?? fetch;
13
+ }
14
+ async generate(request) {
15
+ if (!request.model) {
16
+ throw new RuntimeError("A real LLM provider requires an agent model declaration");
17
+ }
18
+ const parsed = parseLlmUri(request.model);
19
+ const timeoutMs = this.requestTimeoutMs();
20
+ switch (parsed.protocol) {
21
+ case "openai":
22
+ return callOpenAI(request, parsed, this.options, this.fetchImpl, timeoutMs, this.openAIBaseUrl(parsed));
23
+ case "anthropic":
24
+ return callAnthropic(request, parsed, this.options, this.fetchImpl, timeoutMs, this.anthropicBaseUrl(parsed));
25
+ case "ollama":
26
+ return callOllama(request, parsed, this.fetchImpl, timeoutMs, this.ollamaBaseUrl(parsed));
27
+ }
28
+ }
29
+ requestTimeoutMs() {
30
+ const value = this.options.timeoutMs ?? Number.parseInt(process.env.AGENTSCRIPT_LLM_TIMEOUT_MS ?? "", 10);
31
+ return Number.isFinite(value) && value > 0 ? value : 30_000;
32
+ }
33
+ openAIBaseUrl(parsed) {
34
+ return this.providerBaseUrl(parsed, this.options.openaiBaseUrl, "OPENAI_BASE_URL", "https://api.openai.com/v1");
35
+ }
36
+ anthropicBaseUrl(parsed) {
37
+ return this.providerBaseUrl(parsed, this.options.anthropicBaseUrl, "ANTHROPIC_BASE_URL", "https://api.anthropic.com/v1");
38
+ }
39
+ ollamaBaseUrl(parsed) {
40
+ return this.providerBaseUrl(parsed, this.options.ollamaBaseUrl, "OLLAMA_BASE_URL", "http://localhost:11434");
41
+ }
42
+ providerBaseUrl(parsed, configuredUrl, environmentName, fallback) {
43
+ return trimTrailingSlash(parsed.baseUrl ?? configuredUrl ?? process.env[environmentName] ?? fallback);
44
+ }
45
+ }
@@ -0,0 +1,147 @@
1
+ import { RuntimeError } from "../../runtime/errors.js";
2
+ export function budgetToTokenLimit(request) {
3
+ if (!request.budget) {
4
+ return undefined;
5
+ }
6
+ if (request.budget.unit === "k") {
7
+ return Math.max(1, Math.floor(request.budget.amount * 1000));
8
+ }
9
+ return Math.max(1, Math.floor(request.budget.amount));
10
+ }
11
+ export async function postJson(fetchImpl, url, body, timeoutMs, headers = {}) {
12
+ const controller = new AbortController();
13
+ const timeout = setTimeout(() => controller.abort(), timeoutMs);
14
+ let response;
15
+ try {
16
+ response = await fetchImpl(url, {
17
+ method: "POST",
18
+ headers: {
19
+ "content-type": "application/json",
20
+ ...headers,
21
+ },
22
+ body: JSON.stringify(body),
23
+ signal: controller.signal,
24
+ });
25
+ }
26
+ catch (error) {
27
+ const message = error instanceof Error ? error.message : String(error);
28
+ if (controller.signal.aborted) {
29
+ throw new RuntimeError(`LLM provider request timed out after ${timeoutMs}ms: ${url}`);
30
+ }
31
+ throw new RuntimeError(`LLM provider request failed: ${message}`);
32
+ }
33
+ finally {
34
+ clearTimeout(timeout);
35
+ }
36
+ const text = await response.text();
37
+ if (!response.ok) {
38
+ throw new RuntimeError(`LLM provider request failed (${response.status}): ${text}`);
39
+ }
40
+ return parseProviderJson(text);
41
+ }
42
+ export function parseJsonText(text) {
43
+ const candidates = [text, ...extractJsonCandidates(text)];
44
+ try {
45
+ return JSON.parse(candidates[0]);
46
+ }
47
+ catch {
48
+ for (const candidate of candidates.slice(1)) {
49
+ try {
50
+ return JSON.parse(candidate);
51
+ }
52
+ catch {
53
+ continue;
54
+ }
55
+ }
56
+ throw new RuntimeError(`LLM provider did not return JSON: ${snippet(text)}`);
57
+ }
58
+ }
59
+ export function readPath(value, path) {
60
+ let current = value;
61
+ for (const segment of path) {
62
+ if (current === null || current === undefined) {
63
+ break;
64
+ }
65
+ if (Array.isArray(current) && typeof segment === "number") {
66
+ current = current[segment];
67
+ }
68
+ else if (typeof current === "object" && typeof segment === "string") {
69
+ current = current[segment];
70
+ }
71
+ else {
72
+ current = undefined;
73
+ }
74
+ }
75
+ if (typeof current !== "string") {
76
+ throw new RuntimeError(`LLM provider response is missing '${path.join(".")}'`);
77
+ }
78
+ return current;
79
+ }
80
+ export function trimTrailingSlash(value) {
81
+ return value.replace(/\/+$/, "");
82
+ }
83
+ function extractJsonCandidates(text) {
84
+ const candidates = [];
85
+ const fenced = /```(?:json)?\s*([\s\S]*?)```/gi;
86
+ for (const match of text.matchAll(fenced)) {
87
+ if (match[1]) {
88
+ candidates.push(match[1].trim());
89
+ }
90
+ }
91
+ const object = extractFirstJsonObject(text);
92
+ if (object) {
93
+ candidates.push(object);
94
+ }
95
+ return candidates;
96
+ }
97
+ function extractFirstJsonObject(text) {
98
+ let start = -1;
99
+ let depth = 0;
100
+ let inString = false;
101
+ let escaped = false;
102
+ for (let index = 0; index < text.length; index += 1) {
103
+ const char = text[index];
104
+ if (start >= 0 && inString) {
105
+ if (escaped) {
106
+ escaped = false;
107
+ }
108
+ else if (char === "\\") {
109
+ escaped = true;
110
+ }
111
+ else if (char === "\"") {
112
+ inString = false;
113
+ }
114
+ continue;
115
+ }
116
+ if (start >= 0 && char === "\"") {
117
+ inString = true;
118
+ continue;
119
+ }
120
+ if (char === "{") {
121
+ if (start < 0) {
122
+ start = index;
123
+ }
124
+ depth += 1;
125
+ continue;
126
+ }
127
+ if (char === "}" && start >= 0) {
128
+ depth -= 1;
129
+ if (depth === 0) {
130
+ return text.slice(start, index + 1);
131
+ }
132
+ }
133
+ }
134
+ return undefined;
135
+ }
136
+ function parseProviderJson(text) {
137
+ try {
138
+ return JSON.parse(text);
139
+ }
140
+ catch {
141
+ throw new RuntimeError(`LLM provider returned invalid JSON response: ${snippet(text)}`);
142
+ }
143
+ }
144
+ function snippet(text) {
145
+ const normalized = text.replace(/\s+/g, " ").trim();
146
+ return normalized.length > 160 ? `${normalized.slice(0, 157)}...` : normalized;
147
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,24 @@
1
+ import { RuntimeError } from "../../runtime/errors.js";
2
+ const SUPPORTED_LLM_PROTOCOLS = new Set(["openai:", "anthropic:", "ollama:"]);
3
+ export function parseLlmUri(model) {
4
+ if (!model.uri.includes("://")) {
5
+ throw new RuntimeError("LLM URI must use an explicit protocol URL form");
6
+ }
7
+ const url = new URL(model.uri);
8
+ if (!SUPPORTED_LLM_PROTOCOLS.has(url.protocol)) {
9
+ throw new RuntimeError(`Unsupported LLM provider protocol '${url.protocol.replace(":", "")}'`);
10
+ }
11
+ const protocol = url.protocol.slice(0, -1);
12
+ if (protocol === "ollama" && url.pathname.length > 1 && url.hostname) {
13
+ return {
14
+ protocol,
15
+ model: decodeURIComponent(url.pathname.slice(1)),
16
+ baseUrl: `http://${url.host}`,
17
+ };
18
+ }
19
+ const modelName = decodeURIComponent(`${url.hostname}${url.pathname}`.replace(/^\/+/, ""));
20
+ if (!modelName) {
21
+ throw new RuntimeError(`${protocol} URI must include a model name`);
22
+ }
23
+ return { protocol, model: modelName };
24
+ }
@@ -0,0 +1,44 @@
1
+ import { appendFileSync, existsSync, mkdirSync, readFileSync } from "node:fs";
2
+ import { dirname } from "node:path";
3
+ import { RuntimeError } from "../../runtime/errors.js";
4
+ import { createMemoryEnvelope, isMemoryEnvelope, matchesQuery, readLimit } from "./shared.js";
5
+ export class FileMemoryBackend {
6
+ add(request, path) {
7
+ mkdirSync(dirname(path), { recursive: true });
8
+ const envelope = createMemoryEnvelope(request.record);
9
+ appendFileSync(path, `${JSON.stringify(envelope)}\n`, "utf8");
10
+ return envelope;
11
+ }
12
+ query(request, path) {
13
+ const query = request.query;
14
+ const limit = readLimit(query.limit);
15
+ if (!existsSync(path))
16
+ return [];
17
+ const records = readJsonl(path)
18
+ .reverse()
19
+ .filter((item) => matchesQuery(item.record, query));
20
+ return records.slice(0, limit);
21
+ }
22
+ }
23
+ function readJsonl(path) {
24
+ const text = readFileSync(path, "utf8");
25
+ const result = [];
26
+ const lines = text.split(/\r?\n/);
27
+ for (const [index, line] of lines.entries()) {
28
+ if (line.trim().length === 0)
29
+ continue;
30
+ let value;
31
+ try {
32
+ value = JSON.parse(line);
33
+ }
34
+ catch (error) {
35
+ const message = error instanceof Error ? error.message : String(error);
36
+ throw new RuntimeError(`Invalid memory JSONL at ${path}:${index + 1}: ${message}`);
37
+ }
38
+ if (!isMemoryEnvelope(value)) {
39
+ throw new RuntimeError(`Invalid memory JSONL envelope at ${path}:${index + 1}`);
40
+ }
41
+ result.push(value);
42
+ }
43
+ return result;
44
+ }
@@ -0,0 +1,66 @@
1
+ import { isAbsolute, relative, resolve } from "node:path";
2
+ import { RuntimeError } from "../../runtime/errors.js";
3
+ import { isObject } from "../../runtime/guards.js";
4
+ import { uriScheme } from "../../runtime/uri.js";
5
+ import { FileMemoryBackend } from "./file.js";
6
+ import { SqliteMemoryBackend } from "./sqlite.js";
7
+ export class HostMemoryProvider {
8
+ options;
9
+ file = new FileMemoryBackend();
10
+ sqlite = new SqliteMemoryBackend();
11
+ constructor(options = {}) {
12
+ this.options = options;
13
+ }
14
+ async add(request) {
15
+ if (!isObject(request.record)) {
16
+ throw new RuntimeError("memory.add expects an object record");
17
+ }
18
+ return this.backend(request.uri) === "file"
19
+ ? this.file.add(request, this.resolveFileMemoryPath(request.uri))
20
+ : this.sqlite.add(request, this.resolveSqliteMemory(request.uri));
21
+ }
22
+ async query(request) {
23
+ if (!isObject(request.query)) {
24
+ throw new RuntimeError("memory.query expects an object query");
25
+ }
26
+ return this.backend(request.uri) === "file"
27
+ ? this.file.query(request, this.resolveFileMemoryPath(request.uri))
28
+ : this.sqlite.query(request, this.resolveSqliteMemory(request.uri));
29
+ }
30
+ backend(uri) {
31
+ if (uri.startsWith("file://"))
32
+ return "file";
33
+ if (uri.startsWith("sqlite://"))
34
+ return "sqlite";
35
+ throw new RuntimeError(`Unsupported memory URI scheme '${uriScheme(uri)}'`);
36
+ }
37
+ resolveFileMemoryPath(uri) {
38
+ const rawPath = decodeURIComponent(uri.slice("file://".length));
39
+ const path = isAbsolute(rawPath) ? rawPath : resolve(this.options.baseDir ?? process.cwd(), rawPath);
40
+ this.assertWithinWorkspace(path);
41
+ return path;
42
+ }
43
+ resolveSqliteMemory(uri) {
44
+ const raw = uri.slice("sqlite://".length);
45
+ const hashIndex = raw.indexOf("#");
46
+ const rawPath = hashIndex >= 0 ? raw.slice(0, hashIndex) : raw;
47
+ const rawNamespace = hashIndex >= 0 ? raw.slice(hashIndex + 1) : "";
48
+ const decoded = decodeURIComponent(rawPath);
49
+ const path = isAbsolute(decoded) ? decoded : resolve(this.options.baseDir ?? process.cwd(), decoded);
50
+ this.assertWithinWorkspace(path);
51
+ return {
52
+ path,
53
+ namespace: rawNamespace.length > 0 ? decodeURIComponent(rawNamespace) : "memory",
54
+ };
55
+ }
56
+ assertWithinWorkspace(path) {
57
+ const root = resolve(this.options.workspaceRoot ?? process.cwd());
58
+ const rel = relative(root, resolve(path));
59
+ if (rel.startsWith("..") || isAbsolute(rel)) {
60
+ throw new RuntimeError(`Memory path '${path}' is outside workspace root '${root}'`);
61
+ }
62
+ }
63
+ }
64
+ export function createDefaultMemoryProvider(options = {}) {
65
+ return new HostMemoryProvider(options);
66
+ }
@@ -0,0 +1 @@
1
+ export * from "./host.js";