agent-sh 0.14.10 → 0.15.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/README.md +36 -13
- package/dist/agent/agent-loop.d.ts +9 -17
- package/dist/agent/agent-loop.js +123 -150
- package/dist/agent/events.d.ts +10 -12
- package/dist/agent/host-types.d.ts +17 -11
- package/dist/agent/index.d.ts +1 -1
- package/dist/agent/index.js +76 -29
- package/dist/agent/live-view.d.ts +3 -3
- package/dist/agent/live-view.js +15 -7
- package/dist/agent/providers/deepseek.js +9 -1
- package/dist/agent/providers/openrouter.js +9 -0
- package/dist/agent/session-store.js +1 -1
- package/dist/agent/subagent.js +1 -1
- package/dist/agent/system-prompt.d.ts +7 -3
- package/dist/agent/system-prompt.js +11 -14
- package/dist/agent/tool-protocol.js +0 -7
- package/dist/cli/args.js +2 -1
- package/dist/cli/install.d.ts +1 -0
- package/dist/cli/install.js +39 -2
- package/dist/cli/subcommands.js +1 -0
- package/dist/core/event-bus.js +0 -2
- package/dist/core/extension-loader.js +3 -1
- package/dist/core/index.d.ts +1 -1
- package/dist/core/index.js +3 -2
- package/dist/extensions/slash-commands/index.js +16 -11
- package/dist/shell/events.d.ts +3 -0
- package/dist/shell/index.js +9 -0
- package/dist/shell/shell-context.d.ts +2 -2
- package/dist/shell/shell-context.js +26 -11
- package/dist/shell/shell.js +3 -0
- package/dist/shell/tui-renderer.js +0 -1
- package/dist/utils/diff-renderer.d.ts +4 -0
- package/dist/utils/diff-renderer.js +15 -27
- package/dist/utils/handler-registry.d.ts +1 -6
- package/dist/utils/handler-registry.js +1 -6
- package/dist/utils/line-editor.js +0 -2
- package/dist/utils/palette.js +4 -4
- package/dist/utils/terminal-buffer.d.ts +2 -0
- package/dist/utils/terminal-buffer.js +4 -0
- package/examples/extensions/ads/SKILL.md +170 -0
- package/examples/extensions/ash-acp-bridge/src/index.ts +11 -7
- package/examples/extensions/ash-scheme/index.ts +377 -687
- package/examples/extensions/ash-scheme/package.json +1 -1
- package/examples/extensions/ashi/EXTENDING.md +118 -0
- package/examples/extensions/ashi/README.md +26 -54
- package/examples/extensions/ashi/docs/ui-surface-protocol.md +163 -0
- package/examples/extensions/ashi/package.json +14 -2
- package/examples/extensions/ashi/src/autocomplete-controller.ts +95 -0
- package/examples/extensions/ashi/src/autocomplete.ts +1 -23
- package/examples/extensions/ashi/src/capture.ts +54 -10
- package/examples/extensions/ashi/src/chat/assistant.ts +67 -0
- package/examples/extensions/ashi/src/chat/lines.ts +39 -0
- package/examples/extensions/ashi/src/chat/thinking.ts +42 -0
- package/examples/extensions/ashi/src/chat/tool-group.ts +84 -0
- package/examples/extensions/ashi/src/chat/user-message.ts +20 -0
- package/examples/extensions/ashi/src/cli.ts +80 -12
- package/examples/extensions/ashi/src/clipboard-image.ts +41 -0
- package/examples/extensions/ashi/src/commands.ts +11 -1
- package/examples/extensions/ashi/src/dialogs.ts +67 -0
- package/examples/extensions/ashi/src/display-config.ts +16 -1
- package/examples/extensions/ashi/src/docks.ts +31 -0
- package/examples/extensions/ashi/src/events.ts +16 -0
- package/examples/extensions/ashi/src/frontend.ts +456 -268
- package/examples/extensions/ashi/src/hooks.ts +27 -40
- package/examples/extensions/ashi/src/input-prompt.ts +64 -0
- package/examples/extensions/ashi/src/renderer.ts +222 -0
- package/examples/extensions/ashi/src/renderers/pi-tui/app.ts +122 -0
- package/examples/extensions/ashi/src/renderers/pi-tui/index.ts +27 -0
- package/examples/extensions/ashi/src/renderers/pi-tui/nodes.ts +190 -0
- package/examples/extensions/ashi/src/renderers/pi-tui/schema-mount.ts +203 -0
- package/examples/extensions/ashi/src/renderers/pi-tui/theme-adapters.ts +48 -0
- package/examples/extensions/ashi/src/renderers/pi-tui/tool-group.ts +21 -0
- package/examples/extensions/ashi/src/schema.ts +46 -205
- package/examples/extensions/ashi/src/session-commands.ts +2 -1
- package/examples/extensions/ashi/src/status-footer.ts +35 -25
- package/examples/extensions/ashi/src/terminal-mode.ts +9 -0
- package/examples/extensions/ashi/src/theme.ts +1 -47
- package/examples/extensions/ashi/src/ui.ts +88 -0
- package/examples/extensions/ashi-ink/README.md +61 -0
- package/examples/extensions/ashi-ink/package.json +30 -0
- package/examples/extensions/ashi-ink/src/index.ts +6 -0
- package/examples/extensions/ashi-ink/src/ink-renderer.tsx +865 -0
- package/examples/extensions/ashi-ink/src/shims.d.ts +5 -0
- package/examples/extensions/ashi-ink/test/render.test.tsx +408 -0
- package/examples/extensions/ashi-ink/tsconfig.json +14 -0
- package/examples/extensions/ashi-scheme-render.ts +10 -10
- package/examples/extensions/ashi-shell-passthrough.ts +95 -0
- package/examples/extensions/ashi-ui-demo.ts +63 -0
- package/examples/extensions/latex-images.ts +70 -19
- package/examples/extensions/overlay-agent.ts +5 -5
- package/examples/extensions/pi-bridge/index.ts +7 -12
- package/examples/extensions/terminal-buffer.ts +4 -2
- package/package.json +3 -9
- package/examples/extensions/ashi/src/components.ts +0 -238
- package/examples/extensions/ollama.ts +0 -108
- package/examples/extensions/opencode-provider.ts +0 -251
- package/examples/extensions/zai-coding-plan.ts +0 -35
|
@@ -5,9 +5,13 @@
|
|
|
5
5
|
import * as fs from "node:fs";
|
|
6
6
|
import * as os from "node:os";
|
|
7
7
|
import * as path from "node:path";
|
|
8
|
+
import { createRequire } from "node:module";
|
|
8
9
|
import type { AgentContext } from "agent-sh/types";
|
|
9
10
|
import { getSettings } from "agent-sh/settings";
|
|
10
|
-
|
|
11
|
+
// LIPS 1.0's ESM build exposes named exports with no default; older builds (and
|
|
12
|
+
// the CJS interop path) put everything under `default`. Accept both.
|
|
13
|
+
import * as lipsNs from "@jcubic/lips";
|
|
14
|
+
const lips: any = (lipsNs as any).default ?? lipsNs;
|
|
11
15
|
|
|
12
16
|
type ToolResult = {
|
|
13
17
|
content: string; exitCode: number | null; isError: boolean;
|
|
@@ -23,15 +27,18 @@ async function withDisplay(
|
|
|
23
27
|
): Promise<ToolResult> {
|
|
24
28
|
const toolCallId = `scheme-${toolName}-${++callCounter}`;
|
|
25
29
|
bus.emit("agent:tool-started", {
|
|
26
|
-
title: toolName, toolCallId, kind, rawInput, displayDetail,
|
|
30
|
+
title: toolName, toolCallId, kind, rawInput, displayDetail, nested: true,
|
|
27
31
|
});
|
|
28
32
|
const result = await run();
|
|
33
|
+
// Stream the result so the TUI's tracked `output` holds it, not just the summary.
|
|
34
|
+
if (result.content) bus.emit("agent:tool-output-chunk", { toolCallId, chunk: result.content });
|
|
29
35
|
bus.emit("agent:tool-completed", {
|
|
30
36
|
toolCallId,
|
|
31
37
|
exitCode: result.exitCode,
|
|
32
38
|
rawOutput: result.content,
|
|
33
39
|
kind,
|
|
34
40
|
resultDisplay: result.display,
|
|
41
|
+
nested: true,
|
|
35
42
|
});
|
|
36
43
|
return result;
|
|
37
44
|
}
|
|
@@ -41,64 +48,16 @@ async function withDisplay(
|
|
|
41
48
|
// the TUI still renders diffs.
|
|
42
49
|
const HIDDEN_IN_SCHEME_ONLY = ["bash", "pwsh", "read_file", "write_file", "edit_file", "ls", "glob", "grep"];
|
|
43
50
|
|
|
44
|
-
const { Pair, nil, LSymbol, LNumber, Macro,
|
|
45
|
-
|
|
46
|
-
// LIPS' `define` discards the promise returned by async host bindings.
|
|
47
|
-
// With `(define x (read-file …)) x` the exec() advances before `env.set`
|
|
48
|
-
// fires, so `x` is reported unbound. Reinstall to return the promise.
|
|
49
|
-
function installFixedDefine(env: any): void {
|
|
50
|
-
const fixed = Macro.defmacro("define", function (this: any, code: any, eval_args: any) {
|
|
51
|
-
const target = this;
|
|
52
|
-
if (code.car instanceof Pair && code.car.car instanceof LSymbol) {
|
|
53
|
-
return new Pair(
|
|
54
|
-
new LSymbol("define"),
|
|
55
|
-
new Pair(
|
|
56
|
-
code.car.car,
|
|
57
|
-
new Pair(
|
|
58
|
-
new Pair(new LSymbol("lambda"), new Pair(code.car.cdr, code.cdr)),
|
|
59
|
-
nil,
|
|
60
|
-
),
|
|
61
|
-
),
|
|
62
|
-
);
|
|
63
|
-
} else if (eval_args.macro_expand) {
|
|
64
|
-
return;
|
|
65
|
-
}
|
|
66
|
-
if (eval_args.dynamic_scope) eval_args.dynamic_scope = target;
|
|
67
|
-
eval_args.env = target;
|
|
68
|
-
let value = code.cdr.car;
|
|
69
|
-
if (value instanceof Pair) {
|
|
70
|
-
value = lipsEvaluate(value, eval_args);
|
|
71
|
-
} else if (value instanceof LSymbol) {
|
|
72
|
-
value = target.get(value);
|
|
73
|
-
}
|
|
74
|
-
if (code.car instanceof LSymbol) {
|
|
75
|
-
const name = code.car;
|
|
76
|
-
if (value && typeof value.then === "function") {
|
|
77
|
-
return value.then((v: any) => { target.set(name, v); });
|
|
78
|
-
}
|
|
79
|
-
target.set(name, value);
|
|
80
|
-
}
|
|
81
|
-
});
|
|
82
|
-
env.set("define", fixed);
|
|
83
|
-
}
|
|
51
|
+
const { Pair, nil, LSymbol, LNumber, Macro, bootstrap, LString } = lips as any;
|
|
84
52
|
|
|
85
|
-
// LIPS
|
|
86
|
-
//
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
const branch = cond !== false ? code.cdr.car : code.cdr.cdr.car;
|
|
94
|
-
return lipsEvaluate(branch, { env: target, dynamic_scope: dynScope, error: opts.error });
|
|
95
|
-
};
|
|
96
|
-
const condVal = lipsEvaluate(code.car, { env: target, dynamic_scope: dynScope, error: opts.error });
|
|
97
|
-
if (condVal && typeof condVal.then === "function") return condVal.then(choose);
|
|
98
|
-
return choose(condVal);
|
|
99
|
-
});
|
|
100
|
-
env.set("if", lenient);
|
|
101
|
-
}
|
|
53
|
+
// LIPS 1.0 boxes string literals as LString; unbox to a JS primitive so the gap
|
|
54
|
+
// shims and host bridge (which assume JS strings) operate on them correctly.
|
|
55
|
+
const toJsStr = (x: any): any => (x instanceof LString ? x.toString() : x);
|
|
56
|
+
|
|
57
|
+
// LIPS 1.0 stores a symbol's name in `__name__`; `.name` is undefined (the 0.x
|
|
58
|
+
// build used `.name`). Read every symbol name through this so both builds work.
|
|
59
|
+
const symName = (x: any): string | undefined =>
|
|
60
|
+
x instanceof LSymbol ? ((x as any).__name__ ?? (x as any).name) : undefined;
|
|
102
61
|
|
|
103
62
|
const LOG_PATH = path.join(os.homedir(), ".agent-sh", "scheme-eval.log");
|
|
104
63
|
const SCHEME_DEFINE_DIR = path.join(os.homedir(), ".agent-sh", "scheme-define");
|
|
@@ -123,7 +82,7 @@ function installSchemeDefine(
|
|
|
123
82
|
throw new Error("scheme-define: expected (scheme-define name (args …) \"doc\" body …)");
|
|
124
83
|
}
|
|
125
84
|
const nameSym = code.car;
|
|
126
|
-
const name = nameSym
|
|
85
|
+
const name = symName(nameSym)!;
|
|
127
86
|
const argsForm = code.cdr instanceof Pair ? code.cdr.car : nil;
|
|
128
87
|
let rest = code.cdr instanceof Pair ? code.cdr.cdr : nil;
|
|
129
88
|
let doc = "";
|
|
@@ -175,7 +134,7 @@ async function loadPersistedDefines(
|
|
|
175
134
|
const fp = path.join(SCHEME_DEFINE_DIR, f);
|
|
176
135
|
try {
|
|
177
136
|
const src = fs.readFileSync(fp, "utf-8");
|
|
178
|
-
await (lips as any).exec(src, env);
|
|
137
|
+
await (lips as any).exec(src, { env });
|
|
179
138
|
} catch (e) {
|
|
180
139
|
logErr("scheme-define load", e, { file: fp });
|
|
181
140
|
}
|
|
@@ -266,7 +225,7 @@ function lookup(result: unknown, key: string): unknown {
|
|
|
266
225
|
let node: any = result;
|
|
267
226
|
while (node && node instanceof Pair) {
|
|
268
227
|
const entry = node.car;
|
|
269
|
-
if (entry && entry.car && entry.car
|
|
228
|
+
if (entry && entry.car && symName(entry.car) === key) return entry.cdr;
|
|
270
229
|
node = node.cdr;
|
|
271
230
|
}
|
|
272
231
|
return undefined;
|
|
@@ -310,8 +269,11 @@ function stripPagination(raw: string): string[] {
|
|
|
310
269
|
|
|
311
270
|
function format(v: unknown): string {
|
|
312
271
|
if (v === undefined || v === null) return "";
|
|
272
|
+
if (v instanceof LString) v = v.toString();
|
|
313
273
|
if (typeof v === "string") return JSON.stringify(v);
|
|
314
|
-
|
|
274
|
+
// Render JS booleans Scheme-style, matching how #t/#f print inside records.
|
|
275
|
+
if (typeof v === "boolean") return v ? "#t" : "#f";
|
|
276
|
+
if (typeof v === "number") return String(v);
|
|
315
277
|
if (v && typeof (v as any).toString === "function") {
|
|
316
278
|
try { return (v as any).toString(); } catch {}
|
|
317
279
|
}
|
|
@@ -319,40 +281,9 @@ function format(v: unknown): string {
|
|
|
319
281
|
}
|
|
320
282
|
|
|
321
283
|
// ── evaluator ─────────────────────────────────────────────────────
|
|
322
|
-
// LIPS
|
|
323
|
-
//
|
|
324
|
-
|
|
325
|
-
newline: "\n", linefeed: "\n", nl: "\n",
|
|
326
|
-
space: " ", tab: "\t", return: "\r",
|
|
327
|
-
null: "\u0000", nul: "\u0000",
|
|
328
|
-
delete: "\u007f", rubout: "\u007f",
|
|
329
|
-
escape: "\u001b", altmode: "\u001b",
|
|
330
|
-
backspace: "\u0008", alarm: "\u0007", page: "\u000c",
|
|
331
|
-
};
|
|
332
|
-
|
|
333
|
-
// The char after `#\` as a 1-char string + its span, or null for an unknown
|
|
334
|
-
// multi-char name (left untranslated so it surfaces as an error).
|
|
335
|
-
function resolveCharLiteral(source: string, start: number): { ch: string; span: number } | null {
|
|
336
|
-
let j = start;
|
|
337
|
-
while (j < source.length && /[A-Za-z0-9]/.test(source[j])) j++;
|
|
338
|
-
const run = source.slice(start, j);
|
|
339
|
-
if (run.length <= 1) {
|
|
340
|
-
const ch = source[start];
|
|
341
|
-
return ch === undefined ? null : { ch, span: 1 };
|
|
342
|
-
}
|
|
343
|
-
const hex = /^[xX]([0-9a-fA-F]+)$/.exec(run);
|
|
344
|
-
if (hex) {
|
|
345
|
-
const cp = parseInt(hex[1], 16);
|
|
346
|
-
if (cp >= 0 && cp <= 0x10ffff) return { ch: String.fromCodePoint(cp), span: run.length };
|
|
347
|
-
}
|
|
348
|
-
const named = NAMED_CHARS[run.toLowerCase()];
|
|
349
|
-
return named === undefined ? null : { ch: named, span: run.length };
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
// LIPS implements string literals via JSON.parse, which rejects backslash
|
|
353
|
-
// escapes outside JSON's tiny set (\" \\ \/ \b \f \n \r \t \uXXXX). Models
|
|
354
|
-
// routinely write \s \w \d etc. in regex strings. Pre-process: promote any
|
|
355
|
-
// invalid \X to \\X so LIPS parses it as a literal backslash + X.
|
|
284
|
+
// LIPS' string lexer accepts only JSON-style escapes (\" \\ \/ \b \f \n \r \t
|
|
285
|
+
// \uXXXX), but models routinely write \s \w \d etc. in regex strings. Promote
|
|
286
|
+
// any other \X to \\X so it parses as a literal backslash + X.
|
|
356
287
|
function preprocessSchemeSource(source: string): string {
|
|
357
288
|
const JSON_ESC = new Set(["\\", "/", '"', "b", "f", "n", "r", "t"]);
|
|
358
289
|
let out = "";
|
|
@@ -369,14 +300,6 @@ function preprocessSchemeSource(source: string): string {
|
|
|
369
300
|
if (!inStr) {
|
|
370
301
|
if (c === ";") { inComment = true; out += c; continue; }
|
|
371
302
|
if (c === '"') { inStr = true; out += c; continue; }
|
|
372
|
-
if (c === "#" && source[i + 1] === "\\") {
|
|
373
|
-
const lit = resolveCharLiteral(source, i + 2);
|
|
374
|
-
if (lit) {
|
|
375
|
-
out += JSON.stringify(lit.ch);
|
|
376
|
-
i += 1 + lit.span; // skip '\' + the name/char; loop's i++ skips '#'
|
|
377
|
-
continue;
|
|
378
|
-
}
|
|
379
|
-
}
|
|
380
303
|
out += c;
|
|
381
304
|
continue;
|
|
382
305
|
}
|
|
@@ -393,16 +316,16 @@ function preprocessSchemeSource(source: string): string {
|
|
|
393
316
|
i++;
|
|
394
317
|
continue;
|
|
395
318
|
}
|
|
396
|
-
// Any other \X — promote so
|
|
319
|
+
// Any other \X — promote to \\X so it parses as a literal backslash
|
|
397
320
|
out += "\\\\" + next;
|
|
398
321
|
i++;
|
|
399
322
|
}
|
|
400
323
|
return out;
|
|
401
324
|
}
|
|
402
325
|
|
|
403
|
-
// If LIPS
|
|
404
|
-
//
|
|
405
|
-
//
|
|
326
|
+
// If LIPS rejects a string literal, localize the invalid escapes so the agent
|
|
327
|
+
// gets actionable line/col info instead of a raw offset. Only triggers when
|
|
328
|
+
// preprocessing didn't catch everything.
|
|
406
329
|
function formatStringEscapeDiagnostic(source: string, baseMsg: string): string {
|
|
407
330
|
const JSON_ESC = new Set(["\\", "/", '"', "b", "f", "n", "r", "t"]);
|
|
408
331
|
let line = 1, col = 1;
|
|
@@ -512,30 +435,29 @@ function formatParenDiagnostic(source: string, baseMsg: string): string {
|
|
|
512
435
|
|
|
513
436
|
async function evaluate(env: any, source: string, timeoutMs: number) {
|
|
514
437
|
const preprocessed = preprocessSchemeSource(source);
|
|
515
|
-
//
|
|
516
|
-
//
|
|
517
|
-
//
|
|
518
|
-
|
|
519
|
-
const
|
|
520
|
-
const
|
|
438
|
+
// Capture output into the result instead of letting it vanish to console.log.
|
|
439
|
+
// LIPS 1.0's native display/write resolve the *functions* from the env (the
|
|
440
|
+
// stdout-port override alone misses them), so shadow each output procedure.
|
|
441
|
+
const OUTPUT_PROCS = ["stdout", "display", "write", "write-string", "write-char"];
|
|
442
|
+
const prev: Record<string, any> = {};
|
|
443
|
+
for (const name of OUTPUT_PROCS) prev[name] = (env as any).get(name, { throwError: false });
|
|
521
444
|
const buf: string[] = [];
|
|
522
|
-
(
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
return String(a);
|
|
533
|
-
}).join("");
|
|
534
|
-
buf.push(out);
|
|
445
|
+
const raw = (a: any): string => {
|
|
446
|
+
if (a === null || a === undefined) return "";
|
|
447
|
+
if (typeof a === "string") return a;
|
|
448
|
+
if (a && typeof (a as any).toString === "function") return (a as any).toString();
|
|
449
|
+
return String(a);
|
|
450
|
+
};
|
|
451
|
+
(env as any).set("stdout", { write: (...args: any[]) => { for (const a of args) buf.push(raw(a)); } });
|
|
452
|
+
(env as any).set("display", (...args: any[]) => { buf.push(args.map(raw).join("")); });
|
|
453
|
+
(env as any).set("write", (...args: any[]) => {
|
|
454
|
+
buf.push(args.map((a) => (typeof toJsStr(a) === "string" ? JSON.stringify(toJsStr(a)) : raw(a))).join(""));
|
|
535
455
|
});
|
|
456
|
+
(env as any).set("write-string", (...args: any[]) => { buf.push(raw(args[0])); });
|
|
457
|
+
(env as any).set("write-char", (...args: any[]) => { buf.push(raw(args[0])); });
|
|
536
458
|
try {
|
|
537
459
|
const results = await Promise.race<any>([
|
|
538
|
-
(lips as any).exec(preprocessed, env),
|
|
460
|
+
(lips as any).exec(preprocessed, { env }),
|
|
539
461
|
new Promise((_, reject) =>
|
|
540
462
|
setTimeout(() => reject(new Error(`scheme_eval timed out after ${timeoutMs}ms`)), timeoutMs),
|
|
541
463
|
),
|
|
@@ -554,27 +476,25 @@ async function evaluate(env: any, source: string, timeoutMs: number) {
|
|
|
554
476
|
let msg = e?.message ?? String(e);
|
|
555
477
|
if (/[Uu]nbalanced parenthes/.test(msg)) {
|
|
556
478
|
msg = formatParenDiagnostic(source, msg);
|
|
557
|
-
} else if (/Bad escaped character
|
|
479
|
+
} else if (/Invalid string literal|Bad escaped character|Unexpected.*JSON|JSON at position/.test(msg)) {
|
|
558
480
|
msg = formatStringEscapeDiagnostic(source, msg);
|
|
559
481
|
} else if (msg.includes("Unbound variable `#\\")) {
|
|
560
|
-
msg += "\n
|
|
561
|
-
"
|
|
562
|
-
" are accepted (read as strings); other `#\\<name>` forms are not." +
|
|
563
|
-
" Use a string literal instead, e.g. \"\\n\".";
|
|
482
|
+
msg += "\n Unknown character literal. Supported: #\\newline #\\space #\\tab" +
|
|
483
|
+
" #\\return #\\null #\\delete #\\escape, #\\xNN, and #\\<char>.";
|
|
564
484
|
}
|
|
565
485
|
return { ok: false as const, error: msg };
|
|
566
486
|
} finally {
|
|
567
|
-
|
|
568
|
-
|
|
487
|
+
for (const name of OUTPUT_PROCS) {
|
|
488
|
+
if (prev[name] !== undefined) (env as any).set(name, prev[name]);
|
|
489
|
+
}
|
|
569
490
|
}
|
|
570
491
|
}
|
|
571
492
|
|
|
572
493
|
// ── standard-library shims ───────────────────────────────────────
|
|
573
|
-
//
|
|
574
|
-
//
|
|
575
|
-
//
|
|
576
|
-
//
|
|
577
|
-
// works regardless of which Scheme dialect it learned from.
|
|
494
|
+
// Fill the gaps the bootstrapped std library leaves: SRFI-1 helpers, Racket
|
|
495
|
+
// spellings, and cross-dialect aliases. std covers R7RS base, but a model
|
|
496
|
+
// trained on Racket/Chicken/Guile still reaches for names std doesn't bind.
|
|
497
|
+
// defineIfMissing so anything std already provides wins.
|
|
578
498
|
function installStdShims(env: any): void {
|
|
579
499
|
const defineIfMissing = (name: string, fn: any) => {
|
|
580
500
|
if ((env as any).get(name, { throwError: false }) === undefined) env.set(name, fn);
|
|
@@ -586,7 +506,6 @@ function installStdShims(env: any): void {
|
|
|
586
506
|
};
|
|
587
507
|
const truthy = (v: any) => v !== false;
|
|
588
508
|
|
|
589
|
-
// ── R7RS equality ─────────────────────────────────────────
|
|
590
509
|
// LIPS wraps numbers as LNumber instances, so `===` fails on equal-valued
|
|
591
510
|
// numbers from different sources. Handle the wrapper types before recursing.
|
|
592
511
|
const atomEqual = (a: any, b: any): boolean => {
|
|
@@ -594,7 +513,7 @@ function installStdShims(env: any): void {
|
|
|
594
513
|
if (a instanceof LNumber && b instanceof LNumber) return a.cmp(b) === 0;
|
|
595
514
|
if (typeof a === "number" && b instanceof LNumber) return LNumber(a).cmp(b) === 0;
|
|
596
515
|
if (typeof b === "number" && a instanceof LNumber) return LNumber(b).cmp(a) === 0;
|
|
597
|
-
if (a instanceof LSymbol && b instanceof LSymbol) return a
|
|
516
|
+
if (a instanceof LSymbol && b instanceof LSymbol) return symName(a) === symName(b);
|
|
598
517
|
return false;
|
|
599
518
|
};
|
|
600
519
|
const lipsEqual = (a: any, b: any): boolean => {
|
|
@@ -610,41 +529,13 @@ function installStdShims(env: any): void {
|
|
|
610
529
|
}
|
|
611
530
|
return false;
|
|
612
531
|
};
|
|
613
|
-
defineIfMissing("equal?", lipsEqual);
|
|
614
|
-
defineIfMissing("eqv?", atomEqual);
|
|
615
|
-
|
|
616
|
-
// ── R7RS list/member ─────────────────────────────────────
|
|
617
|
-
defineIfMissing("list?", (v: any) => v === nil || v instanceof Pair);
|
|
618
|
-
const memberLike = (eq: (a: any, b: any) => boolean) => (item: any, lst: any) => {
|
|
619
|
-
let p = lst;
|
|
620
|
-
while (p instanceof Pair) {
|
|
621
|
-
if (eq(p.car, item)) return p;
|
|
622
|
-
p = p.cdr;
|
|
623
|
-
}
|
|
624
|
-
return false;
|
|
625
|
-
};
|
|
626
|
-
defineIfMissing("member", memberLike(lipsEqual));
|
|
627
|
-
defineIfMissing("memq", memberLike((a, b) => a === b));
|
|
628
|
-
defineIfMissing("memv", memberLike((a, b) => a === b));
|
|
629
|
-
defineIfMissing("assv", (item: any, lst: any) => {
|
|
630
|
-
let p = lst;
|
|
631
|
-
while (p instanceof Pair) {
|
|
632
|
-
const e = p.car;
|
|
633
|
-
if (e instanceof Pair && e.car === item) return e;
|
|
634
|
-
p = p.cdr;
|
|
635
|
-
}
|
|
636
|
-
return false;
|
|
637
|
-
});
|
|
638
532
|
|
|
639
|
-
// ── SRFI-1 list helpers ──────────────────────────────────
|
|
640
533
|
defineIfMissing("first", (lst: any) => pairToArray(lst)[0]);
|
|
641
534
|
defineIfMissing("second", (lst: any) => pairToArray(lst)[1]);
|
|
642
535
|
defineIfMissing("third", (lst: any) => pairToArray(lst)[2]);
|
|
643
536
|
defineIfMissing("fourth", (lst: any) => pairToArray(lst)[3]);
|
|
644
537
|
defineIfMissing("fifth", (lst: any) => pairToArray(lst)[4]);
|
|
645
538
|
defineIfMissing("last", (lst: any) => { const a = pairToArray(lst); return a[a.length - 1]; });
|
|
646
|
-
defineIfMissing("take", (lst: any, n: any) => toSchemeList(pairToArray(lst).slice(0, Number(n))));
|
|
647
|
-
defineIfMissing("drop", (lst: any, n: any) => toSchemeList(pairToArray(lst).slice(Number(n))));
|
|
648
539
|
defineIfMissing("take-while", (pred: any, lst: any) => {
|
|
649
540
|
const out: any[] = [];
|
|
650
541
|
for (const x of pairToArray(lst)) { if (!truthy(pred(x))) break; out.push(x); }
|
|
@@ -662,7 +553,6 @@ function installStdShims(env: any): void {
|
|
|
662
553
|
return toSchemeList(Array.from({ length: n }, (_, i) => s + i * k));
|
|
663
554
|
});
|
|
664
555
|
defineIfMissing("any", (pred: any, lst: any) => pairToArray(lst).some((x) => truthy(pred(x))));
|
|
665
|
-
defineIfMissing("every", (pred: any, lst: any) => pairToArray(lst).every((x) => truthy(pred(x))));
|
|
666
556
|
defineIfMissing("count", (pred: any, lst: any) => pairToArray(lst).filter((x) => truthy(pred(x))).length);
|
|
667
557
|
defineIfMissing("filter-map", (f: any, lst: any) => {
|
|
668
558
|
const out: any[] = [];
|
|
@@ -688,44 +578,8 @@ function installStdShims(env: any): void {
|
|
|
688
578
|
for (const x of pairToArray(lst)) (truthy(pred(x)) ? t : f).push(x);
|
|
689
579
|
return new Pair(toSchemeList(t), toSchemeList(f));
|
|
690
580
|
});
|
|
691
|
-
|
|
692
|
-
const a = pairToArray(lst); let acc = init;
|
|
693
|
-
for (let i = a.length - 1; i >= 0; i--) acc = f(a[i], acc);
|
|
694
|
-
return acc;
|
|
695
|
-
});
|
|
696
|
-
defineIfMissing("zip", (a: any, b: any) => {
|
|
697
|
-
const xs = pairToArray(a); const ys = pairToArray(b);
|
|
698
|
-
const n = Math.min(xs.length, ys.length);
|
|
699
|
-
return toSchemeList(Array.from({ length: n }, (_, i) => toSchemeList([xs[i], ys[i]])));
|
|
700
|
-
});
|
|
701
|
-
|
|
702
|
-
// ── R7RS numeric predicates / ops ────────────────────────
|
|
703
|
-
defineIfMissing("zero?", (n: any) => Number(n) === 0);
|
|
704
|
-
defineIfMissing("positive?", (n: any) => Number(n) > 0);
|
|
705
|
-
defineIfMissing("negative?", (n: any) => Number(n) < 0);
|
|
706
|
-
defineIfMissing("odd?", (n: any) => Math.abs(Number(n)) % 2 === 1);
|
|
707
|
-
defineIfMissing("even?", (n: any) => Number(n) % 2 === 0);
|
|
708
|
-
defineIfMissing("modulo", (a: any, b: any) => { const m = Number(a) % Number(b); return (m < 0) === (Number(b) < 0) ? m : m + Number(b); });
|
|
709
|
-
defineIfMissing("quotient", (a: any, b: any) => Math.trunc(Number(a) / Number(b)));
|
|
710
|
-
defineIfMissing("remainder", (a: any, b: any) => Number(a) % Number(b));
|
|
711
|
-
defineIfMissing("expt", (a: any, b: any) => Math.pow(Number(a), Number(b)));
|
|
712
|
-
defineIfMissing("ceiling", (n: any) => Math.ceil(Number(n)));
|
|
713
|
-
|
|
714
|
-
// ── R7RS string ops ──────────────────────────────────────
|
|
715
|
-
defineIfMissing("string=?", (a: any, b: any) => String(a) === String(b));
|
|
716
|
-
defineIfMissing("string<?", (a: any, b: any) => String(a) < String(b));
|
|
717
|
-
defineIfMissing("string>?", (a: any, b: any) => String(a) > String(b));
|
|
718
|
-
defineIfMissing("string-upcase", (s: any) => String(s).toUpperCase());
|
|
719
|
-
defineIfMissing("string-downcase", (s: any) => String(s).toLowerCase());
|
|
720
|
-
defineIfMissing("string->list", (s: any) => toSchemeList(Array.from(String(s))));
|
|
721
|
-
defineIfMissing("list->string", (lst: any) => pairToArray(lst).map(String).join(""));
|
|
722
|
-
defineIfMissing("string-ref", (s: any, i: any) => {
|
|
723
|
-
const str = String(s);
|
|
724
|
-
const idx = Math.floor(Number(i));
|
|
725
|
-
return idx >= 0 && idx < str.length ? str[idx] : false;
|
|
726
|
-
});
|
|
581
|
+
|
|
727
582
|
defineIfMissing("string-trim-both", (s: any) => String(s).trim());
|
|
728
|
-
defineIfMissing("identity", (x: any) => x);
|
|
729
583
|
// Pattern can be string or (regexp "pat"). Racket (?i:…) / (?m:…) inline
|
|
730
584
|
// flag groups are translated to JS RegExp flags.
|
|
731
585
|
const compileRegex = (pat: any): RegExp => {
|
|
@@ -741,6 +595,7 @@ function installStdShims(env: any): void {
|
|
|
741
595
|
};
|
|
742
596
|
defineIfMissing("regexp", (pat: any) => compileRegex(pat));
|
|
743
597
|
const regexpMatch = (pat: any, s: any) => {
|
|
598
|
+
s = toJsStr(s);
|
|
744
599
|
if (typeof s !== "string") return false;
|
|
745
600
|
const m = s.match(compileRegex(pat));
|
|
746
601
|
return m ? toSchemeList(Array.from(m)) : false;
|
|
@@ -763,6 +618,7 @@ function installStdShims(env: any): void {
|
|
|
763
618
|
return false;
|
|
764
619
|
});
|
|
765
620
|
defineIfMissing("regexp-match-positions", (pat: any, s: any) => {
|
|
621
|
+
s = toJsStr(s);
|
|
766
622
|
if (typeof s !== "string") return false;
|
|
767
623
|
const m = s.match(compileRegex(pat));
|
|
768
624
|
if (!m) return false;
|
|
@@ -771,101 +627,25 @@ function installStdShims(env: any): void {
|
|
|
771
627
|
return new Pair(full, nil);
|
|
772
628
|
});
|
|
773
629
|
|
|
774
|
-
// ── Racket spellings ─────────────────────────────────────
|
|
775
|
-
defineIfMissing("string-split", (s: any, sep: any) => {
|
|
776
|
-
if (typeof s !== "string") return nil;
|
|
777
|
-
if (sep === undefined) return toSchemeList(s.split(/\s+/).filter(Boolean));
|
|
778
|
-
return toSchemeList(s.split(String(sep)));
|
|
779
|
-
});
|
|
780
|
-
defineIfMissing("string-join", (lst: any, sep: any) =>
|
|
781
|
-
pairToArray(lst).map(String).join(sep === undefined ? " " : String(sep)));
|
|
782
630
|
defineIfMissing("displayln", function (this: any, x: any) {
|
|
783
631
|
const display = (env as any).get("display", { throwError: false });
|
|
784
632
|
if (display) { display(x); display("\n"); }
|
|
785
633
|
});
|
|
786
634
|
|
|
787
|
-
// ── R7RS error/exit ──────────────────────────────────────
|
|
788
635
|
defineIfMissing("error", (...msgs: any[]) => {
|
|
789
636
|
throw new Error(msgs.map((m) => (typeof m === "string" ? m : String(m))).join(" "));
|
|
790
637
|
});
|
|
791
638
|
defineIfMissing("void", () => undefined);
|
|
792
639
|
|
|
793
|
-
// ── R7RS write (LIPS' display is good enough; write quotes strings) ──
|
|
794
640
|
defineIfMissing("write", function (this: any, x: any) {
|
|
795
641
|
const display = (env as any).get("display", { throwError: false });
|
|
796
642
|
if (display) display(typeof x === "string" ? JSON.stringify(x) : x);
|
|
797
643
|
});
|
|
798
644
|
|
|
799
|
-
// ── R7RS numbers (gaps) ────────────────────────────────────
|
|
800
|
-
defineIfMissing("abs", (n: any) => Math.abs(Number(n)));
|
|
801
|
-
defineIfMissing("floor", (n: any) => Math.floor(Number(n)));
|
|
802
|
-
defineIfMissing("round", (n: any) => Math.round(Number(n)));
|
|
803
|
-
defineIfMissing("truncate", (n: any) => Math.trunc(Number(n)));
|
|
804
|
-
defineIfMissing("sqrt", (n: any) => Math.sqrt(Number(n)));
|
|
805
|
-
defineIfMissing("log", (n: any, base: any) =>
|
|
806
|
-
base === undefined ? Math.log(Number(n)) : Math.log(Number(n)) / Math.log(Number(base)));
|
|
807
|
-
defineIfMissing("exp", (n: any) => Math.exp(Number(n)));
|
|
808
|
-
defineIfMissing("sin", (n: any) => Math.sin(Number(n)));
|
|
809
|
-
defineIfMissing("cos", (n: any) => Math.cos(Number(n)));
|
|
810
|
-
defineIfMissing("tan", (n: any) => Math.tan(Number(n)));
|
|
811
|
-
defineIfMissing("asin", (n: any) => Math.asin(Number(n)));
|
|
812
|
-
defineIfMissing("acos", (n: any) => Math.acos(Number(n)));
|
|
813
|
-
defineIfMissing("atan", (a: any, b: any) =>
|
|
814
|
-
b === undefined ? Math.atan(Number(a)) : Math.atan2(Number(a), Number(b)));
|
|
815
|
-
defineIfMissing("gcd", (...args: any[]) => {
|
|
816
|
-
const gcd2 = (a: number, b: number): number => b === 0 ? Math.abs(a) : gcd2(b, a % b);
|
|
817
|
-
return args.length === 0 ? 0 : args.map(Number).reduce(gcd2);
|
|
818
|
-
});
|
|
819
|
-
defineIfMissing("lcm", (...args: any[]) => {
|
|
820
|
-
const gcd2 = (a: number, b: number): number => b === 0 ? Math.abs(a) : gcd2(b, a % b);
|
|
821
|
-
const lcm2 = (a: number, b: number): number => a && b ? Math.abs(a * b) / gcd2(a, b) : 0;
|
|
822
|
-
return args.length === 0 ? 1 : args.map(Number).reduce(lcm2);
|
|
823
|
-
});
|
|
824
|
-
defineIfMissing("exact", (n: any) => Math.round(Number(n)));
|
|
825
|
-
defineIfMissing("inexact", (n: any) => Number(n));
|
|
826
|
-
defineIfMissing("exact->inexact", (n: any) => Number(n));
|
|
827
|
-
defineIfMissing("inexact->exact", (n: any) => Math.round(Number(n)));
|
|
828
|
-
defineIfMissing("exact-integer?", (n: any) => Number.isInteger(Number(n)));
|
|
829
|
-
defineIfMissing("exact?", (n: any) => Number.isInteger(Number(n)));
|
|
830
|
-
defineIfMissing("inexact?", (n: any) => !Number.isInteger(Number(n)));
|
|
831
|
-
defineIfMissing("=", (...args: any[]) => {
|
|
832
|
-
if (args.length < 2) return true;
|
|
833
|
-
const first = Number(args[0]);
|
|
834
|
-
for (let i = 1; i < args.length; i++) if (Number(args[i]) !== first) return false;
|
|
835
|
-
return true;
|
|
836
|
-
});
|
|
837
|
-
defineIfMissing("finite?", (n: any) => Number.isFinite(Number(n)));
|
|
838
|
-
defineIfMissing("infinite?", (n: any) => !Number.isFinite(Number(n)) && !Number.isNaN(Number(n)));
|
|
839
|
-
defineIfMissing("nan?", (n: any) => Number.isNaN(Number(n)));
|
|
840
645
|
defineIfMissing("add1", (n: any) => Number(n) + 1);
|
|
841
646
|
defineIfMissing("sub1", (n: any) => Number(n) - 1);
|
|
842
647
|
defineIfMissing("sqr", (n: any) => Number(n) * Number(n));
|
|
843
648
|
|
|
844
|
-
// ── R7RS predicates ────────────────────────────────────────
|
|
845
|
-
defineIfMissing("boolean?", (x: any) => x === true || x === false);
|
|
846
|
-
defineIfMissing("boolean=?", (a: any, b: any) => a === b && (a === true || a === false));
|
|
847
|
-
defineIfMissing("procedure?", (x: any) => typeof x === "function");
|
|
848
|
-
defineIfMissing("symbol?", (x: any) => x instanceof LSymbol);
|
|
849
|
-
defineIfMissing("symbol=?", (a: any, b: any) =>
|
|
850
|
-
a instanceof LSymbol && b instanceof LSymbol && (a as any).name === (b as any).name);
|
|
851
|
-
defineIfMissing("string->symbol", (s: any) => new LSymbol(String(s)));
|
|
852
|
-
defineIfMissing("integer?", (x: any) => typeof x === "number" ? Number.isInteger(x)
|
|
853
|
-
: (x && typeof x.valueOf === "function" && Number.isInteger(Number(x.valueOf()))));
|
|
854
|
-
|
|
855
|
-
// ── R7RS strings (gaps) ────────────────────────────────────
|
|
856
|
-
defineIfMissing("substring", (s: any, start: any, end: any) => {
|
|
857
|
-
const str = String(s);
|
|
858
|
-
const a = Math.max(0, Math.floor(Number(start) || 0));
|
|
859
|
-
const b = end === undefined ? str.length : Math.min(str.length, Math.floor(Number(end)));
|
|
860
|
-
return str.slice(a, b);
|
|
861
|
-
});
|
|
862
|
-
defineIfMissing("string-copy", (s: any) => String(s));
|
|
863
|
-
defineIfMissing("make-string", (n: any, ch?: any) => {
|
|
864
|
-
const len = Math.max(0, Math.floor(Number(n) || 0));
|
|
865
|
-
const c = ch === undefined ? " " : String(ch);
|
|
866
|
-
return c.length === 1 ? c.repeat(len) : (c + "").repeat(len).slice(0, len);
|
|
867
|
-
});
|
|
868
|
-
defineIfMissing("string-foldcase", (s: any) => String(s).toLowerCase());
|
|
869
649
|
defineIfMissing("string-trim", (s: any) => String(s).trim());
|
|
870
650
|
defineIfMissing("string-trim-left", (s: any) => String(s).replace(/^\s+/, ""));
|
|
871
651
|
defineIfMissing("string-trim-right", (s: any) => String(s).replace(/\s+$/, ""));
|
|
@@ -873,34 +653,15 @@ function installStdShims(env: any): void {
|
|
|
873
653
|
String(s).startsWith(String(prefix)));
|
|
874
654
|
defineIfMissing("string-suffix?", (suffix: any, s: any) =>
|
|
875
655
|
String(s).endsWith(String(suffix)));
|
|
876
|
-
defineIfMissing("non-empty-string?", (x: any) =>
|
|
877
|
-
|
|
656
|
+
defineIfMissing("non-empty-string?", (x: any) => {
|
|
657
|
+
x = toJsStr(x);
|
|
658
|
+
return typeof x === "string" && x.length > 0;
|
|
659
|
+
});
|
|
878
660
|
defineIfMissing("string-index", (s: any, needle: any) => {
|
|
879
661
|
const i = String(s).indexOf(String(needle));
|
|
880
662
|
return i < 0 ? false : i;
|
|
881
663
|
});
|
|
882
|
-
|
|
883
|
-
String(a).toLowerCase() === String(b).toLowerCase());
|
|
884
|
-
defineIfMissing("string-ci<?", (a: any, b: any) =>
|
|
885
|
-
String(a).toLowerCase() < String(b).toLowerCase());
|
|
886
|
-
defineIfMissing("string-ci>?", (a: any, b: any) =>
|
|
887
|
-
String(a).toLowerCase() > String(b).toLowerCase());
|
|
888
|
-
defineIfMissing("string<=?", (a: any, b: any) => String(a) <= String(b));
|
|
889
|
-
defineIfMissing("string>=?", (a: any, b: any) => String(a) >= String(b));
|
|
890
|
-
|
|
891
|
-
// ── R7RS / SRFI-1 list gaps ────────────────────────────────
|
|
892
|
-
defineIfMissing("list-tail", (lst: any, n: any) => {
|
|
893
|
-
let k = Math.floor(Number(n) || 0);
|
|
894
|
-
let cur: any = lst;
|
|
895
|
-
while (k-- > 0 && cur instanceof Pair) cur = cur.cdr;
|
|
896
|
-
return cur;
|
|
897
|
-
});
|
|
898
|
-
defineIfMissing("list-ref", (lst: any, n: any) => {
|
|
899
|
-
let k = Math.floor(Number(n) || 0);
|
|
900
|
-
let cur: any = lst;
|
|
901
|
-
while (k-- > 0 && cur instanceof Pair) cur = cur.cdr;
|
|
902
|
-
return cur instanceof Pair ? cur.car : false;
|
|
903
|
-
});
|
|
664
|
+
|
|
904
665
|
defineIfMissing("list-index", (pred: any, lst: any) => {
|
|
905
666
|
let i = 0, cur: any = lst;
|
|
906
667
|
while (cur instanceof Pair) {
|
|
@@ -940,13 +701,6 @@ function installStdShims(env: any): void {
|
|
|
940
701
|
for (let i = args.length - 2; i >= 0; i--) tail = new Pair(args[i], tail);
|
|
941
702
|
return tail;
|
|
942
703
|
});
|
|
943
|
-
defineIfMissing("list*", (...args: any[]) => {
|
|
944
|
-
if (args.length === 0) return nil;
|
|
945
|
-
if (args.length === 1) return args[0];
|
|
946
|
-
let tail: any = args[args.length - 1];
|
|
947
|
-
for (let i = args.length - 2; i >= 0; i--) tail = new Pair(args[i], tail);
|
|
948
|
-
return tail;
|
|
949
|
-
});
|
|
950
704
|
defineIfMissing("append-reverse", (a: any, b: any) => {
|
|
951
705
|
let cur: any = a, out: any = b;
|
|
952
706
|
while (cur instanceof Pair) { out = new Pair(cur.car, out); cur = cur.cdr; }
|
|
@@ -989,26 +743,6 @@ function installStdShims(env: any): void {
|
|
|
989
743
|
}
|
|
990
744
|
return toSchemeList(out);
|
|
991
745
|
});
|
|
992
|
-
// LIPS' `range` is single-arg only; override (not defineIfMissing) so the
|
|
993
|
-
// 1/2/3-arg Racket form wins.
|
|
994
|
-
env.set("range", (a: any, b: any, step: any) => {
|
|
995
|
-
let start: number, end: number, st: number;
|
|
996
|
-
if (b === undefined) { start = 0; end = Number(a); st = 1; }
|
|
997
|
-
else { start = Number(a); end = Number(b); st = step === undefined ? 1 : Number(step); }
|
|
998
|
-
const out: number[] = [];
|
|
999
|
-
if (st > 0) for (let i = start; i < end; i += st) out.push(i);
|
|
1000
|
-
else if (st < 0) for (let i = start; i > end; i += st) out.push(i);
|
|
1001
|
-
return toSchemeList(out);
|
|
1002
|
-
});
|
|
1003
|
-
defineIfMissing("flatten", (lst: any) => {
|
|
1004
|
-
const out: any[] = [];
|
|
1005
|
-
const walk = (x: any) => {
|
|
1006
|
-
if (x instanceof Pair) { let c: any = x; while (c instanceof Pair) { walk(c.car); c = c.cdr; } }
|
|
1007
|
-
else if (x !== nil) out.push(x);
|
|
1008
|
-
};
|
|
1009
|
-
walk(lst);
|
|
1010
|
-
return toSchemeList(out);
|
|
1011
|
-
});
|
|
1012
746
|
defineIfMissing("index-of", (lst: any, x: any) => {
|
|
1013
747
|
let i = 0, cur: any = lst;
|
|
1014
748
|
while (cur instanceof Pair) {
|
|
@@ -1060,7 +794,6 @@ function installStdShims(env: any): void {
|
|
|
1060
794
|
return toSchemeList(groups.map((g) => toSchemeList(g.items)));
|
|
1061
795
|
});
|
|
1062
796
|
|
|
1063
|
-
// ── Regex (Racket) ─────────────────────────────────────────
|
|
1064
797
|
const reCompile = (pat: any): RegExp => {
|
|
1065
798
|
if (pat instanceof RegExp) return pat;
|
|
1066
799
|
let p = String(pat);
|
|
@@ -1074,10 +807,12 @@ function installStdShims(env: any): void {
|
|
|
1074
807
|
};
|
|
1075
808
|
defineIfMissing("regexp?", (x: any) => x instanceof RegExp);
|
|
1076
809
|
defineIfMissing("regexp-replace", (pat: any, s: any, repl: any) => {
|
|
810
|
+
s = toJsStr(s);
|
|
1077
811
|
if (typeof s !== "string") return s;
|
|
1078
812
|
return s.replace(reCompile(pat), String(repl));
|
|
1079
813
|
});
|
|
1080
814
|
defineIfMissing("regexp-replace*", (pat: any, s: any, repl: any) => {
|
|
815
|
+
s = toJsStr(s);
|
|
1081
816
|
if (typeof s !== "string") return s;
|
|
1082
817
|
const re = reCompile(pat);
|
|
1083
818
|
const global = re.flags.includes("g") ? re : new RegExp(re.source, re.flags + "g");
|
|
@@ -1085,10 +820,11 @@ function installStdShims(env: any): void {
|
|
|
1085
820
|
});
|
|
1086
821
|
defineIfMissing("regexp-quote", (s: any) =>
|
|
1087
822
|
String(s).replace(/[.*+?^${}()|[\]\\]/g, "\\$&"));
|
|
1088
|
-
defineIfMissing("regexp-split", (pat: any, s: any) =>
|
|
1089
|
-
|
|
823
|
+
defineIfMissing("regexp-split", (pat: any, s: any) => {
|
|
824
|
+
s = toJsStr(s);
|
|
825
|
+
return typeof s === "string" ? toSchemeList(s.split(reCompile(pat))) : nil;
|
|
826
|
+
});
|
|
1090
827
|
|
|
1091
|
-
// ── Format (Racket) ────────────────────────────────────────
|
|
1092
828
|
// format: simple ~a ~s ~v ~n support — covers most logging/inspection
|
|
1093
829
|
defineIfMissing("format", (fmt: any, ...rest: any[]) => {
|
|
1094
830
|
const f = String(fmt);
|
|
@@ -1117,7 +853,6 @@ function installStdShims(env: any): void {
|
|
|
1117
853
|
defineIfMissing("~v", (...xs: any[]) =>
|
|
1118
854
|
xs.map((x) => typeof x === "string" ? JSON.stringify(x) : (x === undefined ? "" : x.toString())).join(""));
|
|
1119
855
|
|
|
1120
|
-
// ── Hash tables (Racket) ───────────────────────────────────
|
|
1121
856
|
// Backed by JS Map. Stored as `LipsHash` symbol so we can pattern-match.
|
|
1122
857
|
class LipsHash {
|
|
1123
858
|
map: Map<any, any> = new Map();
|
|
@@ -1125,7 +860,7 @@ function installStdShims(env: any): void {
|
|
|
1125
860
|
if (entries) for (const [k, v] of entries) this.map.set(this._key(k), v);
|
|
1126
861
|
}
|
|
1127
862
|
_key(k: any): any {
|
|
1128
|
-
if (k instanceof LSymbol) return "::sym::" + (k
|
|
863
|
+
if (k instanceof LSymbol) return "::sym::" + symName(k);
|
|
1129
864
|
if (typeof k === "object" && k !== null) return JSON.stringify(k);
|
|
1130
865
|
return k;
|
|
1131
866
|
}
|
|
@@ -1182,20 +917,16 @@ function installStdShims(env: any): void {
|
|
|
1182
917
|
defineIfMissing("hash-values", (h: any) => h instanceof LipsHash ? toSchemeList(h.values()) : nil);
|
|
1183
918
|
defineIfMissing("hash-count", (h: any) => h instanceof LipsHash ? h.size() : 0);
|
|
1184
919
|
|
|
1185
|
-
// ── Sort (R7RS-large / SRFI-132 / Racket) ──────────────────
|
|
1186
920
|
// V8's Array.prototype.sort is stable (ES2019), so one impl serves all.
|
|
1187
921
|
const sortImpl = (lst: any, less: any) => {
|
|
1188
922
|
const arr = pairToArray(lst).slice();
|
|
1189
923
|
arr.sort((a, b) => (truthy(less(a, b)) ? -1 : truthy(less(b, a)) ? 1 : 0));
|
|
1190
924
|
return toSchemeList(arr);
|
|
1191
925
|
};
|
|
1192
|
-
defineIfMissing("sort", sortImpl);
|
|
1193
926
|
defineIfMissing("sort!", sortImpl);
|
|
1194
927
|
// SRFI-132 / R7RS-large flips the argument order.
|
|
1195
928
|
defineIfMissing("list-sort", (less: any, lst: any) => sortImpl(lst, less));
|
|
1196
929
|
|
|
1197
|
-
// ── Racket list aliases & gaps ─────────────────────────────
|
|
1198
|
-
defineIfMissing("empty?", (v: any) => v === nil);
|
|
1199
930
|
defineIfMissing("empty", nil);
|
|
1200
931
|
defineIfMissing("cons?", (v: any) => v instanceof Pair);
|
|
1201
932
|
defineIfMissing("andmap", (pred: any, lst: any) => pairToArray(lst).every((x) => truthy(pred(x))));
|
|
@@ -1213,11 +944,6 @@ function installStdShims(env: any): void {
|
|
|
1213
944
|
}
|
|
1214
945
|
return false;
|
|
1215
946
|
});
|
|
1216
|
-
defineIfMissing("make-list", (n: any, v: any) => {
|
|
1217
|
-
const count = Math.max(0, Math.floor(Number(n) || 0));
|
|
1218
|
-
const fill = v === undefined ? nil : v;
|
|
1219
|
-
return toSchemeList(Array(count).fill(fill));
|
|
1220
|
-
});
|
|
1221
947
|
defineIfMissing("build-list", (n: any, proc: any) => {
|
|
1222
948
|
const count = Math.max(0, Math.floor(Number(n) || 0));
|
|
1223
949
|
const out: any[] = [];
|
|
@@ -1239,14 +965,6 @@ function installStdShims(env: any): void {
|
|
|
1239
965
|
const k = Math.max(0, Math.floor(Number(n) || 0));
|
|
1240
966
|
return new Pair(toSchemeList(a.slice(0, k)), toSchemeList(a.slice(k)));
|
|
1241
967
|
});
|
|
1242
|
-
defineIfMissing("shuffle", (lst: any) => {
|
|
1243
|
-
const a = pairToArray(lst).slice();
|
|
1244
|
-
for (let i = a.length - 1; i > 0; i--) {
|
|
1245
|
-
const j = Math.floor(Math.random() * (i + 1));
|
|
1246
|
-
[a[i], a[j]] = [a[j], a[i]];
|
|
1247
|
-
}
|
|
1248
|
-
return toSchemeList(a);
|
|
1249
|
-
});
|
|
1250
968
|
defineIfMissing("add-between", (lst: any, sep: any) => {
|
|
1251
969
|
const a = pairToArray(lst);
|
|
1252
970
|
if (a.length < 2) return toSchemeList(a);
|
|
@@ -1368,16 +1086,8 @@ function installStdShims(env: any): void {
|
|
|
1368
1086
|
return toSchemeList(arr.filter((x) => !targets.some((t) => lipsEqual(t, x))));
|
|
1369
1087
|
});
|
|
1370
1088
|
|
|
1371
|
-
// ── Racket numbers (gaps) ──────────────────────────────────
|
|
1372
1089
|
defineIfMissing("pi", Math.PI);
|
|
1373
1090
|
// Racket overloads: (random) 0≤x<1, (random k) 0≤i<k, (random lo hi).
|
|
1374
|
-
defineIfMissing("random", (a: any, b: any) => {
|
|
1375
|
-
if (a === undefined) return Math.random();
|
|
1376
|
-
if (b === undefined) return Math.floor(Math.random() * Math.max(0, Math.floor(Number(a))));
|
|
1377
|
-
const lo = Math.floor(Number(a));
|
|
1378
|
-
const hi = Math.floor(Number(b));
|
|
1379
|
-
return lo + Math.floor(Math.random() * Math.max(0, hi - lo));
|
|
1380
|
-
});
|
|
1381
1091
|
defineIfMissing("exact-floor", (n: any) => Math.floor(Number(n)));
|
|
1382
1092
|
defineIfMissing("exact-ceiling", (n: any) => Math.ceil(Number(n)));
|
|
1383
1093
|
defineIfMissing("exact-round", (n: any) => Math.round(Number(n)));
|
|
@@ -1396,7 +1106,6 @@ function installStdShims(env: any): void {
|
|
|
1396
1106
|
return Number(n).toFixed(d);
|
|
1397
1107
|
});
|
|
1398
1108
|
|
|
1399
|
-
// ── Racket strings & chars (gaps) ──────────────────────────
|
|
1400
1109
|
defineIfMissing("string-titlecase", (s: any) =>
|
|
1401
1110
|
String(s).replace(/\b([a-z])/g, (_, c) => c.toUpperCase()));
|
|
1402
1111
|
defineIfMissing("string-pad", (s: any, width: any, ch?: any) => {
|
|
@@ -1411,27 +1120,6 @@ function installStdShims(env: any): void {
|
|
|
1411
1120
|
const c = ch === undefined ? " " : String(ch).charAt(0) || " ";
|
|
1412
1121
|
return str.length >= w ? str : str + c.repeat(w - str.length);
|
|
1413
1122
|
});
|
|
1414
|
-
defineIfMissing("char-upcase", (c: any) => String(c).toUpperCase());
|
|
1415
|
-
defineIfMissing("char-downcase", (c: any) => String(c).toLowerCase());
|
|
1416
|
-
// Characters are 1-char strings; char? therefore can't tell a char from a
|
|
1417
|
-
// length-1 string.
|
|
1418
|
-
const charStr = (c: any) => (typeof c === "string" ? c : String(c));
|
|
1419
|
-
const codeOf = (c: any) => charStr(c).codePointAt(0) ?? 0;
|
|
1420
|
-
defineIfMissing("char->integer", (c: any) => codeOf(c));
|
|
1421
|
-
defineIfMissing("integer->char", (n: any) => String.fromCodePoint(Math.max(0, Math.floor(Number(n) || 0))));
|
|
1422
|
-
defineIfMissing("char?", (x: any) => typeof x === "string" && Array.from(x).length === 1);
|
|
1423
|
-
defineIfMissing("char=?", (a: any, b: any) => charStr(a) === charStr(b));
|
|
1424
|
-
defineIfMissing("char<?", (a: any, b: any) => codeOf(a) < codeOf(b));
|
|
1425
|
-
defineIfMissing("char>?", (a: any, b: any) => codeOf(a) > codeOf(b));
|
|
1426
|
-
defineIfMissing("char<=?", (a: any, b: any) => codeOf(a) <= codeOf(b));
|
|
1427
|
-
defineIfMissing("char>=?", (a: any, b: any) => codeOf(a) >= codeOf(b));
|
|
1428
|
-
defineIfMissing("char-ci=?", (a: any, b: any) => charStr(a).toLowerCase() === charStr(b).toLowerCase());
|
|
1429
|
-
defineIfMissing("char-alphabetic?", (c: any) => /^\p{L}$/u.test(charStr(c)));
|
|
1430
|
-
defineIfMissing("char-numeric?", (c: any) => /^\p{Nd}$/u.test(charStr(c)));
|
|
1431
|
-
defineIfMissing("char-whitespace?", (c: any) => /^\s$/.test(charStr(c)));
|
|
1432
|
-
defineIfMissing("char-upper-case?", (c: any) => { const s = charStr(c); return s.length === 1 && s === s.toUpperCase() && s !== s.toLowerCase(); });
|
|
1433
|
-
defineIfMissing("char-lower-case?", (c: any) => { const s = charStr(c); return s.length === 1 && s === s.toLowerCase() && s !== s.toUpperCase(); });
|
|
1434
|
-
defineIfMissing("digit-value", (c: any) => { const s = charStr(c); return /^[0-9]$/.test(s) ? Number(s) : false; });
|
|
1435
1123
|
defineIfMissing("string-normalize-spaces", (s: any, sep?: any, repl?: any) => {
|
|
1436
1124
|
const str = String(s).trim();
|
|
1437
1125
|
const splitOn = sep === undefined ? /\s+/ : (sep instanceof RegExp ? sep : new RegExp(String(sep)));
|
|
@@ -1445,7 +1133,6 @@ function installStdShims(env: any): void {
|
|
|
1445
1133
|
return out;
|
|
1446
1134
|
});
|
|
1447
1135
|
|
|
1448
|
-
// ── Racket hash (gaps) ─────────────────────────────────────
|
|
1449
1136
|
defineIfMissing("hash-update!", (h: any, k: any, upd: any, dflt: any) => {
|
|
1450
1137
|
if (!(h instanceof LipsHash)) return h;
|
|
1451
1138
|
const cur = h.has(k) ? h.get(k) : (typeof dflt === "function" ? dflt() : dflt);
|
|
@@ -1524,14 +1211,12 @@ function installStdShims(env: any): void {
|
|
|
1524
1211
|
defineIfMissing("make-hasheqv", (alist?: any) => (env as any).get("make-hash")(alist));
|
|
1525
1212
|
defineIfMissing("make-immutable-hash", (alist?: any) => (env as any).get("make-hash")(alist));
|
|
1526
1213
|
|
|
1527
|
-
// ── Racket boxes (mutable cells) ───────────────────────────
|
|
1528
1214
|
class LipsBox { constructor(public v: any) {} }
|
|
1529
1215
|
defineIfMissing("box", (v: any) => new LipsBox(v));
|
|
1530
1216
|
defineIfMissing("box?", (x: any) => x instanceof LipsBox);
|
|
1531
1217
|
defineIfMissing("unbox", (b: any) => b instanceof LipsBox ? b.v : b);
|
|
1532
1218
|
defineIfMissing("set-box!", (b: any, v: any) => { if (b instanceof LipsBox) b.v = v; return undefined; });
|
|
1533
1219
|
|
|
1534
|
-
// ── Environment introspection ──────────────────────────────
|
|
1535
1220
|
const collectEnvNames = (): string[] => {
|
|
1536
1221
|
const seen = new Set<string>();
|
|
1537
1222
|
let cur: any = env;
|
|
@@ -1543,19 +1228,17 @@ function installStdShims(env: any): void {
|
|
|
1543
1228
|
return Array.from(seen).sort();
|
|
1544
1229
|
};
|
|
1545
1230
|
defineIfMissing("defined?", (sym: any) => {
|
|
1546
|
-
const name = sym instanceof LSymbol ? (sym
|
|
1231
|
+
const name = sym instanceof LSymbol ? symName(sym) : String(sym);
|
|
1547
1232
|
return (env as any).get(name, { throwError: false }) !== undefined;
|
|
1548
1233
|
});
|
|
1549
1234
|
const aproposImpl = (pat: any) => {
|
|
1550
|
-
const needle = pat instanceof LSymbol ? (pat
|
|
1235
|
+
const needle = pat instanceof LSymbol ? (symName(pat) ?? "") : String(pat ?? "");
|
|
1551
1236
|
const re = pat instanceof RegExp ? pat : null;
|
|
1552
1237
|
const match = (n: string) => re ? re.test(n) : n.includes(needle);
|
|
1553
1238
|
return toSchemeList(collectEnvNames().filter(match).map((n) => new LSymbol(n)));
|
|
1554
1239
|
};
|
|
1555
|
-
defineIfMissing("apropos", aproposImpl);
|
|
1556
1240
|
defineIfMissing("apropos-list", aproposImpl);
|
|
1557
1241
|
|
|
1558
|
-
// ── Misc Racket ────────────────────────────────────────────
|
|
1559
1242
|
defineIfMissing("current-seconds", () => Math.floor(Date.now() / 1000));
|
|
1560
1243
|
defineIfMissing("current-milliseconds", () => Date.now());
|
|
1561
1244
|
defineIfMissing("current-inexact-milliseconds", () => performance.now());
|
|
@@ -1564,7 +1247,7 @@ function installStdShims(env: any): void {
|
|
|
1564
1247
|
// Canonical names we claim coverage for. R = R7RS small base; S = SRFI-1;
|
|
1565
1248
|
// K = Racket racket/base/list/string/format. Continuations, ports, and
|
|
1566
1249
|
// bytevectors are intentionally omitted — they'd be misleading "coverage"
|
|
1567
|
-
// without real functionality.
|
|
1250
|
+
// without real functionality.
|
|
1568
1251
|
const COVERAGE_CHECKLIST: string[] = [
|
|
1569
1252
|
// R7RS § 6.1 equivalence
|
|
1570
1253
|
"eq?", "eqv?", "equal?",
|
|
@@ -1685,7 +1368,7 @@ function auditShimCoverage(env: any): { defined: number; missing: string[] } {
|
|
|
1685
1368
|
function unwrapSchemeBool(v: any): any {
|
|
1686
1369
|
if (v === true || v === false) return v;
|
|
1687
1370
|
if (v instanceof LSymbol) {
|
|
1688
|
-
const n = (v
|
|
1371
|
+
const n = symName(v);
|
|
1689
1372
|
if (n === "#t") return true;
|
|
1690
1373
|
if (n === "#f") return false;
|
|
1691
1374
|
}
|
|
@@ -1694,38 +1377,94 @@ function unwrapSchemeBool(v: any): any {
|
|
|
1694
1377
|
return v;
|
|
1695
1378
|
}
|
|
1696
1379
|
|
|
1697
|
-
//
|
|
1698
|
-
//
|
|
1699
|
-
//
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1380
|
+
// Single source of truth for the host primitive surface: drives both the tool
|
|
1381
|
+
// description and `(help …)`, so what the model reads matches what it can
|
|
1382
|
+
// introspect at runtime. Each binding returns the natural Scheme value for its
|
|
1383
|
+
// job; bash returns a record because the exit code has nowhere else to live (a
|
|
1384
|
+
// plain bash tool call drops it before the model ever sees it).
|
|
1385
|
+
type HostSig = { name: string; sig: string; ret: string; doc: string };
|
|
1386
|
+
const HOST_SIGS: HostSig[] = [
|
|
1387
|
+
{ name: "bash", sig: '(bash "cmd" [:timeout sec])',
|
|
1388
|
+
ret: "((output . str) (exit-code . n) (error . bool))",
|
|
1389
|
+
doc: "run a shell command; full result. Accessors: output-of exit-code-of ok? error?" },
|
|
1390
|
+
{ name: "sh", sig: '(sh "cmd" [:timeout sec])', ret: "str",
|
|
1391
|
+
doc: "run a shell command, return stdout only (stderr text on failure)" },
|
|
1392
|
+
{ name: "read-file", sig: '(read-file "path" [:offset n] [:limit n])', ret: "str | #f",
|
|
1393
|
+
doc: "file contents, or #f on error. :offset is 1-indexed; :limit caps lines" },
|
|
1394
|
+
{ name: "write-file", sig: '(write-file "path" "content")', ret: "#t | err-str",
|
|
1395
|
+
doc: "overwrite a file" },
|
|
1396
|
+
{ name: "edit-file", sig: '(edit-file "path" "old" "new" [:replace-all #t])', ret: "#t | err-str",
|
|
1397
|
+
doc: "replace exact text; :replace-all #t replaces every occurrence" },
|
|
1398
|
+
{ name: "grep", sig: '(grep "pat" ["dir"] [:opt val …])',
|
|
1399
|
+
ret: "(listof ((file . str) (line . n) (text . str)))",
|
|
1400
|
+
doc: "ripgrep search. options: :path :include :case-insensitive :context-before :context-after :limit :offset" },
|
|
1401
|
+
{ name: "grep-files", sig: '(grep-files "pat" ["dir"] [:opt val …])', ret: "(listof str)",
|
|
1402
|
+
doc: "files containing a match. options: :path :include :case-insensitive :limit :offset" },
|
|
1403
|
+
{ name: "glob", sig: '(glob "pat" [:path "dir"])', ret: "(listof str)",
|
|
1404
|
+
doc: "paths matching a glob, mtime-sorted" },
|
|
1405
|
+
];
|
|
1406
|
+
const sigLine = (h: HostSig): string => `${h.sig} → ${h.ret}`;
|
|
1407
|
+
const sigForName = (name: string): string => {
|
|
1408
|
+
const h = HOST_SIGS.find((s) => s.name === name);
|
|
1409
|
+
return h ? sigLine(h) : name;
|
|
1410
|
+
};
|
|
1411
|
+
|
|
1412
|
+
// Append a primitive's signature to any exception it throws, so a malformed
|
|
1413
|
+
// call teaches the right shape in one round-trip instead of a bare stack.
|
|
1414
|
+
function withSig(name: string, fn: (...a: any[]) => Promise<any>) {
|
|
1415
|
+
return async (...a: any[]) => {
|
|
1416
|
+
try {
|
|
1417
|
+
return await fn(...a);
|
|
1418
|
+
} catch (e: any) {
|
|
1419
|
+
const base = e?.message ?? String(e);
|
|
1420
|
+
if (!String(base).includes("signature:")) {
|
|
1421
|
+
try { e.message = `${base}\n signature: ${sigForName(name)}`; } catch { /* frozen */ }
|
|
1718
1422
|
}
|
|
1423
|
+
throw e;
|
|
1719
1424
|
}
|
|
1720
|
-
|
|
1425
|
+
};
|
|
1426
|
+
}
|
|
1427
|
+
|
|
1428
|
+
const isKwSym = (x: any): boolean => {
|
|
1429
|
+
const n = symName(x);
|
|
1430
|
+
return typeof n === "string" && n.startsWith(":");
|
|
1431
|
+
};
|
|
1432
|
+
|
|
1433
|
+
// Split a primitive's args into leading positionals and a trailing :key value
|
|
1434
|
+
// option map. An unknown key throws so a wrong option name teaches the valid set.
|
|
1435
|
+
function splitArgs(
|
|
1436
|
+
args: any[], keyMap: Record<string, string>, numericKeys?: Set<string>,
|
|
1437
|
+
): { positionals: any[]; opts: Record<string, unknown> } {
|
|
1438
|
+
const positionals: any[] = [];
|
|
1439
|
+
let i = 0;
|
|
1440
|
+
while (i < args.length && !isKwSym(args[i])) { positionals.push(args[i]); i++; }
|
|
1441
|
+
const opts: Record<string, unknown> = {};
|
|
1442
|
+
while (i < args.length) {
|
|
1443
|
+
if (!isKwSym(args[i])) { i++; continue; }
|
|
1444
|
+
const key = symName(args[i])!.slice(1);
|
|
1445
|
+
const tgt = keyMap[key];
|
|
1446
|
+
if (tgt === undefined) {
|
|
1447
|
+
const valid = Object.keys(keyMap).map((k) => `:${k}`).join(" ");
|
|
1448
|
+
throw new Error(`unknown option :${key}; valid options: ${valid || "(none)"}`);
|
|
1449
|
+
}
|
|
1450
|
+
const raw = args[i + 1];
|
|
1451
|
+
opts[tgt] = numericKeys && numericKeys.has(tgt)
|
|
1452
|
+
? Number(raw)
|
|
1453
|
+
: unwrapSchemeBool(toJsStr(raw));
|
|
1454
|
+
i += 2;
|
|
1721
1455
|
}
|
|
1722
|
-
return
|
|
1456
|
+
return { positionals, opts };
|
|
1723
1457
|
}
|
|
1724
1458
|
|
|
1459
|
+
// Cache executors on globalThis (survives reload's module-cache bust): in
|
|
1460
|
+
// schemeOnly the built-ins get unregistered, but the tool objects outlive it.
|
|
1461
|
+
const EXECUTOR_CACHE: Record<string, ToolExecutor> =
|
|
1462
|
+
((globalThis as any).__ashSchemeExecutors ??= {});
|
|
1725
1463
|
function resolveExecutor(ctx: AgentContext, name: string): ToolExecutor {
|
|
1726
1464
|
const tool = ctx.agent.getTools().find((t) => t.name === name);
|
|
1727
|
-
if (
|
|
1728
|
-
|
|
1465
|
+
if (tool) return (EXECUTOR_CACHE[name] = (args) => tool.execute(args));
|
|
1466
|
+
if (EXECUTOR_CACHE[name]) return EXECUTOR_CACHE[name];
|
|
1467
|
+
throw new Error(`scheme bridge: tool '${name}' not registered`);
|
|
1729
1468
|
}
|
|
1730
1469
|
|
|
1731
1470
|
function installBindings(
|
|
@@ -1738,55 +1477,87 @@ function installBindings(
|
|
|
1738
1477
|
grep: ToolExecutor | null,
|
|
1739
1478
|
glob: ToolExecutor | null,
|
|
1740
1479
|
): void {
|
|
1480
|
+
// Optional args are :key value pairs; a trailing positional is still accepted.
|
|
1481
|
+
const READ_KEYMAP: Record<string, string> = { "offset": "offset", "limit": "limit" };
|
|
1482
|
+
const READ_NUMERIC = new Set(["offset", "limit"]);
|
|
1483
|
+
const BASH_KEYMAP: Record<string, string> = { "timeout": "timeout" };
|
|
1484
|
+
const BASH_NUMERIC = new Set(["timeout"]);
|
|
1485
|
+
const EDIT_KEYMAP: Record<string, string> = { "replace-all": "replace_all" };
|
|
1486
|
+
const GLOB_KEYMAP: Record<string, string> = { "path": "path" };
|
|
1487
|
+
const GREP_KEYMAP: Record<string, string> = {
|
|
1488
|
+
"include": "include",
|
|
1489
|
+
"case-insensitive": "case_insensitive",
|
|
1490
|
+
"context-before": "context_before",
|
|
1491
|
+
"context-after": "context_after",
|
|
1492
|
+
"limit": "head_limit",
|
|
1493
|
+
"offset": "offset",
|
|
1494
|
+
"path": "path",
|
|
1495
|
+
};
|
|
1496
|
+
const GREP_NUMERIC = new Set([
|
|
1497
|
+
"context_before", "context_after", "head_limit", "offset",
|
|
1498
|
+
]);
|
|
1499
|
+
|
|
1500
|
+
// LIPS has no keyword-argument syntax: bind each option key to a self-quoting
|
|
1501
|
+
// symbol so a bare `:offset` yields the symbol instead of an unbound error.
|
|
1502
|
+
for (const km of [READ_KEYMAP, BASH_KEYMAP, EDIT_KEYMAP, GLOB_KEYMAP, GREP_KEYMAP]) {
|
|
1503
|
+
for (const key of Object.keys(km)) {
|
|
1504
|
+
const kw = `:${key}`;
|
|
1505
|
+
env.set(kw, new LSymbol(kw));
|
|
1506
|
+
}
|
|
1507
|
+
}
|
|
1508
|
+
|
|
1741
1509
|
const runBash = async (command: string, timeoutSec?: number) => {
|
|
1742
|
-
const args: Record<string, unknown> = { command };
|
|
1510
|
+
const args: Record<string, unknown> = { command: toJsStr(command) };
|
|
1743
1511
|
if (typeof timeoutSec === "number") args.timeout = timeoutSec;
|
|
1744
1512
|
const result = await bash(args);
|
|
1745
|
-
let
|
|
1513
|
+
let output = typeof result.content === "string" ? result.content : String(result.content ?? "");
|
|
1746
1514
|
// Undo bash.ts's "(no output)" sentinel so `(eq? out "")` works.
|
|
1747
|
-
if (
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
};
|
|
1515
|
+
if (output === "(no output)") output = "";
|
|
1516
|
+
// stdout and stderr are merged upstream — the bash tool surfaces only a
|
|
1517
|
+
// combined `content` — so the record exposes one `output`, not a fabricated
|
|
1518
|
+
// split. `exit-code` is the real shell code and the only channel that
|
|
1519
|
+
// carries it: a plain bash tool call drops it before the model.
|
|
1520
|
+
return { exitCode: result.exitCode ?? (result.isError ? 1 : 0), output, error: result.isError };
|
|
1754
1521
|
};
|
|
1755
|
-
|
|
1522
|
+
const bashTimeout = (positionals: any[], opts: Record<string, unknown>): number | undefined => {
|
|
1523
|
+
const t = opts.timeout !== undefined ? opts.timeout : positionals[1];
|
|
1524
|
+
if (t === undefined || t === null) return undefined;
|
|
1525
|
+
const n = Number(t);
|
|
1526
|
+
return isNaN(n) ? undefined : n;
|
|
1527
|
+
};
|
|
1528
|
+
env.set("bash", withSig("bash", async (...rest: any[]) => {
|
|
1529
|
+
const { positionals, opts } = splitArgs(rest, BASH_KEYMAP, BASH_NUMERIC);
|
|
1530
|
+
const command = positionals[0];
|
|
1756
1531
|
try {
|
|
1757
|
-
const r = await runBash(command,
|
|
1532
|
+
const r = await runBash(command, bashTimeout(positionals, opts));
|
|
1758
1533
|
return alist([
|
|
1534
|
+
["output", r.output],
|
|
1759
1535
|
["exit-code", r.exitCode],
|
|
1760
|
-
["
|
|
1761
|
-
["stderr", r.stderr],
|
|
1762
|
-
["success", r.success],
|
|
1536
|
+
["error", r.error],
|
|
1763
1537
|
]);
|
|
1764
1538
|
} catch (e: any) {
|
|
1765
1539
|
logErr("bash", e, { command, typeofCommand: typeof command });
|
|
1766
1540
|
throw e;
|
|
1767
1541
|
}
|
|
1768
|
-
});
|
|
1769
|
-
// Shortcut:
|
|
1770
|
-
|
|
1542
|
+
}));
|
|
1543
|
+
// Shortcut: stdout as a string. Use `bash` when you need the exit code, or
|
|
1544
|
+
// `(ok? (bash "…"))` for a success predicate.
|
|
1545
|
+
env.set("sh", withSig("sh", async (...rest: any[]) => {
|
|
1546
|
+
const { positionals, opts } = splitArgs(rest, BASH_KEYMAP, BASH_NUMERIC);
|
|
1547
|
+
const command = positionals[0];
|
|
1771
1548
|
try {
|
|
1772
|
-
|
|
1773
|
-
return r.stdout;
|
|
1549
|
+
return (await runBash(command, bashTimeout(positionals, opts))).output;
|
|
1774
1550
|
} catch (e: any) {
|
|
1775
1551
|
logErr("sh", e, { command });
|
|
1776
1552
|
return "";
|
|
1777
1553
|
}
|
|
1778
|
-
});
|
|
1779
|
-
env.set("sh-ok?", async (command: string, timeoutSec?: number) => {
|
|
1780
|
-
try {
|
|
1781
|
-
const r = await runBash(command, timeoutSec);
|
|
1782
|
-
return r.success;
|
|
1783
|
-
} catch {
|
|
1784
|
-
return false;
|
|
1785
|
-
}
|
|
1786
|
-
});
|
|
1554
|
+
}));
|
|
1787
1555
|
|
|
1788
|
-
env.set("read-file", async (
|
|
1789
|
-
const
|
|
1556
|
+
env.set("read-file", withSig("read-file", async (...rest: any[]) => {
|
|
1557
|
+
const { positionals, opts } = splitArgs(rest, READ_KEYMAP, READ_NUMERIC);
|
|
1558
|
+
const args: Record<string, unknown> = { path: toJsStr(positionals[0]), bypass_cache: true };
|
|
1559
|
+
const offset = opts.offset !== undefined ? opts.offset : positionals[1];
|
|
1560
|
+
const limit = opts.limit !== undefined ? opts.limit : positionals[2];
|
|
1790
1561
|
if (offset !== undefined && offset !== null) {
|
|
1791
1562
|
const n = Number(offset);
|
|
1792
1563
|
if (!isNaN(n)) args.offset = n;
|
|
@@ -1797,19 +1568,25 @@ function installBindings(
|
|
|
1797
1568
|
}
|
|
1798
1569
|
const result = await readFile(args);
|
|
1799
1570
|
return result.isError ? false : result.content;
|
|
1800
|
-
});
|
|
1571
|
+
}));
|
|
1801
1572
|
|
|
1802
|
-
env.set("write-file", async (filePath: string, content: string) => {
|
|
1573
|
+
env.set("write-file", withSig("write-file", async (filePath: string, content: string) => {
|
|
1574
|
+
filePath = toJsStr(filePath); content = toJsStr(content);
|
|
1803
1575
|
// Re-emit tool lifecycle events so the TUI shows diffs.
|
|
1804
1576
|
const result = await withDisplay(
|
|
1805
1577
|
bus, "write_file", "write", { path: filePath, content }, filePath,
|
|
1806
1578
|
() => writeFile({ path: filePath, content }),
|
|
1807
1579
|
);
|
|
1808
1580
|
return result.isError ? result.content : true;
|
|
1809
|
-
});
|
|
1581
|
+
}));
|
|
1810
1582
|
|
|
1811
1583
|
if (editFile) {
|
|
1812
|
-
env.set("edit-file",
|
|
1584
|
+
env.set("edit-file", withSig("edit-file", async (...rest: any[]) => {
|
|
1585
|
+
const { positionals, opts } = splitArgs(rest, EDIT_KEYMAP);
|
|
1586
|
+
const filePath = toJsStr(positionals[0]);
|
|
1587
|
+
const oldStr = toJsStr(positionals[1]);
|
|
1588
|
+
const newStr = toJsStr(positionals[2]);
|
|
1589
|
+
const replaceAll = opts.replace_all !== undefined ? opts.replace_all : positionals[3];
|
|
1813
1590
|
const toolArgs: Record<string, unknown> = { path: filePath, old_text: oldStr, new_text: newStr };
|
|
1814
1591
|
if (unwrapSchemeBool(replaceAll) === true) toolArgs.replace_all = true;
|
|
1815
1592
|
const result = await withDisplay(
|
|
@@ -1817,22 +1594,10 @@ function installBindings(
|
|
|
1817
1594
|
() => editFile(toolArgs),
|
|
1818
1595
|
);
|
|
1819
1596
|
return result.isError ? result.content : true;
|
|
1820
|
-
});
|
|
1597
|
+
}));
|
|
1821
1598
|
}
|
|
1822
1599
|
|
|
1823
1600
|
if (grep) {
|
|
1824
|
-
const GREP_KEYMAP = {
|
|
1825
|
-
"include": "include",
|
|
1826
|
-
"case-insensitive": "case_insensitive",
|
|
1827
|
-
"context-before": "context_before",
|
|
1828
|
-
"context-after": "context_after",
|
|
1829
|
-
"limit": "head_limit",
|
|
1830
|
-
"offset": "offset",
|
|
1831
|
-
} as const;
|
|
1832
|
-
const GREP_NUMERIC = new Set([
|
|
1833
|
-
"context_before", "context_after", "head_limit", "offset",
|
|
1834
|
-
]);
|
|
1835
|
-
|
|
1836
1601
|
// Ripgrep uses Rust/ERE regex, but models write BRE (the default flavor
|
|
1837
1602
|
// of plain grep/sed) where \| \( \) \{ \} \+ \? are metacharacters.
|
|
1838
1603
|
// Translate BRE escapes to their ERE equivalents so the model's intent is
|
|
@@ -1845,97 +1610,97 @@ function installBindings(
|
|
|
1845
1610
|
.replace(/\\\+/g, "+").replace(/\\\?/g, "?");
|
|
1846
1611
|
};
|
|
1847
1612
|
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
}
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
}
|
|
1613
|
+
// pattern is positional; search root is the 2nd positional or :path.
|
|
1614
|
+
env.set("grep", withSig("grep", async (...rest: any[]) => {
|
|
1615
|
+
const { positionals, opts } = splitArgs(rest, GREP_KEYMAP, GREP_NUMERIC);
|
|
1616
|
+
const args: Record<string, unknown> = {
|
|
1617
|
+
pattern: normalizePattern(String(positionals[0] ?? "")), output_mode: "content", ...opts,
|
|
1618
|
+
};
|
|
1619
|
+
const posPath = toJsStr(positionals[1]);
|
|
1620
|
+
if (args.path === undefined && typeof posPath === "string") args.path = posPath;
|
|
1621
|
+
const pStr = typeof args.path === "string" ? args.path : undefined;
|
|
1858
1622
|
const result = await grep(args);
|
|
1859
1623
|
if (result.isError) return nil;
|
|
1860
1624
|
if (result.content === "No matches found.") return nil;
|
|
1861
1625
|
const rows: unknown[] = [];
|
|
1862
|
-
for (const line of stripPagination(result.content)) {
|
|
1863
|
-
const parsed = parseGrepLine(line,
|
|
1626
|
+
for (const line of stripPagination(result.content as string)) {
|
|
1627
|
+
const parsed = parseGrepLine(line, pStr);
|
|
1864
1628
|
if (parsed) rows.push(parsed);
|
|
1865
1629
|
}
|
|
1866
1630
|
return toSchemeList(rows);
|
|
1867
|
-
});
|
|
1631
|
+
}));
|
|
1868
1632
|
|
|
1869
|
-
env.set("
|
|
1870
|
-
const
|
|
1871
|
-
|
|
1872
|
-
|
|
1633
|
+
env.set("grep-files", withSig("grep-files", async (...rest: any[]) => {
|
|
1634
|
+
const { positionals, opts } = splitArgs(rest, GREP_KEYMAP, GREP_NUMERIC);
|
|
1635
|
+
const args: Record<string, unknown> = {
|
|
1636
|
+
pattern: normalizePattern(String(positionals[0] ?? "")), output_mode: "files_with_matches", ...opts,
|
|
1637
|
+
};
|
|
1638
|
+
const posPath = toJsStr(positionals[1]);
|
|
1639
|
+
if (args.path === undefined && typeof posPath === "string") args.path = posPath;
|
|
1873
1640
|
const result = await grep(args);
|
|
1874
1641
|
if (result.isError || result.content === "No matches found.") return nil;
|
|
1875
|
-
return toSchemeList(stripPagination(result.content));
|
|
1876
|
-
});
|
|
1642
|
+
return toSchemeList(stripPagination(result.content as string));
|
|
1643
|
+
}));
|
|
1877
1644
|
}
|
|
1878
1645
|
|
|
1879
1646
|
if (glob) {
|
|
1880
1647
|
// Strip leading "./" so glob paths match grep's — otherwise eq? on the
|
|
1881
1648
|
// file field fails across the two.
|
|
1882
|
-
env.set("glob", async (
|
|
1883
|
-
const
|
|
1884
|
-
|
|
1649
|
+
env.set("glob", withSig("glob", async (...rest: any[]) => {
|
|
1650
|
+
const { positionals, opts } = splitArgs(rest, GLOB_KEYMAP);
|
|
1651
|
+
const args: Record<string, unknown> = { pattern: toJsStr(positionals[0]) };
|
|
1652
|
+
const pStr = opts.path !== undefined ? String(opts.path) : toJsStr(positionals[1]);
|
|
1653
|
+
if (typeof pStr === "string") args.path = pStr;
|
|
1885
1654
|
const result = await glob(args);
|
|
1886
1655
|
if (result.isError || result.content === "No files matched.") return nil;
|
|
1887
|
-
const paths = stripPagination(result.content).map((l) =>
|
|
1656
|
+
const paths = stripPagination(result.content as string).map((l) =>
|
|
1888
1657
|
l.startsWith("./") ? l.slice(2) : l,
|
|
1889
1658
|
);
|
|
1890
1659
|
return toSchemeList(paths);
|
|
1891
|
-
});
|
|
1660
|
+
}));
|
|
1892
1661
|
}
|
|
1893
1662
|
|
|
1894
|
-
//
|
|
1663
|
+
// Accessors on a bash result — JS-side so they're never missing.
|
|
1664
|
+
env.set("output-of", (r: unknown) => lookup(r, "output"));
|
|
1895
1665
|
env.set("exit-code-of", (r: unknown) => lookup(r, "exit-code"));
|
|
1896
|
-
env.set("
|
|
1897
|
-
env.set("
|
|
1898
|
-
|
|
1666
|
+
env.set("error?", (r: unknown) => lookup(r, "error") === true);
|
|
1667
|
+
env.set("ok?", (r: unknown) => lookup(r, "error") === false);
|
|
1668
|
+
|
|
1669
|
+
// Runtime discovery: (help) lists available host primitives; (help 'grep)
|
|
1670
|
+
// shows one. Filtered to what actually got bound this session.
|
|
1671
|
+
const availableNames = new Set<string>(["bash", "sh", "read-file", "write-file"]);
|
|
1672
|
+
if (editFile) availableNames.add("edit-file");
|
|
1673
|
+
if (grep) { availableNames.add("grep"); availableNames.add("grep-files"); }
|
|
1674
|
+
if (glob) availableNames.add("glob");
|
|
1675
|
+
const availableSigs = HOST_SIGS.filter((h) => availableNames.has(h.name));
|
|
1676
|
+
env.set("help", (name?: any) => {
|
|
1677
|
+
if (name === undefined || name === null) {
|
|
1678
|
+
return availableSigs.map(sigLine).join("\n");
|
|
1679
|
+
}
|
|
1680
|
+
const key = String(name instanceof LSymbol ? symName(name) : toJsStr(name)).replace(/^:/, "");
|
|
1681
|
+
const h = availableSigs.find((s) => s.name === key);
|
|
1682
|
+
return h ? `${sigLine(h)}\n ${h.doc}` : `no host primitive named ${key}; try (help) for the list`;
|
|
1683
|
+
});
|
|
1899
1684
|
|
|
1900
1685
|
// R7RS / string helpers LIPS doesn't ship.
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
typeof s === "string" && typeof needle === "string" && s.includes(needle);
|
|
1686
|
+
const stringContains = (s: unknown, needle: unknown) => {
|
|
1687
|
+
s = toJsStr(s); needle = toJsStr(needle);
|
|
1688
|
+
return typeof s === "string" && typeof needle === "string" && s.includes(needle);
|
|
1689
|
+
};
|
|
1904
1690
|
env.set("string-contains?", stringContains);
|
|
1905
1691
|
// Racket spells it without the `?`. Bind both so the model isn't punished
|
|
1906
1692
|
// for guessing dialect.
|
|
1907
1693
|
env.set("string-contains", stringContains);
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
// LIPS' native `string` returns only its first argument; override to actually
|
|
1911
|
-
// concatenate (chars are 1-char strings here, so that's the right semantics).
|
|
1912
|
-
env.set("string", (...parts: unknown[]) =>
|
|
1913
|
-
parts.map((p) => (p === undefined || p === null ? "" : String(p))).join(""));
|
|
1914
|
-
env.set("number->string", (n: unknown) => String(n));
|
|
1915
|
-
env.set("string->number", (s: unknown) => {
|
|
1916
|
-
if (typeof s !== "string") return false;
|
|
1917
|
-
const n = Number(s);
|
|
1918
|
-
return Number.isNaN(n) ? false : n;
|
|
1919
|
-
});
|
|
1920
|
-
env.set("symbol->string", (sym: any) => (sym && sym.name) ? sym.name : String(sym));
|
|
1921
|
-
// LIPS doesn't ship max/min — useful enough that not having them breaks
|
|
1922
|
-
// common idioms like `(max 1 (- n 5))` for line-bound clamping.
|
|
1923
|
-
env.set("max", (...args: any[]) => args.reduce((a, b) => (Number(a) >= Number(b) ? a : b)));
|
|
1924
|
-
env.set("min", (...args: any[]) => args.reduce((a, b) => (Number(a) <= Number(b) ? a : b)));
|
|
1925
|
-
installStdShims(env);
|
|
1926
|
-
// Global string substitution — LIPS' built-in `replace` with a string
|
|
1927
|
-
// pattern only replaces the first match (it calls JS String.replace).
|
|
1928
|
-
// This binding replaces every occurrence, sed-style.
|
|
1694
|
+
// Global string substitution: `replace` with a string pattern replaces only
|
|
1695
|
+
// the first match; this binding replaces every occurrence, sed-style.
|
|
1929
1696
|
env.set("string-replace", (oldStr: unknown, newStr: unknown, s: unknown) => {
|
|
1697
|
+
s = toJsStr(s);
|
|
1930
1698
|
if (typeof s !== "string") return s;
|
|
1931
1699
|
return s.split(String(oldStr ?? "")).join(String(newStr ?? ""));
|
|
1932
1700
|
});
|
|
1933
1701
|
|
|
1934
|
-
// LIPS' tokenizer doesn't recognize R7RS `#t`/`#f` — they parse as
|
|
1935
|
-
// unbound symbols. Bind them so the natural R7RS reflex works.
|
|
1936
|
-
env.set("#t", true);
|
|
1937
|
-
env.set("#f", false);
|
|
1938
1702
|
env.set("lines", (s: unknown) => {
|
|
1703
|
+
s = toJsStr(s);
|
|
1939
1704
|
if (typeof s !== "string" || s.length === 0) return nil;
|
|
1940
1705
|
const parts = s.split("\n");
|
|
1941
1706
|
if (parts.length > 0 && parts[parts.length - 1] === "") parts.pop();
|
|
@@ -1946,102 +1711,66 @@ function installBindings(
|
|
|
1946
1711
|
}
|
|
1947
1712
|
|
|
1948
1713
|
// ── tool registration ─────────────────────────────────────────────
|
|
1714
|
+
// Generated from HOST_SIGS so the catalog the model reads matches what
|
|
1715
|
+
// `(help …)` reports at runtime.
|
|
1716
|
+
const HOST_BINDINGS_BLOCK = HOST_SIGS.map(
|
|
1717
|
+
(h) => ` ${h.sig.padEnd(44)} → ${h.ret}\n ${h.doc}`,
|
|
1718
|
+
).join("\n");
|
|
1719
|
+
|
|
1949
1720
|
const DESCRIPTION = [
|
|
1950
1721
|
"Evaluate a Scheme expression (R7RS-compatible).",
|
|
1951
1722
|
"",
|
|
1952
|
-
"A Scheme runtime with host bindings to the shell, filesystem, search
|
|
1953
|
-
"
|
|
1954
|
-
"
|
|
1955
|
-
"
|
|
1723
|
+
"A Scheme runtime with host bindings to the shell, filesystem, and search.",
|
|
1724
|
+
"The environment persists across calls within a session — `define`s in one",
|
|
1725
|
+
"submission are visible in the next.",
|
|
1726
|
+
"",
|
|
1727
|
+
"You already know the language: assume the full R7RS / SRFI-1 / Racket stdlib",
|
|
1728
|
+
"is present (map filter fold assoc, string-* and list ops, cond/when/unless,",
|
|
1729
|
+
"char and hash-table ops, …). The only novel surface is the host bindings.",
|
|
1956
1730
|
"",
|
|
1957
|
-
"
|
|
1958
|
-
" -
|
|
1959
|
-
"
|
|
1960
|
-
"
|
|
1961
|
-
"
|
|
1962
|
-
"
|
|
1963
|
-
"
|
|
1964
|
-
"
|
|
1965
|
-
"
|
|
1966
|
-
"
|
|
1967
|
-
" - The env persists across submissions, so binding intermediate",
|
|
1968
|
-
" results once (e.g. `(define files (glob …))`) avoids recomputing",
|
|
1969
|
-
" them in later calls.",
|
|
1970
|
-
" - `(bash …)` calls a real shell — natural for shell-shaped work",
|
|
1971
|
-
" (tests, builds, git, system commands). Three variants of the shell",
|
|
1972
|
-
" binding return different shapes:",
|
|
1973
|
-
" `(sh \"cmd\")` → just the output as a string. Fits \"run this,",
|
|
1974
|
-
" show me the result\" without unwrapping.",
|
|
1975
|
-
" `(sh-ok? \"cmd\")` → just a boolean. Fits `(if (sh-ok? \"…\") …)`",
|
|
1976
|
-
" branches and existence checks.",
|
|
1977
|
-
" `(bash \"cmd\")` → full alist when you need stdout *and* exit",
|
|
1978
|
-
" code *and* stderr separately (e.g. capture",
|
|
1979
|
-
" stderr while letting stdout flow on).",
|
|
1980
|
-
" For file content work, the host bindings (`grep`, `read-file`,",
|
|
1981
|
-
" `glob`) avoid shell-quoting entirely and return structured data.",
|
|
1982
|
-
" - `scheme-define` saves a procedure to disk so it auto-loads next",
|
|
1983
|
-
" session — useful when you've worked out something reusable.",
|
|
1731
|
+
"Calling convention:",
|
|
1732
|
+
" - Required arguments are positional: (read-file \"x\"), (grep \"pat\").",
|
|
1733
|
+
" - Optional arguments are :key value pairs, and the same key reads the same",
|
|
1734
|
+
" on every binding — :offset/:limit on read-file and grep, :timeout on bash:",
|
|
1735
|
+
" (read-file \"x\" :offset 40 :limit 20)",
|
|
1736
|
+
" (grep \"TODO\" \"src/\" :include \"*.ts\" :context-after 2)",
|
|
1737
|
+
" (A trailing positional is still accepted for each optional, but :key is",
|
|
1738
|
+
" the canonical form and never depends on argument order.)",
|
|
1739
|
+
" - Each binding returns the natural Scheme value for its job (a string, a",
|
|
1740
|
+
" list, a boolean); bash returns a record because you usually want the code.",
|
|
1984
1741
|
"",
|
|
1985
1742
|
"Host bindings:",
|
|
1986
|
-
|
|
1987
|
-
" cmd is run via `bash -c`. Pipes/redirects/$VARS/&&/||/here-docs work",
|
|
1988
|
-
" inside the string; there's no piping between separate bash calls.",
|
|
1989
|
-
" (sh cmd [timeout-sec]) → stdout string (stderr text on failure)",
|
|
1990
|
-
" (sh-ok? cmd [timeout-sec]) → #t if exit code 0, else #f",
|
|
1991
|
-
" (read-file path) → string, or #f on error",
|
|
1992
|
-
" (read-file path offset) → from line offset (1-indexed) to end",
|
|
1993
|
-
" (read-file path offset limit) → offset + N lines",
|
|
1994
|
-
" (write-file path content) → #t on success, error string on failure",
|
|
1995
|
-
" (edit-file path old new) → #t on success, error string on failure",
|
|
1996
|
-
" (edit-file path old new #t) → replace every occurrence (not just one)",
|
|
1997
|
-
" (grep pattern [path] [limit|opts]) → list of ((file . S) (line . N) (text . S))",
|
|
1998
|
-
" (grep-files pattern [path] [opts]) → list of file paths",
|
|
1999
|
-
" Patterns are ripgrep regex (Rust). Both POSIX BRE escapes (`\\|`,",
|
|
2000
|
-
" `\\(`, `\\)`, `\\{`, `\\}`, `\\+`, `\\?`) and bare ERE-style metacharacters",
|
|
2001
|
-
" (`|`, `(`, `)`, `{`, `}`, `+`, `?`) work — the bridge translates BRE to",
|
|
2002
|
-
" ERE before invoking ripgrep. `.` is any char, `\\b` is a word boundary.",
|
|
2003
|
-
" opts: ((include . \"*.ts\") ; filename glob filter",
|
|
2004
|
-
" (case-insensitive . #t)",
|
|
2005
|
-
" (context-before . N) (context-after . N) ; grep only",
|
|
2006
|
-
" (limit . N) (offset . N))",
|
|
2007
|
-
" The opts alist is auto-quoted, so `((k . v) …)` and `'((k . v) …)` both work.",
|
|
2008
|
-
" (glob pattern [base-dir]) → list of file paths (mtime-sorted)",
|
|
2009
|
-
"Accessors on bash result: (stdout-of r) (stderr-of r) (exit-code-of r) (success? r)",
|
|
2010
|
-
"Strings: (string-length s) (string-contains? s n) (string-append . parts)",
|
|
2011
|
-
" (string-replace old new s) (number->string n) (string->number s)",
|
|
2012
|
-
" (lines s) (split sep s) (replace pat repl s) (max …) (min …)",
|
|
1743
|
+
HOST_BINDINGS_BLOCK,
|
|
2013
1744
|
"",
|
|
2014
|
-
"
|
|
2015
|
-
"
|
|
2016
|
-
"
|
|
1745
|
+
" Discover at runtime: (help) lists these, (help 'grep) shows one.",
|
|
1746
|
+
" bash runs via `bash -c` — pipes/redirects/$VARS/&&/here-docs work inside the",
|
|
1747
|
+
" string; stdout+stderr are merged into `output`, and `exit-code` is the real",
|
|
1748
|
+
" shell code. grep/grep-files patterns are ripgrep regex (Rust); POSIX BRE",
|
|
1749
|
+
" escapes (\\|, \\(, \\), \\{, \\}, \\+, \\?) and bare ERE metacharacters both work.",
|
|
2017
1750
|
"",
|
|
2018
|
-
"
|
|
2019
|
-
"
|
|
2020
|
-
"
|
|
2021
|
-
"
|
|
2022
|
-
"
|
|
2023
|
-
"
|
|
2024
|
-
" the equivalent string; `char->integer`/`integer->char`/`char?`/`char=?`/",
|
|
2025
|
-
" `char-whitespace?` etc. operate on them. A bare newline is just \"\\n\".",
|
|
2026
|
-
" - SRFI-1: `member`, `assq`/`assv`/`assoc`, `delete-duplicates`, `first`",
|
|
2027
|
-
" through `fifth`, `last`, `take`, `drop`, `iota`, `any`, `every`, `count`,",
|
|
2028
|
-
" `find`, `filter-map`, `append-map`, `concatenate`, `partition`, `remove`,",
|
|
2029
|
-
" `delete`, `zip`, `take-while`, `drop-while`, `fold-right` are all bound.",
|
|
2030
|
-
" - R7RS extras: `string-upcase`/`-downcase`, `string-split`/`-join`,",
|
|
2031
|
-
" `zero?`/`positive?`/`negative?`/`odd?`/`even?`, `modulo`, `quotient`,",
|
|
2032
|
-
" `remainder`, `expt`, `ceiling`, `error`, `newline`, `displayln`.",
|
|
1751
|
+
"Composition is the point: chain read-only bindings in one submission so",
|
|
1752
|
+
"intermediate results stay in the Scheme heap instead of the conversation.",
|
|
1753
|
+
" (map (lambda (m) (read-file (cdr (assoc 'file m)) :offset (cdr (assoc 'line m)) :limit 3))",
|
|
1754
|
+
" (grep \"TODO\" \"src/\"))",
|
|
1755
|
+
"Side-effecting calls (write-file, edit-file, mutating bash) are clearer one",
|
|
1756
|
+
"at a time so you can react to each result.",
|
|
2033
1757
|
"",
|
|
1758
|
+
"scheme-define saves a procedure to ~/.agent-sh/scheme-define/{name}.scm so it",
|
|
1759
|
+
"auto-loads next session:",
|
|
2034
1760
|
" (scheme-define name (args …) \"docstring\" body …)",
|
|
2035
|
-
"
|
|
2036
|
-
"
|
|
1761
|
+
"",
|
|
1762
|
+
"Dialect notes:",
|
|
1763
|
+
" - R7RS truthy semantics: only `#f` is false. `(if 0 …)`, `(if '() …)`,",
|
|
1764
|
+
" `(if \"\" …)` all take the then-branch.",
|
|
1765
|
+
" - Characters are a real type: `#\\A` `#\\newline` `#\\space` `#\\tab` `#\\xNN`.",
|
|
1766
|
+
" - String escapes are JSON-style (`\\\\` `\\\"` `\\n` `\\r` `\\t` `\\uXXXX`);",
|
|
1767
|
+
" for a literal backslash write `\\\\`.",
|
|
2037
1768
|
"",
|
|
2038
1769
|
"Default timeout 15s; pass timeout_ms to override (max 60s).",
|
|
2039
1770
|
].join("\n");
|
|
2040
1771
|
|
|
2041
|
-
// Scheme prelude
|
|
2042
|
-
//
|
|
2043
|
-
// macro form (defmacro-style); used here because `define-syntax` isn't
|
|
2044
|
-
// available either.
|
|
1772
|
+
// Scheme prelude (Lisp `define-macro`s), run after std is bootstrapped.
|
|
1773
|
+
// cond/when/unless/newline/assq for convenience.
|
|
2045
1774
|
const PRELUDE = `
|
|
2046
1775
|
(define-macro (cond . clauses)
|
|
2047
1776
|
(if (null? clauses)
|
|
@@ -2062,37 +1791,10 @@ const PRELUDE = `
|
|
|
2062
1791
|
;; R7RS shims for things models commonly reach for that LIPS doesn't ship.
|
|
2063
1792
|
(define (newline) (display "\n"))
|
|
2064
1793
|
(define assq assoc)
|
|
2065
|
-
|
|
2066
|
-
;; grep / grep-files: auto-quote alist-literal opts so callers can write
|
|
2067
|
-
;; either ((k . v) ...) or '((k . v) ...). Without this, the bare form is
|
|
2068
|
-
;; read as a function call on (k . v) and errors with an unbound-variable
|
|
2069
|
-
;; message that doesn't point at the cause.
|
|
2070
|
-
(define (%alist-literal? x)
|
|
2071
|
-
(and (pair? x) (pair? (car x)) (symbol? (car (car x)))))
|
|
2072
|
-
|
|
2073
|
-
(define-macro (grep . args)
|
|
2074
|
-
(if (and (>= (length args) 3) (%alist-literal? (car (cdr (cdr args)))))
|
|
2075
|
-
(cons '%grep
|
|
2076
|
-
(cons (car args)
|
|
2077
|
-
(cons (car (cdr args))
|
|
2078
|
-
(cons (list 'quote (car (cdr (cdr args))))
|
|
2079
|
-
(cdr (cdr (cdr args)))))))
|
|
2080
|
-
(cons '%grep args)))
|
|
2081
|
-
|
|
2082
|
-
(define-macro (grep-files . args)
|
|
2083
|
-
(if (and (>= (length args) 3) (%alist-literal? (car (cdr (cdr args)))))
|
|
2084
|
-
(cons '%grep-files
|
|
2085
|
-
(cons (car args)
|
|
2086
|
-
(cons (car (cdr args))
|
|
2087
|
-
(cons (list 'quote (car (cdr (cdr args))))
|
|
2088
|
-
(cdr (cdr (cdr args)))))))
|
|
2089
|
-
(cons '%grep-files args)))
|
|
2090
1794
|
`;
|
|
2091
1795
|
|
|
2092
1796
|
export default function activate(ctx: AgentContext): void {
|
|
2093
1797
|
const env = (lips as any).env.inherit("scheme-ext");
|
|
2094
|
-
installFixedDefine(env);
|
|
2095
|
-
installLenientIf(env);
|
|
2096
1798
|
const defineRegistry: DefineRegistry = new Map();
|
|
2097
1799
|
const defineLoading = { active: false };
|
|
2098
1800
|
// Forward decl: assigned after baseInstruction is computed below.
|
|
@@ -2112,21 +1814,32 @@ export default function activate(ctx: AgentContext): void {
|
|
|
2112
1814
|
try { glob = resolveExecutor(ctx, "glob"); } catch { /* optional */ }
|
|
2113
1815
|
installBindings(env, ctx.bus, bash, readFile, writeFile, editFile, grep, glob);
|
|
2114
1816
|
|
|
2115
|
-
//
|
|
2116
|
-
//
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
}
|
|
2129
|
-
|
|
1817
|
+
// Bootstrap LIPS' compiled std library into the global env, then install the
|
|
1818
|
+
// gap-filling shims (skipped where std already defines a name, so native
|
|
1819
|
+
// R7RS wins), the prelude macros, and any persisted scheme-defines. Bootstrap
|
|
1820
|
+
// must precede installStdShims so native bindings take priority.
|
|
1821
|
+
void (async () => {
|
|
1822
|
+
try {
|
|
1823
|
+
const stdXcb = path.join(
|
|
1824
|
+
path.dirname(createRequire(import.meta.url).resolve("@jcubic/lips")),
|
|
1825
|
+
"std.xcb",
|
|
1826
|
+
);
|
|
1827
|
+
await bootstrap(stdXcb);
|
|
1828
|
+
} catch (e) {
|
|
1829
|
+
logErr("bootstrap", e);
|
|
1830
|
+
}
|
|
1831
|
+
installStdShims(env);
|
|
1832
|
+
await (lips as any).exec(PRELUDE, { env });
|
|
1833
|
+
await loadPersistedDefines(env, defineRegistry, defineLoading);
|
|
1834
|
+
const audit = auditShimCoverage(env);
|
|
1835
|
+
if (audit.missing.length > 0) {
|
|
1836
|
+
logErr("shim-audit", new Error("missing canonical names"), {
|
|
1837
|
+
defined: audit.defined,
|
|
1838
|
+
total: audit.defined + audit.missing.length,
|
|
1839
|
+
missing: audit.missing,
|
|
1840
|
+
});
|
|
1841
|
+
}
|
|
1842
|
+
})().catch((e: any) => logErr("init", e));
|
|
2130
1843
|
|
|
2131
1844
|
if (schemeOnly) {
|
|
2132
1845
|
for (const name of HIDDEN_IN_SCHEME_ONLY) {
|
|
@@ -2159,11 +1872,8 @@ export default function activate(ctx: AgentContext): void {
|
|
|
2159
1872
|
},
|
|
2160
1873
|
getDisplayInfo: () => ({ kind: "execute", icon: "λ", sourceLanguage: "scheme" }),
|
|
2161
1874
|
formatResult: (args, result) => {
|
|
2162
|
-
//
|
|
2163
|
-
//
|
|
2164
|
-
// reveals what ran rather than what came back. scheme-render.ts (if
|
|
2165
|
-
// loaded) honors this; ashi's default renderer currently ignores
|
|
2166
|
-
// body.kind === "lines", which is fine — summary still carries the gist.
|
|
1875
|
+
// TUI body shows the SOURCE, not the result (Ctrl+O reveals what ran);
|
|
1876
|
+
// the LLM still gets full content via tool_result.
|
|
2167
1877
|
const sourceLines = String(args.source ?? "").split("\n");
|
|
2168
1878
|
if (!result.isError) {
|
|
2169
1879
|
return {
|
|
@@ -2194,44 +1904,24 @@ export default function activate(ctx: AgentContext): void {
|
|
|
2194
1904
|
},
|
|
2195
1905
|
});
|
|
2196
1906
|
|
|
2197
|
-
//
|
|
2198
|
-
//
|
|
2199
|
-
//
|
|
2200
|
-
// add behavioral framing here — specifically the context-preservation
|
|
2201
|
-
// nudge that would be lost in the long tool description.
|
|
1907
|
+
// Behavioral framing in the system prompt (the API + examples live in the
|
|
1908
|
+
// tool description). Keep it short and non-duplicative: just when to reach
|
|
1909
|
+
// for scheme_eval over a direct tool call.
|
|
2202
1910
|
const baseInstruction = schemeOnly
|
|
2203
1911
|
? [
|
|
2204
1912
|
"# Scheme runtime",
|
|
2205
|
-
"scheme_eval is your only tool; see its description for the API.",
|
|
2206
|
-
"",
|
|
2207
|
-
"
|
|
2208
|
-
"
|
|
2209
|
-
"multi-step operations into a single scheme_eval call so intermediate",
|
|
2210
|
-
"results stay in the Scheme heap instead of the conversation. Example:",
|
|
2211
|
-
" (let ((files (glob \"src/**/*.ts\"))",
|
|
2212
|
-
" (matches (grep \"TODO\" \"src/\")))",
|
|
2213
|
-
" (filter (lambda (f) (member f (map (lambda (m) (cdr (assoc 'file m))) matches)))",
|
|
2214
|
-
" files))",
|
|
2215
|
-
"This does glob + grep + filter in one round-trip. Use `define` to",
|
|
2216
|
-
"cache results across calls: `(define files (glob …))` once, reuse later.",
|
|
1913
|
+
"scheme_eval is your only tool; see its description for the API. Each tool",
|
|
1914
|
+
"round-trip consumes context permanently, so compose multi-step work into a",
|
|
1915
|
+
"single call — intermediate results stay in the Scheme heap, not the",
|
|
1916
|
+
"conversation. `define` caches across calls.",
|
|
2217
1917
|
].join("\n")
|
|
2218
1918
|
: [
|
|
2219
1919
|
"# Scheme runtime",
|
|
2220
|
-
"scheme_eval evaluates Scheme with host bindings
|
|
2221
|
-
"
|
|
2222
|
-
"",
|
|
2223
|
-
"
|
|
2224
|
-
"
|
|
2225
|
-
"single operations. scheme_eval becomes valuable when you'd chain 2+ read-only",
|
|
2226
|
-
"tool calls that don't need inspection between steps — composing them inside",
|
|
2227
|
-
"Scheme keeps intermediate results in the Scheme heap instead of the",
|
|
2228
|
-
"conversation, saving context. Example:",
|
|
2229
|
-
" (let ((matches (grep \"pattern\" \"src/\")))",
|
|
2230
|
-
" (map (lambda (m) (list (cdr (assoc 'file m))",
|
|
2231
|
-
" (read-file (cdr (assoc 'file m)) (cdr (assoc 'line m)) 3)))",
|
|
2232
|
-
" (take matches 5)))",
|
|
2233
|
-
"does grep + read-file × 5 in one round-trip. `define` caches across",
|
|
2234
|
-
"calls: `(define files (glob …))` once, reuse in later submissions.",
|
|
1920
|
+
"scheme_eval evaluates Scheme with host bindings (bash, read-file, grep, glob,",
|
|
1921
|
+
"…); see its description for the API. Direct tools are the right default for",
|
|
1922
|
+
"single operations; reach for scheme_eval to chain 2+ read-only calls that",
|
|
1923
|
+
"don't need inspection between steps — composing them keeps intermediate",
|
|
1924
|
+
"results in the Scheme heap instead of the conversation.",
|
|
2235
1925
|
].join("\n");
|
|
2236
1926
|
// Re-register when the scheme-define registry changes so the index stays
|
|
2237
1927
|
// current within a session.
|