@inhuman.tools/scheme-program 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.md +7 -0
- package/dist/emission.d.ts +10 -0
- package/dist/emission.d.ts.map +1 -0
- package/dist/emission.js +74 -0
- package/dist/emission.js.map +1 -0
- package/dist/eval.d.ts +7 -0
- package/dist/eval.d.ts.map +1 -0
- package/dist/eval.js +6 -0
- package/dist/eval.js.map +1 -0
- package/dist/evaluate.d.ts +90 -0
- package/dist/evaluate.d.ts.map +1 -0
- package/dist/evaluate.js +316 -0
- package/dist/evaluate.js.map +1 -0
- package/dist/failure.d.ts +10 -0
- package/dist/failure.d.ts.map +1 -0
- package/dist/failure.js +30 -0
- package/dist/failure.js.map +1 -0
- package/dist/file-path.d.ts +9 -0
- package/dist/file-path.d.ts.map +1 -0
- package/dist/file-path.js +25 -0
- package/dist/file-path.js.map +1 -0
- package/dist/host.d.ts +55 -0
- package/dist/host.d.ts.map +1 -0
- package/dist/host.js +2 -0
- package/dist/host.js.map +1 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -0
- package/dist/notes.d.ts +12 -0
- package/dist/notes.d.ts.map +1 -0
- package/dist/notes.js +12 -0
- package/dist/notes.js.map +1 -0
- package/dist/open.d.ts +23 -0
- package/dist/open.d.ts.map +1 -0
- package/dist/open.js +53 -0
- package/dist/open.js.map +1 -0
- package/dist/replay-eval.d.ts +5 -0
- package/dist/replay-eval.d.ts.map +1 -0
- package/dist/replay-eval.js +72 -0
- package/dist/replay-eval.js.map +1 -0
- package/dist/run.d.ts +4 -0
- package/dist/run.d.ts.map +1 -0
- package/dist/run.js +81 -0
- package/dist/run.js.map +1 -0
- package/dist/runtime.d.ts +8 -0
- package/dist/runtime.d.ts.map +1 -0
- package/dist/runtime.js +77 -0
- package/dist/runtime.js.map +1 -0
- package/dist/source-fs.d.ts +4 -0
- package/dist/source-fs.d.ts.map +1 -0
- package/dist/source-fs.js +36 -0
- package/dist/source-fs.js.map +1 -0
- package/dist/source.d.ts +22 -0
- package/dist/source.d.ts.map +1 -0
- package/dist/source.js +76 -0
- package/dist/source.js.map +1 -0
- package/package.json +67 -3
- package/src/emission.ts +82 -0
- package/src/eval.ts +20 -0
- package/src/evaluate.ts +398 -0
- package/src/failure.ts +35 -0
- package/src/file-path.ts +27 -0
- package/src/host.ts +57 -0
- package/src/index.ts +8 -0
- package/src/notes.ts +15 -0
- package/src/open.ts +73 -0
- package/src/replay-eval.ts +84 -0
- package/src/run.ts +103 -0
- package/src/runtime.ts +99 -0
- package/src/source-fs.ts +35 -0
- package/src/source.ts +78 -0
- package/README.md +0 -3
package/src/file-path.ts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Path stamp on a `read_file` string. Identity-keyed: substring/append mint a
|
|
3
|
+
* new `AString` and drop it. `withProvenance` clones the same box — copy the
|
|
4
|
+
* stamp onto the clone. Not provenance.
|
|
5
|
+
*/
|
|
6
|
+
import { AString } from "@here.build/arrival/reflect-internals";
|
|
7
|
+
|
|
8
|
+
const paths = new WeakMap<AString, string>();
|
|
9
|
+
|
|
10
|
+
const originalWithProvenance = AString.prototype.withProvenance;
|
|
11
|
+
Object.defineProperty(AString.prototype, "withProvenance", {
|
|
12
|
+
value(this: AString, p: ReadonlySet<number>) {
|
|
13
|
+
const next = originalWithProvenance.call(this, p);
|
|
14
|
+
const path = paths.get(this);
|
|
15
|
+
if (path !== undefined && next instanceof AString) paths.set(next, path);
|
|
16
|
+
return next;
|
|
17
|
+
},
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
export function stampFilePath(value: AString, path: string): AString {
|
|
21
|
+
paths.set(value, path);
|
|
22
|
+
return value;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function filePathOf(value: unknown): string | undefined {
|
|
26
|
+
return value instanceof AString ? paths.get(value) : undefined;
|
|
27
|
+
}
|
package/src/host.ts
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import type { EnvCapability } from "@here.build/arrival";
|
|
2
|
+
import type { EvalTrace } from "@here.build/arrival/provenance";
|
|
3
|
+
import type { AgentCallNotification, ApprovableJob, ProgrammaticJob } from "@inhuman.tools/harness-model";
|
|
4
|
+
import type { BoundTool } from "@inhuman.tools/substrate/bound-tool";
|
|
5
|
+
import type { DoorSession } from "@inhuman.tools/substrate/doors";
|
|
6
|
+
|
|
7
|
+
export type PreparedProgram = {
|
|
8
|
+
readonly capabilities: readonly EnvCapability[];
|
|
9
|
+
readonly config: object;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* What the interpreter needs from an engine. Catalog, secrets, and tool bodies
|
|
14
|
+
* stay in the capabilities `prepare` returns. This package does not look them up.
|
|
15
|
+
*/
|
|
16
|
+
export type ProgramHost = {
|
|
17
|
+
/** Stable across wrappers. The runtime map keys on this, not on the wrapper. */
|
|
18
|
+
readonly identity: object;
|
|
19
|
+
readonly signal?: AbortSignal;
|
|
20
|
+
readonly doors: DoorSession;
|
|
21
|
+
readonly root: string;
|
|
22
|
+
readonly abort?: (leaf: ApprovableJob, reason?: unknown) => void;
|
|
23
|
+
readonly notify?: (note: AgentCallNotification) => void;
|
|
24
|
+
readonly evalProgram?: boolean;
|
|
25
|
+
readonly moduleRoot?: string;
|
|
26
|
+
/**
|
|
27
|
+
* Wraps one run of the program (the whole one-shot, or one period tick) so the host owns its trace.
|
|
28
|
+
* The host mints the trace, arms what it reads, and reads it as the run goes; when `run` settles, the run is over.
|
|
29
|
+
*/
|
|
30
|
+
readonly observe?: <T>(run: (trace: EvalTrace) => Promise<T>) => Promise<T>;
|
|
31
|
+
/** Unbound-variable door. Called per evaluation; `prepare` snapshots tools onto config separately. */
|
|
32
|
+
readonly tools: () => ReadonlyMap<string, BoundTool>;
|
|
33
|
+
/** Engine vocabulary. The interpreter appends display. The watchdog wraps this to append meta-reactivity. */
|
|
34
|
+
readonly prepare: (opts: {
|
|
35
|
+
readonly moduleRoot: string | undefined;
|
|
36
|
+
readonly job: ProgrammaticJob;
|
|
37
|
+
}) => PreparedProgram;
|
|
38
|
+
/** Read recorded children. Do not mint, do not append notes, do not seal the job. */
|
|
39
|
+
readonly replayOnly?: boolean;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export type ProgramRuntime = {
|
|
43
|
+
arm(opts: {
|
|
44
|
+
readonly moduleRoot: string | undefined;
|
|
45
|
+
/** Accepted so existing arm calls typecheck. The watchdog roots meta-reactivity by wrapping prepare. */
|
|
46
|
+
readonly watching?: boolean;
|
|
47
|
+
}): void;
|
|
48
|
+
beginTick(tick: {
|
|
49
|
+
readonly signal: AbortSignal;
|
|
50
|
+
readonly origin: number;
|
|
51
|
+
readonly maxInternalCalls: number | undefined;
|
|
52
|
+
}): void;
|
|
53
|
+
clearFailure(): void;
|
|
54
|
+
failureCode(): string | undefined;
|
|
55
|
+
readonly capabilities: readonly EnvCapability[];
|
|
56
|
+
readonly config: object;
|
|
57
|
+
};
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export { appendNotify, failureCodeOf, SchemeReplToolFailure } from "./failure.js";
|
|
2
|
+
export { filePathOf, stampFilePath } from "./file-path.js";
|
|
3
|
+
export { type ProgramHost, type ProgramRuntime } from "./host.js";
|
|
4
|
+
export { SCHEME_REPL_DISPLAY_NOTE } from "./notes.js";
|
|
5
|
+
export { replayEvaluation } from "./replay-eval.js";
|
|
6
|
+
export { runProgram } from "./run.js";
|
|
7
|
+
export { detachedRuntime, programRuntimeOf } from "./runtime.js";
|
|
8
|
+
export { recordedModuleLoader } from "./source.js";
|
package/src/notes.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { TopLevelSplice } from "@inhuman.tools/substrate/unwrap-begin";
|
|
2
|
+
|
|
3
|
+
/** Own tool text part per spliced wrapper spelling. Not a statement value. */
|
|
4
|
+
export const SCHEME_REPL_UNWRAP_NOTES = {
|
|
5
|
+
begin: ";; unwrapped (begin …): each statement is returned as its own result. Write consecutive top-level forms.",
|
|
6
|
+
list: ";; unwrapped (list …): each statement is returned as its own result. Write consecutive top-level forms.",
|
|
7
|
+
vector: ";; unwrapped []: each statement is returned as its own result. Write consecutive top-level forms.",
|
|
8
|
+
} as const satisfies Record<TopLevelSplice, string>;
|
|
9
|
+
|
|
10
|
+
/** Attached to a top-level statement that was a `(display X)` — the rewrite unwrapped it to X and
|
|
11
|
+
* X's value IS the answer block, so this teaches that the display spelling was unnecessary:
|
|
12
|
+
* scheme_repl already returns each top-level statement as its own tool response. scheme_watch is
|
|
13
|
+
* repl-only and does not surface per-statement responses, so it does not attach this note. */
|
|
14
|
+
export const SCHEME_REPL_DISPLAY_NOTE =
|
|
15
|
+
";; you do not need to use (display ...) — each top-level statement of scheme_repl is written as a tool response";
|
package/src/open.ts
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { parse, type SchemeValue } from "@here.build/arrival";
|
|
2
|
+
import { ProgrammaticJob, periodMsOf, SCHEME_REPL_NAMING, SCHEME_REPL_TOOL_NAME } from "@inhuman.tools/harness-model";
|
|
3
|
+
import { emptyExprDoor, isUnterminatedDictLiteral, unterminatedDictDoor } from "@inhuman.tools/substrate/doors";
|
|
4
|
+
import { type TopLevelSplice, unwrapTopLevel } from "@inhuman.tools/substrate/unwrap-begin";
|
|
5
|
+
|
|
6
|
+
import { errorText, fail, sealChunk } from "./evaluate.js";
|
|
7
|
+
import type { ProgramHost } from "./host.js";
|
|
8
|
+
import { moduleRootOf, programSourceOf, type ProgramSource } from "./source.js";
|
|
9
|
+
|
|
10
|
+
export type OpenedProgram = {
|
|
11
|
+
readonly forms: readonly SchemeValue[];
|
|
12
|
+
readonly kinds: readonly TopLevelSplice[];
|
|
13
|
+
readonly timeoutMs: number | undefined;
|
|
14
|
+
readonly maxInternalCalls: number | undefined;
|
|
15
|
+
readonly periodMs: number | undefined;
|
|
16
|
+
readonly source: ProgramSource;
|
|
17
|
+
/** Jail root for a one-shot, including `sourcePath`'s directory. */
|
|
18
|
+
readonly moduleRoot: string | undefined;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
function cappedCalls(value: number | undefined): number | undefined {
|
|
22
|
+
if (value == null) return undefined;
|
|
23
|
+
if (!Number.isFinite(value)) return 1;
|
|
24
|
+
return Math.max(1, Math.floor(value));
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Schema already accepted. Seals empty expr, a source without a path, and a program that does not parse. */
|
|
28
|
+
export async function openProgram(
|
|
29
|
+
chunk: ProgrammaticJob,
|
|
30
|
+
host: ProgramHost,
|
|
31
|
+
data: {
|
|
32
|
+
readonly expr: string;
|
|
33
|
+
readonly timeoutMs?: number;
|
|
34
|
+
readonly maxInternalCalls?: number;
|
|
35
|
+
readonly period?: string;
|
|
36
|
+
},
|
|
37
|
+
seal = true,
|
|
38
|
+
): Promise<OpenedProgram | undefined> {
|
|
39
|
+
const refuse = (result: Parameters<typeof sealChunk>[1]): undefined => {
|
|
40
|
+
if (seal) sealChunk(chunk, result);
|
|
41
|
+
return undefined;
|
|
42
|
+
};
|
|
43
|
+
if (data.expr.trim() === "") {
|
|
44
|
+
return refuse(fail(host.doors.render(emptyExprDoor(SCHEME_REPL_NAMING), SCHEME_REPL_TOOL_NAME)));
|
|
45
|
+
}
|
|
46
|
+
const source = programSourceOf(chunk.args, host.evalProgram === true);
|
|
47
|
+
if (source.source != null && source.sourcePath == null) {
|
|
48
|
+
return refuse(fail("Error: source requires sourcePath"));
|
|
49
|
+
}
|
|
50
|
+
let forms: readonly SchemeValue[];
|
|
51
|
+
let kinds: readonly TopLevelSplice[] = [];
|
|
52
|
+
try {
|
|
53
|
+
const unwrapped = unwrapTopLevel(await parse(data.expr));
|
|
54
|
+
forms = unwrapped.forms;
|
|
55
|
+
kinds = unwrapped.kinds;
|
|
56
|
+
} catch (error) {
|
|
57
|
+
const text = errorText(error);
|
|
58
|
+
if (isUnterminatedDictLiteral(text)) {
|
|
59
|
+
return refuse(fail(host.doors.render(unterminatedDictDoor(), SCHEME_REPL_TOOL_NAME)));
|
|
60
|
+
}
|
|
61
|
+
return refuse(fail(text));
|
|
62
|
+
}
|
|
63
|
+
const periodMs = typeof data.period === "string" ? periodMsOf(data.period) : undefined;
|
|
64
|
+
return {
|
|
65
|
+
forms,
|
|
66
|
+
kinds,
|
|
67
|
+
timeoutMs: data.timeoutMs,
|
|
68
|
+
maxInternalCalls: cappedCalls(data.maxInternalCalls),
|
|
69
|
+
periodMs,
|
|
70
|
+
source,
|
|
71
|
+
moduleRoot: moduleRootOf(source.sourcePath, host.moduleRoot, host.root),
|
|
72
|
+
};
|
|
73
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Re-run a program against the children it already minted. Does not seal the
|
|
3
|
+
* job, does not append notes, and does not move the watch cursor.
|
|
4
|
+
*/
|
|
5
|
+
import { parse } from "@here.build/arrival";
|
|
6
|
+
import type { EvalTrace } from "@here.build/arrival/provenance";
|
|
7
|
+
import { crossingEpoch, crossingOriginAfter, ProgrammaticJob } from "@inhuman.tools/harness-model";
|
|
8
|
+
import { callArgsSchema } from "@inhuman.tools/harness-model/schemas";
|
|
9
|
+
import { unwrapTopLevelBegins } from "@inhuman.tools/substrate/unwrap-begin";
|
|
10
|
+
import { when } from "mobx";
|
|
11
|
+
|
|
12
|
+
import { evaluateForms, observed, type SharedEval } from "./evaluate.js";
|
|
13
|
+
import type { ProgramHost } from "./host.js";
|
|
14
|
+
import { openProgram, type OpenedProgram } from "./open.js";
|
|
15
|
+
import { detachedRuntime, type ProgramRuntime } from "./runtime.js";
|
|
16
|
+
|
|
17
|
+
async function once(
|
|
18
|
+
job: ProgrammaticJob,
|
|
19
|
+
host: ProgramHost,
|
|
20
|
+
opened: OpenedProgram,
|
|
21
|
+
runtime: ProgramRuntime,
|
|
22
|
+
trace: EvalTrace | undefined,
|
|
23
|
+
): Promise<void> {
|
|
24
|
+
const limits = { timeoutMs: undefined, maxInternalCalls: opened.maxInternalCalls, trace };
|
|
25
|
+
const sourceText = opened.source.source;
|
|
26
|
+
if (sourceText == null) {
|
|
27
|
+
await evaluateForms(job, host, opened.forms, { kind: "once", ...limits }, runtime);
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
const sourceForms = unwrapTopLevelBegins(await parse(sourceText));
|
|
31
|
+
const shared: SharedEval = { signal: host.signal ?? new AbortController().signal };
|
|
32
|
+
const loaded = await evaluateForms(job, host, sourceForms, { kind: "source-load", ...limits, shared }, runtime);
|
|
33
|
+
if (loaded.kind !== "ok") return;
|
|
34
|
+
await evaluateForms(job, host, opened.forms, { kind: "source-body", ...limits, shared }, runtime);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async function periods(
|
|
38
|
+
job: ProgrammaticJob,
|
|
39
|
+
host: ProgramHost,
|
|
40
|
+
opened: OpenedProgram,
|
|
41
|
+
runtime: ProgramRuntime,
|
|
42
|
+
trace: EvalTrace | undefined,
|
|
43
|
+
): Promise<void> {
|
|
44
|
+
const limits = { timeoutMs: undefined, maxInternalCalls: opened.maxInternalCalls, trace };
|
|
45
|
+
let origin = 0;
|
|
46
|
+
while (host.signal?.aborted !== true) {
|
|
47
|
+
if ((job.completed || job.cancelled) && origin >= job.toolCalls.length) return;
|
|
48
|
+
const before = job.toolCalls.length;
|
|
49
|
+
const epoch = crossingEpoch(job, origin);
|
|
50
|
+
await evaluateForms(job, host, opened.forms, { kind: "period", tick: { origin }, ...limits }, runtime);
|
|
51
|
+
const next = crossingOriginAfter(job, origin, epoch);
|
|
52
|
+
if (next > origin) {
|
|
53
|
+
origin = next;
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
if (job.completed || job.cancelled || before < job.toolCalls.length) return;
|
|
57
|
+
const joined = when(
|
|
58
|
+
() => host.signal?.aborted === true || job.completed || job.cancelled || job.toolCalls.length > before,
|
|
59
|
+
);
|
|
60
|
+
try {
|
|
61
|
+
await joined;
|
|
62
|
+
} finally {
|
|
63
|
+
joined.cancel();
|
|
64
|
+
}
|
|
65
|
+
if (job.toolCalls.length === before) return;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export async function replayEvaluation(job: ProgrammaticJob, host: ProgramHost, trace: EvalTrace): Promise<void> {
|
|
70
|
+
const parsed = callArgsSchema.safeParse(job.args);
|
|
71
|
+
if (!parsed.success) return;
|
|
72
|
+
const opened = await openProgram(job, host, parsed.data, false);
|
|
73
|
+
if (opened == null) return;
|
|
74
|
+
const runtime = detachedRuntime(job, host);
|
|
75
|
+
runtime.arm({ watching: opened.periodMs != null, moduleRoot: opened.moduleRoot });
|
|
76
|
+
await observed(host, async (live) => {
|
|
77
|
+
const tap = live ?? trace;
|
|
78
|
+
if (opened.periodMs == null) {
|
|
79
|
+
await once(job, host, opened, runtime, tap);
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
await periods(job, host, opened, runtime, tap);
|
|
83
|
+
});
|
|
84
|
+
}
|
package/src/run.ts
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One-shot Scheme program. The host passes the vocabulary. `period` is rejected
|
|
3
|
+
* here; ongoing evaluation is the watchdog.
|
|
4
|
+
*/
|
|
5
|
+
import { parse, type SchemeValue } from "@here.build/arrival";
|
|
6
|
+
import { ProgrammaticJob } from "@inhuman.tools/harness-model";
|
|
7
|
+
import { callArgsSchema, REPL_PERIOD_ERR } from "@inhuman.tools/harness-model/schemas";
|
|
8
|
+
import { unwrapTopLevelBegins } from "@inhuman.tools/substrate/unwrap-begin";
|
|
9
|
+
|
|
10
|
+
import {
|
|
11
|
+
createEvaluationTimeout,
|
|
12
|
+
displayPrinted,
|
|
13
|
+
errorText,
|
|
14
|
+
evaluateForms,
|
|
15
|
+
fail,
|
|
16
|
+
guardProgram,
|
|
17
|
+
observed,
|
|
18
|
+
printedValues,
|
|
19
|
+
sealChunk,
|
|
20
|
+
type EvalOutcome,
|
|
21
|
+
type SharedEval,
|
|
22
|
+
} from "./evaluate.js";
|
|
23
|
+
import type { ProgramHost } from "./host.js";
|
|
24
|
+
import { openProgram } from "./open.js";
|
|
25
|
+
import { programRuntimeOf } from "./runtime.js";
|
|
26
|
+
|
|
27
|
+
function rawHasPeriod(raw: unknown): boolean {
|
|
28
|
+
return raw !== null && typeof raw === "object" && "period" in raw;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export async function runProgram(chunk: ProgrammaticJob, host: ProgramHost): Promise<void> {
|
|
32
|
+
await guardProgram(chunk, host, async () => {
|
|
33
|
+
const rawArgs = chunk.args;
|
|
34
|
+
if (rawHasPeriod(rawArgs)) {
|
|
35
|
+
sealChunk(chunk, fail(`Error: ${REPL_PERIOD_ERR}`));
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
const parsed = callArgsSchema.safeParse(rawArgs);
|
|
39
|
+
if (!parsed.success) {
|
|
40
|
+
const issue = parsed.error.issues[0]?.message ?? "invalid arguments";
|
|
41
|
+
sealChunk(chunk, fail(`Error: ${issue}`));
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
const opened = await openProgram(chunk, host, parsed.data);
|
|
45
|
+
if (opened == null) return;
|
|
46
|
+
|
|
47
|
+
const runtime = programRuntimeOf(chunk, host);
|
|
48
|
+
runtime.arm({ watching: false, moduleRoot: opened.moduleRoot });
|
|
49
|
+
const sourceText = opened.source.source;
|
|
50
|
+
let sourceForms: readonly SchemeValue[] | undefined;
|
|
51
|
+
if (sourceText != null) {
|
|
52
|
+
try {
|
|
53
|
+
sourceForms = unwrapTopLevelBegins(await parse(sourceText));
|
|
54
|
+
} catch (error) {
|
|
55
|
+
sealChunk(chunk, fail(errorText(error)));
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
const limits = { timeoutMs: opened.timeoutMs, maxInternalCalls: opened.maxInternalCalls };
|
|
60
|
+
const outcome: EvalOutcome = await observed(host, async (trace) => {
|
|
61
|
+
if (sourceForms === undefined) {
|
|
62
|
+
return evaluateForms(chunk, host, opened.forms, { kind: "once", ...limits, trace }, runtime);
|
|
63
|
+
}
|
|
64
|
+
const timeout = createEvaluationTimeout(host.signal, opened.timeoutMs);
|
|
65
|
+
const shared: SharedEval = { signal: timeout.signal };
|
|
66
|
+
try {
|
|
67
|
+
const loaded = await evaluateForms(
|
|
68
|
+
chunk,
|
|
69
|
+
host,
|
|
70
|
+
sourceForms,
|
|
71
|
+
{ kind: "source-load", ...limits, shared, trace },
|
|
72
|
+
runtime,
|
|
73
|
+
);
|
|
74
|
+
return loaded.kind === "ok"
|
|
75
|
+
? await evaluateForms(chunk, host, opened.forms, { kind: "source-body", ...limits, shared, trace }, runtime)
|
|
76
|
+
: loaded;
|
|
77
|
+
} finally {
|
|
78
|
+
timeout.dispose();
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
if (outcome.kind === "park") return;
|
|
82
|
+
if (outcome.kind === "halt") {
|
|
83
|
+
sealChunk(chunk, outcome.result);
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
if (outcome.kind === "fail") {
|
|
87
|
+
sealChunk(chunk, fail(outcome.values.map(displayPrinted).join("\n\n"), outcome.code));
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
const sealed: {
|
|
91
|
+
ok: true;
|
|
92
|
+
value: ReturnType<typeof printedValues>;
|
|
93
|
+
json?: unknown;
|
|
94
|
+
jsonError?: string;
|
|
95
|
+
} = {
|
|
96
|
+
ok: true,
|
|
97
|
+
value: printedValues(outcome.values, opened.kinds),
|
|
98
|
+
};
|
|
99
|
+
if (outcome.jsonError != null) sealed.jsonError = outcome.jsonError;
|
|
100
|
+
else if ("json" in outcome) sealed.json = outcome.json;
|
|
101
|
+
sealChunk(chunk, sealed);
|
|
102
|
+
});
|
|
103
|
+
}
|
package/src/runtime.ts
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { EnvCapability } from "@here.build/arrival";
|
|
2
|
+
import type { ProgrammaticJob } from "@inhuman.tools/harness-model";
|
|
3
|
+
import { DISPLAY_INTERNAL, displaySymbol } from "@inhuman.tools/substrate";
|
|
4
|
+
import invariant from "tiny-invariant";
|
|
5
|
+
|
|
6
|
+
import type { PreparedProgram, ProgramHost, ProgramRuntime } from "./host.js";
|
|
7
|
+
|
|
8
|
+
type ReplRig = {
|
|
9
|
+
signal: AbortSignal;
|
|
10
|
+
origin: number;
|
|
11
|
+
replayAt: number;
|
|
12
|
+
/** toolCalls length at the start of the current top-level statement. */
|
|
13
|
+
formAt: number;
|
|
14
|
+
maxInternalCalls: number | undefined;
|
|
15
|
+
failureCode: string | undefined;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
/** `(display …)` is a host verb. Arrival has no IO; the runner rewrites the call onto this symbol. */
|
|
19
|
+
const displayCapability = EnvCapability.define("harness/scheme-repl-display", {
|
|
20
|
+
symbols: () => ({
|
|
21
|
+
[DISPLAY_INTERNAL]: displaySymbol(),
|
|
22
|
+
}),
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
const runtimes = new WeakMap<
|
|
26
|
+
ProgrammaticJob,
|
|
27
|
+
{ readonly identity: object; readonly signal?: AbortSignal; readonly runtime: ProgramRuntime }
|
|
28
|
+
>();
|
|
29
|
+
|
|
30
|
+
function runtimeFor(job: ProgrammaticJob, host: ProgramHost): ProgramRuntime {
|
|
31
|
+
const rig: ReplRig = {
|
|
32
|
+
signal: new AbortController().signal,
|
|
33
|
+
origin: 0,
|
|
34
|
+
replayAt: 0,
|
|
35
|
+
formAt: 0,
|
|
36
|
+
maxInternalCalls: undefined,
|
|
37
|
+
failureCode: undefined,
|
|
38
|
+
};
|
|
39
|
+
let armed: PreparedProgram | undefined;
|
|
40
|
+
const view = (): PreparedProgram => {
|
|
41
|
+
if (armed === undefined) throw new Error("scheme-repl: arm the runtime before evaluating");
|
|
42
|
+
return armed;
|
|
43
|
+
};
|
|
44
|
+
const runtime: ProgramRuntime = {
|
|
45
|
+
arm(opts) {
|
|
46
|
+
if (armed !== undefined) return;
|
|
47
|
+
const prepared = host.prepare({ moduleRoot: opts.moduleRoot, job });
|
|
48
|
+
armed = {
|
|
49
|
+
capabilities: [...prepared.capabilities, displayCapability],
|
|
50
|
+
config: { ...prepared.config, job, rig },
|
|
51
|
+
};
|
|
52
|
+
},
|
|
53
|
+
beginTick(tick) {
|
|
54
|
+
rig.signal = tick.signal;
|
|
55
|
+
rig.origin = tick.origin;
|
|
56
|
+
rig.replayAt = tick.origin;
|
|
57
|
+
rig.maxInternalCalls = tick.maxInternalCalls;
|
|
58
|
+
rig.failureCode = undefined;
|
|
59
|
+
},
|
|
60
|
+
clearFailure() {
|
|
61
|
+
rig.formAt = job.toolCalls.length;
|
|
62
|
+
rig.failureCode = undefined;
|
|
63
|
+
},
|
|
64
|
+
failureCode() {
|
|
65
|
+
return rig.failureCode;
|
|
66
|
+
},
|
|
67
|
+
get capabilities() {
|
|
68
|
+
return view().capabilities;
|
|
69
|
+
},
|
|
70
|
+
get config() {
|
|
71
|
+
return view().config;
|
|
72
|
+
},
|
|
73
|
+
};
|
|
74
|
+
return runtime;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** One runtime per job. A new host is legal only after the previous signal parked. The first prepare wins. */
|
|
78
|
+
export function programRuntimeOf(job: ProgrammaticJob, host: ProgramHost): ProgramRuntime {
|
|
79
|
+
const existing = runtimes.get(job);
|
|
80
|
+
if (existing !== undefined) {
|
|
81
|
+
if (existing.identity === host.identity) return existing.runtime;
|
|
82
|
+
const stale = existing.signal;
|
|
83
|
+
invariant(
|
|
84
|
+
stale != null && stale.aborted && stale.reason === "park",
|
|
85
|
+
"scheme-repl: ProgrammaticJob rebound to a different host",
|
|
86
|
+
);
|
|
87
|
+
runtimes.delete(job);
|
|
88
|
+
}
|
|
89
|
+
const runtime = runtimeFor(job, host);
|
|
90
|
+
runtimes.set(job, { identity: host.identity, signal: host.signal, runtime });
|
|
91
|
+
return runtime;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** A second host reading the same job (the browser's replay) must not take the daemon's runtime. */
|
|
95
|
+
export function detachedRuntime(job: ProgrammaticJob, host: ProgramHost): ProgramRuntime {
|
|
96
|
+
return runtimeFor(job, host);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export { type ProgramRuntime } from "./host.js";
|
package/src/source-fs.ts
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Disk `require` jail. Replay never imports this: it reads `job.modules`.
|
|
3
|
+
*/
|
|
4
|
+
import { readFile, realpath } from "node:fs/promises";
|
|
5
|
+
import { join as posixJoin } from "node:path/posix";
|
|
6
|
+
|
|
7
|
+
import { makeFsLoader, type Loader } from "@here.build/arrival-modules";
|
|
8
|
+
import type { ProgrammaticJob } from "@inhuman.tools/harness-model";
|
|
9
|
+
import { runInAction } from "mobx";
|
|
10
|
+
|
|
11
|
+
export function programSourceLoader(root: string, job?: ProgrammaticJob): Loader {
|
|
12
|
+
return makeFsLoader({
|
|
13
|
+
async readFile(key: string) {
|
|
14
|
+
if (job !== undefined) {
|
|
15
|
+
const recorded = job.modules[key];
|
|
16
|
+
if (recorded !== undefined) return recorded;
|
|
17
|
+
}
|
|
18
|
+
const abs = posixJoin(root, key);
|
|
19
|
+
const realRoot = await realpath(root);
|
|
20
|
+
let real: string;
|
|
21
|
+
try {
|
|
22
|
+
real = await realpath(abs);
|
|
23
|
+
} catch {
|
|
24
|
+
throw new Error(`no such file: ${key}`);
|
|
25
|
+
}
|
|
26
|
+
const prefix = realRoot.endsWith("/") ? realRoot : `${realRoot}/`;
|
|
27
|
+
if (real !== realRoot && !real.startsWith(prefix)) {
|
|
28
|
+
throw new Error(`require path escapes program source root: ${key}`);
|
|
29
|
+
}
|
|
30
|
+
const text = await readFile(real, "utf8");
|
|
31
|
+
if (job !== undefined) runInAction(() => (job.modules[key] = text));
|
|
32
|
+
return text;
|
|
33
|
+
},
|
|
34
|
+
});
|
|
35
|
+
}
|
package/src/source.ts
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Module `require` jail. Eval with `sourcePath` stays under the entry file's
|
|
3
|
+
* directory. Interactive `scheme_repl` / `scheme_watch` jail at the workspace
|
|
4
|
+
* (`host.root`). Skill inputs stay on licensed `read_file`.
|
|
5
|
+
*/
|
|
6
|
+
import { toJS, type SchemeValue } from "@here.build/arrival";
|
|
7
|
+
import { makeFsLoader, type Loader } from "@here.build/arrival-modules";
|
|
8
|
+
import type { ProgrammaticJob } from "@inhuman.tools/harness-model";
|
|
9
|
+
|
|
10
|
+
export type ProgramSource = {
|
|
11
|
+
readonly source?: string;
|
|
12
|
+
readonly sourcePath?: string;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export function programSourceOf(raw: unknown, allow: boolean): ProgramSource {
|
|
16
|
+
if (!allow || raw === null || typeof raw !== "object" || Array.isArray(raw)) return {};
|
|
17
|
+
const rec = raw as Record<string, unknown>;
|
|
18
|
+
const source = typeof rec.source === "string" && rec.source !== "" ? rec.source : undefined;
|
|
19
|
+
const sourcePath =
|
|
20
|
+
typeof rec.sourcePath === "string" && rec.sourcePath.trim() !== "" ? rec.sourcePath.trim() : undefined;
|
|
21
|
+
return { source, sourcePath };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function parentDir(path: string): string {
|
|
25
|
+
const slash = Math.max(path.lastIndexOf("/"), path.lastIndexOf("\\"));
|
|
26
|
+
if (slash <= 0) return slash === 0 ? path.slice(0, 1) : ".";
|
|
27
|
+
return path.slice(0, slash);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function moduleRootOf(
|
|
31
|
+
sourcePath: string | undefined,
|
|
32
|
+
override?: string,
|
|
33
|
+
workspaceRoot?: string,
|
|
34
|
+
): string | undefined {
|
|
35
|
+
if (override != null && override !== "") return override;
|
|
36
|
+
if (sourcePath != null && sourcePath !== "") return parentDir(sourcePath);
|
|
37
|
+
if (workspaceRoot != null && workspaceRoot !== "") return workspaceRoot;
|
|
38
|
+
return undefined;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Replay read. The live loader records the same key the first time it touches disk. */
|
|
42
|
+
export function recordedModuleLoader(job: ProgrammaticJob): Loader {
|
|
43
|
+
return makeFsLoader({
|
|
44
|
+
readFile(key: string) {
|
|
45
|
+
const text = job.modules[key];
|
|
46
|
+
if (text === undefined) throw new Error(`replay: no recorded module ${key}`);
|
|
47
|
+
return text;
|
|
48
|
+
},
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** toJS of a dict or list is an egress proxy. structuredClone rejects proxies. */
|
|
53
|
+
function plainData(value: unknown, seen = new WeakSet<object>()): unknown {
|
|
54
|
+
if (typeof value === "function") throw new Error("not json");
|
|
55
|
+
if (value === null || typeof value !== "object") return value;
|
|
56
|
+
if (seen.has(value)) throw new Error("cycle");
|
|
57
|
+
seen.add(value);
|
|
58
|
+
if (Array.isArray(value)) return value.map((item) => plainData(item, seen));
|
|
59
|
+
const out: Record<string, unknown> = {};
|
|
60
|
+
for (const key of Reflect.ownKeys(value)) {
|
|
61
|
+
if (typeof key !== "string") throw new Error("not json");
|
|
62
|
+
out[key] = plainData((value as Record<string, unknown>)[key], seen);
|
|
63
|
+
}
|
|
64
|
+
return out;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Objects/arrays become the additive `json` sibling. Scalars stay display-only. */
|
|
68
|
+
export function structuredJson(value: SchemeValue): { json?: unknown; jsonError?: string } {
|
|
69
|
+
const js = toJS(value);
|
|
70
|
+
if (typeof js === "function") return { jsonError: "value is not JSON-compatible" };
|
|
71
|
+
if (js === null || typeof js !== "object") return {};
|
|
72
|
+
try {
|
|
73
|
+
const cloned: unknown = structuredClone(plainData(js));
|
|
74
|
+
return { json: cloned };
|
|
75
|
+
} catch {
|
|
76
|
+
return { jsonError: "value is not JSON-compatible" };
|
|
77
|
+
}
|
|
78
|
+
}
|
package/README.md
DELETED