@inhuman.tools/harness-scheme 0.0.0-stage → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/LICENSE.md +7 -0
  2. package/dist/bound-tools.d.ts +6 -0
  3. package/dist/bound-tools.d.ts.map +1 -0
  4. package/dist/bound-tools.js +88 -0
  5. package/dist/bound-tools.js.map +1 -0
  6. package/dist/catalog-description.d.ts +5 -0
  7. package/dist/catalog-description.d.ts.map +1 -0
  8. package/dist/catalog-description.js +150 -0
  9. package/dist/catalog-description.js.map +1 -0
  10. package/dist/connection-models.d.ts +20 -0
  11. package/dist/connection-models.d.ts.map +1 -0
  12. package/dist/connection-models.js +31 -0
  13. package/dist/connection-models.js.map +1 -0
  14. package/dist/env.d.ts +109 -0
  15. package/dist/env.d.ts.map +1 -0
  16. package/dist/env.js +923 -0
  17. package/dist/env.js.map +1 -0
  18. package/dist/file-path.d.ts +2 -0
  19. package/dist/file-path.d.ts.map +1 -0
  20. package/dist/file-path.js +2 -0
  21. package/dist/file-path.js.map +1 -0
  22. package/dist/index.d.ts +10 -0
  23. package/dist/index.d.ts.map +1 -0
  24. package/dist/index.js +10 -0
  25. package/dist/index.js.map +1 -0
  26. package/dist/interpret.d.ts +13 -0
  27. package/dist/interpret.d.ts.map +1 -0
  28. package/dist/interpret.js +26 -0
  29. package/dist/interpret.js.map +1 -0
  30. package/dist/keywords.d.ts +17 -0
  31. package/dist/keywords.d.ts.map +1 -0
  32. package/dist/keywords.js +76 -0
  33. package/dist/keywords.js.map +1 -0
  34. package/dist/limits.d.ts +11 -0
  35. package/dist/limits.d.ts.map +1 -0
  36. package/dist/limits.js +4 -0
  37. package/dist/limits.js.map +1 -0
  38. package/dist/program-source.d.ts +2 -0
  39. package/dist/program-source.d.ts.map +1 -0
  40. package/dist/program-source.js +2 -0
  41. package/dist/program-source.js.map +1 -0
  42. package/dist/replay.d.ts +9 -0
  43. package/dist/replay.d.ts.map +1 -0
  44. package/dist/replay.js +38 -0
  45. package/dist/replay.js.map +1 -0
  46. package/package.json +80 -3
  47. package/src/bound-tools.ts +108 -0
  48. package/src/catalog-description.ts +150 -0
  49. package/src/connection-models.ts +39 -0
  50. package/src/env.ts +1185 -0
  51. package/src/file-path.ts +1 -0
  52. package/src/index.ts +17 -0
  53. package/src/interpret.ts +26 -0
  54. package/src/keywords.ts +84 -0
  55. package/src/limits.ts +13 -0
  56. package/src/program-source.ts +1 -0
  57. package/src/replay.ts +46 -0
  58. package/README.md +0 -3
package/src/env.ts ADDED
@@ -0,0 +1,1185 @@
1
+ /**
2
+ * Scheme-repl verbs. Module singleton: job, host, rig, and tools are
3
+ * configuration, read when a verb runs.
4
+ *
5
+ * Calls look synchronous; Arrival's trampoline fans out independent host
6
+ * promises. Background is an outer tool arg, not a verb — the ProgrammaticJob
7
+ * is foreground or background as a whole.
8
+ *
9
+ * Config is the stable bag (writable, host, tools, loader, prompt, keyOf). The
10
+ * interpreter adds job and rig at arm and mutates the rig in place. The
11
+ * vocabulary memo keys on capability identity plus that object, so a watch
12
+ * tick does not re-bake.
13
+ *
14
+ * Toolkit we own: required positionals, optional trailing dict. Ask schemas are
15
+ * Arrival s/* tagged lists (`schemaCapability`); this pack does not mint a
16
+ * second type wire. Pack-tool *names* are not known at `define()`, so they bind
17
+ * as their own symbols on a per-catalog overlay onto the same `call` substrate.
18
+ * `mcp/call` is catalog lookup onto that same `call`. Connection and tool
19
+ * are positionals; the last argument stays Arrival `inputRest` as `sz.dynamic`
20
+ * so a trailing dict and `:k v` pairs both land — a record kwargs shape
21
+ * would drop unknown MCP keys. Closed toolkit verbs (list_dir, grep, write/edit,
22
+ * shell, web_search, web_extract) keep their positionals and take a `sz.dynamic`
23
+ * rest through `keywordsOf`. A record `inputRest` is keywords only and drops
24
+ * the positional input. `read_file` stays native and peels its own rest.
25
+ * bot/send, and bot/ask when it names a spec, construct
26
+ * BotCallJob. agent/call, agent/ask, and an omitted-spec bot/ask construct
27
+ * AgentCallJob. The job name stays agent__send or agent__ask; the class owns
28
+ * the worker, so these verbs do not mint oneShot. The verb `registerJob`s the
29
+ * job it constructed. Pack bot-call tools, worker replies, and ordinary host
30
+ * tools still admit, then `registerJob` the leaf. A `.prompt` on this program
31
+ * constructs an AgentCallJob the same way. Spec/message/[schema]
32
+ * stay positional; `:readonly` and `:model` are Arrival kwargs on `inputRest`.
33
+ * `:model` is `"connection/model"` and pins that disposable worker.
34
+ * `connection/models` lists a wire's live ids.
35
+ * Comms are the same call onto agent__* wires. `agent/respond` is a
36
+ * notImplemented door — settle with the `agent__respond` JSON tool.
37
+ * `self/notify` is an origin atom on this bag (or the enclosing Call); it does
38
+ * not settle. Bag-home atoms recall only when the bag is background; a sitter
39
+ * writes the enclosing Call.
40
+ */
41
+ import {
42
+ type CallCtx,
43
+ EnvCapability,
44
+ isSchemeValue,
45
+ type SchemeValue,
46
+ type SymbolDeclaration,
47
+ toJS,
48
+ } from "@here.build/arrival";
49
+ import { schemaCapability } from "@here.build/arrival/capabilities/schema";
50
+ import { ANil, AString } from "@here.build/arrival/reflect-internals";
51
+ import { tagToJsonSchema } from "@here.build/arrival/schema-tag";
52
+ import { type Loader, arrivalLoaderCapability } from "@here.build/arrival-modules";
53
+ import { arrivalYamlCapability } from "@here.build/arrival-modules/yaml";
54
+ import { toSExprString } from "@here.build/arrival-serializer";
55
+ import { promptProgramCapability, type PromptComplete } from "@inhuman.tools/env-capability-module-prompt";
56
+ import {
57
+ AGENT_ASK,
58
+ AGENT_MESSAGE,
59
+ AGENT_NOTIFY,
60
+ AGENT_SEND,
61
+ AGENT_VOID,
62
+ AgentCallJob,
63
+ type AgentCallNotification,
64
+ ApprovableJob,
65
+ AutonomousToolError,
66
+ BotCallJob,
67
+ cancelOutcomeText,
68
+ claimCrossing,
69
+ crossingPending,
70
+ crossingWindow,
71
+ isReplaying,
72
+ ProgrammaticJob,
73
+ SCHEME_REPL_NAMING,
74
+ SCHEME_REPL_TOOL_NAME,
75
+ SCHEME_WATCH_TOOL_NAME,
76
+ UPDATE_METADATA_TOOL_NAME,
77
+ type SessionRoot,
78
+ takeCrossing,
79
+ ToolCallJob,
80
+ WEB_EXTRACT_TOOL_NAME,
81
+ WEB_SEARCH_TOOL_NAME,
82
+ type WorkflowLeafMeta,
83
+ } from "@inhuman.tools/harness-model";
84
+ import {
85
+ appendNotify,
86
+ failureCodeOf,
87
+ programRuntimeOf,
88
+ recordedModuleLoader,
89
+ SchemeReplToolFailure,
90
+ type ProgramHost,
91
+ type ProgramRuntime,
92
+ } from "@inhuman.tools/scheme-program";
93
+ import type { BoundTool } from "@inhuman.tools/substrate/bound-tool";
94
+ import { DoorSession, internalCallBudgetDoor, unknownToolDoor } from "@inhuman.tools/substrate/doors";
95
+ import type { ToolCall } from "@inhuman.tools/tool-call-normalize";
96
+ import type { KernelToolCatalog } from "@inhuman.tools/turn/catalog";
97
+ import { runInAction, when } from "mobx";
98
+ import invariant from "tiny-invariant";
99
+ import { z } from "zod";
100
+
101
+ import { boundToolsOf } from "./bound-tools.js";
102
+ import { stampFilePath } from "./file-path.js";
103
+ import { isKeyword, keywordsOf, splitKeywords } from "./keywords.js";
104
+ import { DEFAULT_SCHEME_REPL_LIMITS } from "./limits.js";
105
+
106
+ /** Wire name. Do not import the builtin roster just for this token. */
107
+ const SHELL_TOOL_NAME = "shell";
108
+
109
+ const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
110
+
111
+ export type SchemeReplHost = {
112
+ /** Absent on a replay host, which binds `replayTools` and never admits. */
113
+ readonly catalog?: KernelToolCatalog;
114
+ readonly root: string;
115
+ /** Match recorded children. Missing is an error, and nothing is minted. */
116
+ readonly replayOnly?: boolean;
117
+ /** Pack names recorded on the job. Replaces the daemon catalog. */
118
+ readonly replayTools?: ReadonlyMap<string, BoundTool>;
119
+ /** Disk `require`. Replay leaves this unset and reads `job.modules`. */
120
+ readonly loadSource?: (root: string, job: ProgrammaticJob) => Loader;
121
+ readonly signal?: AbortSignal;
122
+ readonly sessionId?: string;
123
+ readonly session?: SessionRoot;
124
+ readonly doors: DoorSession;
125
+ readonly admit: (call: ToolCall) => ToolCallJob | ProgrammaticJob;
126
+ readonly abort?: (leaf: ApprovableJob, reason?: unknown) => void;
127
+ /** Origin atom. Default writes `job.notifications`. Orchestrator may XOR onto an enclosing Call. */
128
+ readonly notify?: (note: AgentCallNotification) => void;
129
+ /** Watch key. Omit → pack provenance from the per-tick tap. */
130
+ readonly keyOf?: (value: unknown) => string | undefined;
131
+ /** Ask desk. Bound-tools allowlist drops write/edit; shell stays. */
132
+ readonly readonly?: boolean;
133
+ /** Live model ids for `(connection/models)`. Missing name throws. */
134
+ readonly connectionModels?: (connection: string) => readonly string[];
135
+ /** Wraps each run so the daemon owns its trace; see `ProgramHost.observe`. */
136
+ readonly observe?: ProgramHost["observe"];
137
+ /** Unattended eval. Honors `source` / `sourcePath` on the bag. */
138
+ readonly evalProgram?: boolean;
139
+ /** Override jail root. Default is `dirname(sourcePath)`. */
140
+ readonly moduleRoot?: string;
141
+ readonly complete?: PromptComplete;
142
+ readonly connectionId?: string;
143
+ readonly completionModel?: string;
144
+ };
145
+
146
+ /** Per-tick. Verbs read it when they run; it is not baked into the capability. */
147
+ type ReplRig = {
148
+ signal: AbortSignal;
149
+ origin: number;
150
+ replayAt: number;
151
+ /** toolCalls length at the start of the current top-level statement. */
152
+ formAt: number;
153
+ maxInternalCalls: number | undefined;
154
+ failureCode: string | undefined;
155
+ };
156
+
157
+ function isSchemeReplHost(value: unknown): value is SchemeReplHost {
158
+ if (value === null || typeof value !== "object") return false;
159
+ const rec = value as SchemeReplHost;
160
+ return typeof rec.admit === "function" && rec.doors != null && (rec.catalog != null || rec.replayTools != null);
161
+ }
162
+
163
+ function isReplRig(value: unknown): value is ReplRig {
164
+ if (value === null || typeof value !== "object") return false;
165
+ return typeof (value as ReplRig).replayAt === "number";
166
+ }
167
+
168
+ const replConfiguration = {
169
+ writable: z.boolean().optional(),
170
+ job: z.custom<ProgrammaticJob>((value) => value === undefined || value instanceof ProgrammaticJob).optional(),
171
+ host: z.custom<SchemeReplHost>((value) => value === undefined || isSchemeReplHost(value)).optional(),
172
+ rig: z.custom<ReplRig>((value) => value === undefined || isReplRig(value)).optional(),
173
+ tools: z.custom<ReadonlyMap<string, BoundTool>>((value) => value === undefined || value instanceof Map).optional(),
174
+ };
175
+
176
+ type ReplConfig = {
177
+ job?: ProgrammaticJob;
178
+ host?: SchemeReplHost;
179
+ rig?: ReplRig;
180
+ tools?: ReadonlyMap<string, BoundTool>;
181
+ };
182
+
183
+ type ReplActivation = {
184
+ invocation?: CallCtx["invocation"];
185
+ configuration?: unknown;
186
+ runCtx?: { capabilityConfigurations?: { get(key: object): unknown } };
187
+ };
188
+
189
+ function isReplConfig(value: unknown): value is ReplConfig {
190
+ if (value === null || typeof value !== "object") return false;
191
+ return isSchemeReplHost((value as ReplConfig).host);
192
+ }
193
+
194
+ /** Direct dispatch has `this.configuration`. HOF (`map read_file`) omits it; the
195
+ * run table is keyed on the singleton capability object. */
196
+ function configured(ctx: ReplActivation): ReplConfig {
197
+ if (isReplConfig(ctx.configuration)) return ctx.configuration;
198
+ const mapped = ctx.runCtx?.capabilityConfigurations?.get(schemeReplCapability);
199
+ if (isReplConfig(mapped)) return mapped;
200
+ throw new Error("scheme-repl: host is not configured");
201
+ }
202
+
203
+ function replBag(ctx: ReplActivation): {
204
+ job: ProgrammaticJob;
205
+ host: SchemeReplHost;
206
+ rig: ReplRig;
207
+ tools: ReadonlyMap<string, BoundTool>;
208
+ } {
209
+ const { job, host, rig, tools } = configured(ctx);
210
+ invariant(job instanceof ProgrammaticJob, "scheme-repl: job is not configured");
211
+ invariant(isSchemeReplHost(host), "scheme-repl: host is not configured");
212
+ invariant(isReplRig(rig), "scheme-repl: rig is not configured");
213
+ invariant(tools instanceof Map, "scheme-repl: tools are not configured");
214
+ return { job, host, rig, tools };
215
+ }
216
+
217
+ export type ReplRuntime = ProgramRuntime;
218
+
219
+ /** Arrival `isSchemeValue` is Boxed (Macro/Syntax/Function); `toJS` wants SchemeValue. */
220
+ function toJsValue(value: unknown): unknown {
221
+ return isSchemeValue(value) ? toJS(value as SchemeValue) : value;
222
+ }
223
+
224
+ function argsOf(record: Record<string, unknown>): Record<string, unknown> {
225
+ const out: Record<string, unknown> = {};
226
+ for (const [key, value] of Object.entries(record)) {
227
+ if (value === undefined) continue;
228
+ out[key] = toJsValue(value);
229
+ }
230
+ return out;
231
+ }
232
+
233
+ function jsRecord(record: Record<string, unknown> | undefined): Record<string, unknown> {
234
+ return record === undefined ? {} : argsOf(record);
235
+ }
236
+
237
+ /** Open MCP keys cannot use a record `inputRest` — Arrival drops unknown kwargs.
238
+ * Native contour also does not fold `:k v` (no membrane decode). Both peel
239
+ * `sz.dynamic` rest / leftover args as a trailing dict or even `:k v` pairs. */
240
+ function restKwargs(rest: readonly unknown[], verb: string): Record<string, unknown> {
241
+ if (rest.length === 0) return {};
242
+ if (rest.length === 1) {
243
+ const js = toJsValue(rest[0]);
244
+ if (isPlainRecord(js) && !isSchemeValue(js)) return argsOf(js);
245
+ throw new Error(`${verb}: last argument must be a dict or :key value pairs`);
246
+ }
247
+ if (rest.length % 2 !== 0) {
248
+ throw new Error(`${verb}: dangling keyword`);
249
+ }
250
+ const out: Record<string, unknown> = {};
251
+ for (let i = 0; i < rest.length; i += 2) {
252
+ out[String(rest[i]).replace(/^:/, "")] = toJsValue(rest[i + 1]);
253
+ }
254
+ return out;
255
+ }
256
+
257
+ function stringOf(value: unknown): string {
258
+ if (typeof value === "string") return value;
259
+ if (value instanceof AString) return value.valueOf();
260
+ return String(toJsValue(value));
261
+ }
262
+
263
+ function isStringLike(value: unknown): boolean {
264
+ if (typeof value === "string" || value instanceof AString) return true;
265
+ return typeof toJsValue(value) === "string";
266
+ }
267
+
268
+ function isPlainRecord(value: unknown): value is Record<string, unknown> {
269
+ return value !== null && typeof value === "object" && !Array.isArray(value);
270
+ }
271
+
272
+ const METADATA_KEYWORDS = {
273
+ current_turn_title: z.any().optional(),
274
+ current_turn_description: z.any().optional(),
275
+ };
276
+
277
+ function metadataBlank(value: unknown): boolean {
278
+ if (value == null || value instanceof ANil) return true;
279
+ const js = toJsValue(value);
280
+ return js == null || js instanceof ANil;
281
+ }
282
+
283
+ function metadataString(value: unknown, field: string): string {
284
+ const js = toJsValue(value);
285
+ if (typeof js !== "string") throw new Error(`update_metadata: ${field} must be a string`);
286
+ return js;
287
+ }
288
+
289
+ /** Positional title and description, a keyword tail, or one dict. */
290
+ function metadataArgs(slots: readonly unknown[]): Record<string, unknown> {
291
+ if (slots.length === 1) {
292
+ const sole = toJsValue(slots[0]);
293
+ if (isPlainRecord(sole)) return metadataRecord(sole);
294
+ }
295
+ const { positionals, keywords } = splitKeywords(UPDATE_METADATA_TOOL_NAME, slots, METADATA_KEYWORDS);
296
+ if (positionals.length > 2) throw new Error("update_metadata: at most a title and a description");
297
+ const titleSlot = keywords.current_turn_title ?? positionals[0];
298
+ if (metadataBlank(titleSlot)) throw new Error("update_metadata: current_turn_title is required");
299
+ const described =
300
+ keywords.current_turn_description !== undefined ? keywords.current_turn_description : positionals[1];
301
+ const out: Record<string, unknown> = { current_turn_title: metadataString(titleSlot, "current_turn_title") };
302
+ if (!metadataBlank(described)) {
303
+ out.current_turn_description = metadataString(described, "current_turn_description");
304
+ }
305
+ return out;
306
+ }
307
+
308
+ function metadataRecord(record: Record<string, unknown>): Record<string, unknown> {
309
+ if (metadataBlank(record.current_turn_title)) throw new Error("update_metadata: current_turn_title is required");
310
+ const out: Record<string, unknown> = {
311
+ current_turn_title: metadataString(record.current_turn_title, "current_turn_title"),
312
+ };
313
+ if (!metadataBlank(record.current_turn_description)) {
314
+ out.current_turn_description = metadataString(record.current_turn_description, "current_turn_description");
315
+ }
316
+ return out;
317
+ }
318
+
319
+ function workerSpec(verb: string, slot: unknown): string {
320
+ if (isKeyword(slot)) throw new Error(`${verb}: the worker spec comes before keywords, got ${String(slot)}`);
321
+ const spec = stringOf(slot).trim();
322
+ if (spec === "") throw new Error(`${verb}: a worker spec is required`);
323
+ return spec;
324
+ }
325
+
326
+ function shellCwd(rest: readonly unknown[]): string {
327
+ if (rest.length === 1 && isPlainRecord(rest[0]) && !isKeyword(rest[0])) {
328
+ const cwd = rest[0].cwd;
329
+ return cwd === undefined ? "." : stringOf(cwd);
330
+ }
331
+ const at = rest.findIndex((arg) => isKeyword(arg) && String(arg) === ":cwd");
332
+ const value = at === -1 ? undefined : rest[at + 1];
333
+ return value === undefined ? "." : stringOf(value);
334
+ }
335
+
336
+ function compiledAskSchema(value: unknown): Record<string, unknown> | undefined {
337
+ if (value === undefined) return undefined;
338
+ const js = toJsValue(value);
339
+ if (typeof js === "string" || Array.isArray(js)) return tagToJsonSchema(js);
340
+ if (!isPlainRecord(js)) throw new Error("ask schema: expected a type or field dict");
341
+ if (typeof js.type === "string") return js;
342
+ return tagToJsonSchema(["object", ...Object.entries(js).map(([name, type]) => [name, type])]);
343
+ }
344
+
345
+ function fsPath(path: string): readonly (readonly string[])[] {
346
+ return [["fs", path]];
347
+ }
348
+
349
+ function abortThrow(signal: AbortSignal): Error {
350
+ const text = cancelOutcomeText(signal.reason);
351
+ return new Error(text === "cancelled" ? "Scheme REPL call cancelled" : text);
352
+ }
353
+
354
+ function rawOutcome(snap: { readonly text?: string; readonly value?: unknown }): unknown {
355
+ if ("value" in snap) return snap.value;
356
+ return snap.text;
357
+ }
358
+
359
+ function sexpr(value: unknown): string {
360
+ try {
361
+ return toSExprString(value as SchemeValue);
362
+ } catch {
363
+ return JSON.stringify(value);
364
+ }
365
+ }
366
+
367
+ /** What a settled inner call actually returned. Not the Scheme value the form would have produced. */
368
+ function rawCallLine(inner: ToolCallJob): string {
369
+ const snap = inner.outcomeSnap;
370
+ const name = inner.name.trim() === "" ? "tool" : inner.name.trim();
371
+ const args = sexpr(inner.args ?? {});
372
+ if (snap === undefined || !inner.completed) return `${name} ${args} — no outcome`;
373
+ if (!snap.ok) {
374
+ const own = typeof snap.text === "string" ? snap.text.trim() : "";
375
+ const raw = sexpr(rawOutcome(snap));
376
+ return `${name} ${args} — ${own !== "" ? own : raw}; raw ${raw}`;
377
+ }
378
+ return `${name} ${args} — raw ${sexpr(rawOutcome(snap))}`;
379
+ }
380
+
381
+ /**
382
+ * The failing call is not a Scheme result, so name it: arguments, the tool's own
383
+ * error, and the raw outcomes of the other calls this statement already made.
384
+ */
385
+ function toolFailureReport(
386
+ job: ProgrammaticJob,
387
+ failed: ToolCallJob,
388
+ snap: { readonly text?: string; readonly value?: unknown; readonly code?: string },
389
+ formAt: number,
390
+ ): string {
391
+ const name = failed.name.trim() === "" ? "tool" : failed.name.trim();
392
+ const own = typeof snap.text === "string" ? snap.text.trim() : "";
393
+ const raw = sexpr(rawOutcome(snap));
394
+ const detail = own !== "" ? own : raw;
395
+ const siblings = job.toolCalls.slice(formAt).filter((inner) => inner !== failed && inner.completed);
396
+ const lines = [`${name} ${sexpr(failed.args ?? {})} failed: ${detail}`, `raw ${raw}`];
397
+ if (siblings.length > 0) {
398
+ lines.push(
399
+ "Other tool calls in this statement already settled. Their raw results are not part of the statement's value:",
400
+ ...siblings.map((inner) => rawCallLine(inner)),
401
+ );
402
+ }
403
+ return lines.join("\n");
404
+ }
405
+
406
+ function resultFromSnap(job: ProgrammaticJob, inner: ToolCallJob, formAt: number): unknown {
407
+ const snap = inner.outcomeSnap;
408
+ invariant(snap !== undefined, `${inner.name} sealed without an outcome`);
409
+ if (!snap.ok) {
410
+ throw new SchemeReplToolFailure(toolFailureReport(job, inner, snap, formAt), snap.code);
411
+ }
412
+ const out: unknown = "value" in snap ? snap.value : typeof snap.text === "string" ? snap.text : undefined;
413
+ if (!Array.isArray(out)) return out;
414
+ const max = DEFAULT_SCHEME_REPL_LIMITS.maxAttachments;
415
+ let seen = 0;
416
+ return out.map((block) => {
417
+ if (block === null || typeof block !== "object" || Array.isArray(block)) return block;
418
+ const type = "type" in block ? block.type : undefined;
419
+ if (type !== "image" && type !== "audio" && type !== "resource") return block;
420
+ seen += 1;
421
+ if (seen <= max) return block;
422
+ return { type: "text", text: `[attachment omitted: quota ${max}]` };
423
+ });
424
+ }
425
+
426
+ function readToolResult(job: ProgrammaticJob, inner: ToolCallJob, rig: ReplRig, formAt: number): unknown {
427
+ try {
428
+ return resultFromSnap(job, inner, formAt);
429
+ } catch (error) {
430
+ const code = failureCodeOf(error);
431
+ if (code != null) rig.failureCode = code;
432
+ throw error;
433
+ }
434
+ }
435
+
436
+ /** Abort-win leaves `inner` to seal on its own. */
437
+ async function joinInner(inner: ToolCallJob, signal: AbortSignal): Promise<"settled" | "aborted"> {
438
+ if (signal.aborted || inner.cancelled) return "aborted";
439
+ if (inner.completed) return "settled";
440
+ let onAbort: (() => void) | undefined;
441
+ const aborted = new Promise<"aborted">((resolve) => {
442
+ onAbort = () => resolve("aborted");
443
+ signal.addEventListener("abort", onAbort, { once: true });
444
+ });
445
+ const joined = when(() => inner.completed || inner.cancelled);
446
+ try {
447
+ const result = await Promise.race([
448
+ joined.then(
449
+ () => "settled" as const,
450
+ () => "aborted" as const,
451
+ ),
452
+ aborted,
453
+ ]);
454
+ if (result === "aborted" || inner.cancelled) return "aborted";
455
+ return "settled";
456
+ } finally {
457
+ joined.cancel();
458
+ if (onAbort !== undefined) signal.removeEventListener("abort", onAbort);
459
+ }
460
+ }
461
+
462
+ function resolveToolName(
463
+ host: SchemeReplHost,
464
+ tools: ReadonlyMap<string, BoundTool>,
465
+ connection: string,
466
+ tool: string,
467
+ ): string {
468
+ const catalog = host.catalog;
469
+ invariant(catalog != null, "scheme-repl: catalog is not configured");
470
+ for (const entry of catalog.catalogEntries()) {
471
+ if (entry.source.kind !== "connection") continue;
472
+ if (!tools.has(entry.wireName)) continue;
473
+ if (entry.source.connectionKey === connection && entry.source.serverToolName === tool) {
474
+ return entry.wireName;
475
+ }
476
+ }
477
+ const attempted = `${connection}/${tool}`;
478
+ throw new Error(
479
+ host.doors.render(unknownToolDoor(attempted, [...tools.keys()], tools, SCHEME_REPL_NAMING), attempted),
480
+ );
481
+ }
482
+
483
+ async function callTool(
484
+ job: ProgrammaticJob,
485
+ host: SchemeReplHost,
486
+ rig: ReplRig,
487
+ tools: ReadonlyMap<string, BoundTool>,
488
+ name: string,
489
+ toolArgs: Record<string, unknown>,
490
+ cross: Crossing,
491
+ ctx: CallCtx,
492
+ ): Promise<unknown> {
493
+ const { signal } = rig;
494
+ if (signal.aborted || job.cancelled) throw abortThrow(signal);
495
+ invariant(
496
+ name !== SCHEME_REPL_TOOL_NAME && name !== SCHEME_WATCH_TOOL_NAME,
497
+ `${name} cannot be invoked inside scheme_repl`,
498
+ );
499
+ // Replay matches the recorded child by track. The wire name was resolved when it was minted.
500
+ if (!host.replayOnly) {
501
+ const catalog = host.catalog;
502
+ invariant(catalog != null, "scheme-repl: catalog is not configured");
503
+ const entry = catalog.catalogEntry(name);
504
+ if (entry != null && entry.packMeta?.kind === "bot-call") throw new AutonomousToolError(name);
505
+ invariant(entry !== undefined && tools.has(name), () =>
506
+ host.doors.render(unknownToolDoor(name, [...tools.keys()], tools, SCHEME_REPL_NAMING), name),
507
+ );
508
+ }
509
+ return joinMinted(job, host, rig, name, cross, ctx, () => {
510
+ const minted = host.admit({
511
+ id: innerToolCallId(job),
512
+ type: "function",
513
+ function: { name, arguments: JSON.stringify(toolArgs) },
514
+ });
515
+ invariant(minted instanceof ToolCallJob, `${name} cannot be invoked inside scheme_repl`);
516
+ job.registerJob(minted);
517
+ return minted;
518
+ });
519
+ }
520
+
521
+ function innerToolCallId(job: ProgrammaticJob): string {
522
+ return `${job.toolCallId === "" ? "program" : job.toolCallId}:inner:${job.toolCalls.length + 1}`;
523
+ }
524
+
525
+ /**
526
+ * What the program's graph learns of a crossing. Taken while the verb is still on
527
+ * its own stack, where `currentInvocation` is the call; stamped once the inner job
528
+ * is known, which is before the call is awaited, so a running call is already a leaf.
529
+ */
530
+ type Crossing = (inner: ToolCallJob) => void;
531
+
532
+ function stringList(text: string): string[] {
533
+ const parsed: unknown = JSON.parse(text);
534
+ if (!Array.isArray(parsed)) throw new Error("replay: connection/models record is not a list of strings");
535
+ const ids: string[] = [];
536
+ for (const item of parsed) {
537
+ if (typeof item !== "string") throw new Error("replay: connection/models record is not a list of strings");
538
+ ids.push(item);
539
+ }
540
+ return ids;
541
+ }
542
+
543
+ /** A card shows a line or two; the whole text stays on the inner job, so the feed carries a glance. */
544
+ const GLANCE_CHARS = 280;
545
+ const glance = (value: unknown): unknown =>
546
+ typeof value === "string" && value.length > GLANCE_CHARS ? `${value.slice(0, GLANCE_CHARS)}…` : value;
547
+
548
+ function crossing(ctx: ReplActivation, full: Omit<WorkflowLeafMeta, "jobId">): Crossing {
549
+ const meta = {
550
+ ...full,
551
+ ...(full.message === undefined ? {} : { message: glance(full.message) as string }),
552
+ ...(full.args === undefined
553
+ ? {}
554
+ : { args: Object.fromEntries(Object.entries(full.args).map(([key, arg]) => [key, glance(arg)])) }),
555
+ };
556
+ const inv = ctx.invocation?.currentInvocation;
557
+ return (inner) => {
558
+ if (inv === undefined) return;
559
+ inv.markProvenancePoint?.();
560
+ // A job outside any document (a host test's) has no uuid yet; the card just lacks its link.
561
+ let jobId: string | undefined;
562
+ try {
563
+ jobId = inner.uuid;
564
+ } catch {
565
+ /* unmaterialized */
566
+ }
567
+ inv.setMetadata?.({ ...meta, ...(jobId === undefined ? {} : { jobId }) } satisfies WorkflowLeafMeta);
568
+ };
569
+ }
570
+
571
+ function budgetDoor(host: SchemeReplHost, rig: ReplRig, job: ProgrammaticJob): void {
572
+ if (rig.maxInternalCalls == null || job.toolCalls.length - rig.origin < rig.maxInternalCalls) return;
573
+ const tool = job.toolName === "" ? SCHEME_REPL_TOOL_NAME : job.toolName;
574
+ throw new Error(host.doors.render(internalCallBudgetDoor(rig.maxInternalCalls, tool), tool));
575
+ }
576
+
577
+ async function finishJoined(
578
+ job: ProgrammaticJob,
579
+ rig: ReplRig,
580
+ formAt: number,
581
+ inner: ToolCallJob,
582
+ cross: Crossing,
583
+ ): Promise<unknown> {
584
+ cross(inner);
585
+ if (!inner.completed && (await joinInner(inner, rig.signal)) === "aborted") throw abortThrow(rig.signal);
586
+ return readToolResult(job, inner, rig, formAt);
587
+ }
588
+
589
+ /**
590
+ * A tracked run claims its path before the wait, then takes the child stamped with it.
591
+ * A job from before tracks still walks `toolCalls` by position and name.
592
+ * `mint` parents the job.
593
+ */
594
+ async function joinMinted(
595
+ job: ProgrammaticJob,
596
+ host: SchemeReplHost,
597
+ rig: ReplRig,
598
+ name: string,
599
+ cross: Crossing,
600
+ ctx: CallCtx,
601
+ mint: () => ToolCallJob,
602
+ ): Promise<unknown> {
603
+ const { signal } = rig;
604
+ if (signal.aborted || job.cancelled) throw abortThrow(signal);
605
+ const formAt = rig.formAt;
606
+ const emission = ctx.runCtx.emission;
607
+ if (job.trackMatch && emission != null) {
608
+ const path = claimCrossing(emission, crossingWindow(ctx));
609
+ let replay = takeCrossing(job, emission, path);
610
+ if (replay == null && (host.replayOnly === true || isReplaying(emission))) {
611
+ if (!job.completed && !job.cancelled && !signal.aborted) {
612
+ const joined = when(
613
+ () => job.completed || job.cancelled || signal.aborted || crossingPending(job, emission, path),
614
+ );
615
+ try {
616
+ await joined;
617
+ } finally {
618
+ joined.cancel();
619
+ }
620
+ }
621
+ if (signal.aborted || job.cancelled) throw abortThrow(signal);
622
+ replay = takeCrossing(job, emission, path);
623
+ if (replay == null) throw new Error(`replay: no recorded ${name}`);
624
+ }
625
+ if (replay != null) return finishJoined(job, rig, formAt, replay, cross);
626
+ budgetDoor(host, rig, job);
627
+ const minted = mint();
628
+ runInAction(() => (minted.track = path));
629
+ rig.replayAt = job.toolCalls.length;
630
+ return finishJoined(job, rig, formAt, minted, cross);
631
+ }
632
+ const replay = job.toolCalls[rig.replayAt];
633
+ if (replay != null && replay.name === name) {
634
+ rig.replayAt += 1;
635
+ return finishJoined(job, rig, formAt, replay, cross);
636
+ }
637
+ budgetDoor(host, rig, job);
638
+ const minted = mint();
639
+ rig.replayAt = job.toolCalls.length;
640
+ return finishJoined(job, rig, formAt, minted, cross);
641
+ }
642
+
643
+ /**
644
+ * Standing bot when `standing` and the spec is non-blank; otherwise an owned child.
645
+ * Args stay the message bag: spec, message, and `:model`. Ask flags live on the job.
646
+ */
647
+ function spawnCall(
648
+ ctx: CallCtx,
649
+ name: typeof AGENT_SEND | typeof AGENT_ASK,
650
+ standing: boolean,
651
+ spec: string | undefined,
652
+ message: string,
653
+ flags: {
654
+ /** The verb the program wrote; `name` is the wire it lowers to. */
655
+ readonly verb: string;
656
+ readonly deskReadonly?: boolean;
657
+ readonly schema?: Record<string, unknown>;
658
+ readonly model?: string;
659
+ },
660
+ ): Promise<unknown> {
661
+ const { job, host, rig } = replBag(ctx);
662
+ const raw = spec ?? "";
663
+ const trimmed = raw.trim();
664
+ const args: Record<string, unknown> = {};
665
+ if (trimmed !== "") args.spec = raw;
666
+ args.message = message;
667
+ const model = flags.model?.trim() ?? "";
668
+ if (model !== "") args.model = model;
669
+ const props = {
670
+ name,
671
+ argsJson: JSON.stringify(args),
672
+ voidLegal: name === AGENT_SEND,
673
+ readonly: flags.deskReadonly === true,
674
+ responseSchema: flags.schema ?? null,
675
+ };
676
+ const bot = standing && trimmed !== "";
677
+ const cross = crossing(ctx, {
678
+ kind: bot ? "bot" : "agent",
679
+ verb: flags.verb,
680
+ name: trimmed,
681
+ ...(model === "" ? {} : { model }),
682
+ message,
683
+ ...(job.background ? { background: true } : {}),
684
+ });
685
+ return joinMinted(job, host, rig, name, cross, ctx, () => {
686
+ const toolCallId = innerToolCallId(job);
687
+ const minted = bot
688
+ ? new BotCallJob({ ...props, toolCallId, botName: trimmed })
689
+ : new AgentCallJob({ ...props, toolCallId });
690
+ job.registerJob(minted);
691
+ return minted;
692
+ });
693
+ }
694
+
695
+ /** `verb` is what the program wrote when that is not the wire name (`mcp/call`). */
696
+ function call(ctx: CallCtx, name: string, args: Record<string, unknown>, verb = name): Promise<unknown> {
697
+ const { job, host, rig, tools } = replBag(ctx);
698
+ const cross = crossing(ctx, { kind: "tool", verb, name, args, ...(job.background ? { background: true } : {}) });
699
+ return callTool(job, host, rig, tools, name, args, cross, ctx);
700
+ }
701
+
702
+ export const schemeReplCapability = EnvCapability.define("harness/scheme-repl", {
703
+ /** Arrival s/* tagged lists. Ask lowers them; do not mint a second type wire. */
704
+ deps: [schemaCapability],
705
+ configuration: replConfiguration,
706
+ symbols: (symbol, sz) => ({
707
+ read_file: symbol.native`read_file: read a UTF-8 text file under the project root`(
708
+ {
709
+ input: [sz.string],
710
+ inputRest: { offset: sz.integer.optional(), limit: sz.integer.optional() },
711
+ output: [sz.string],
712
+ provenance: "source",
713
+ requiresConfig: ["host"],
714
+ },
715
+ async function (path, ...rest: unknown[]) {
716
+ invariant(path !== undefined, "read_file: path is required");
717
+ const pathText = stringOf(path);
718
+ // native contour does not fold record rest into a kwargs object.
719
+ const raw = await call(this, "read_file", argsOf({ path: pathText, ...restKwargs(rest, "read_file") }));
720
+ return stampFilePath(new AString(typeof raw === "string" ? raw : String(raw ?? "")), pathText);
721
+ },
722
+ ),
723
+
724
+ [UPDATE_METADATA_TOOL_NAME]:
725
+ symbol.rosetta`update_metadata: record this turn's session-list title and an optional description`(
726
+ {
727
+ input: [],
728
+ inputRest: sz.dynamic,
729
+ output: [sz.dynamic],
730
+ provenance: "source",
731
+ requiresConfig: ["host"],
732
+ },
733
+ function (...slots: unknown[]) {
734
+ return call(this, UPDATE_METADATA_TOOL_NAME, metadataArgs(slots));
735
+ },
736
+ ),
737
+
738
+ list_dir: symbol.rosetta`list_dir: list a directory under the project root`(
739
+ {
740
+ input: [sz.string],
741
+ inputRest: sz.dynamic,
742
+ output: [sz.list(sz.string)],
743
+ provenance: "source",
744
+ requiresConfig: ["host"],
745
+ },
746
+ function (path, ...rest: unknown[]) {
747
+ const { depth } = keywordsOf("list_dir", rest, { depth: sz.integer.optional() });
748
+ return call(this, "list_dir", argsOf({ path, depth })) as Promise<string[]>;
749
+ },
750
+ ),
751
+
752
+ grep: symbol.rosetta`grep: search file contents under the project root`(
753
+ {
754
+ input: [sz.string],
755
+ inputRest: sz.dynamic,
756
+ output: [sz.list(sz.string)],
757
+ provenance: "source",
758
+ requiresConfig: ["host"],
759
+ },
760
+ function (pattern, ...rest: unknown[]) {
761
+ const kwargs = keywordsOf("grep", rest, {
762
+ path: sz.string.optional(),
763
+ glob: sz.string.optional(),
764
+ mode: sz.string.optional(),
765
+ limit: sz.integer.optional(),
766
+ before: sz.integer.optional(),
767
+ after: sz.integer.optional(),
768
+ });
769
+ return call(this, "grep", argsOf({ pattern, ...kwargs })) as Promise<string[]>;
770
+ },
771
+ ),
772
+
773
+ glob: symbol.rosetta`glob: find paths under the project root matching a glob`(
774
+ {
775
+ input: [
776
+ sz.string,
777
+ sz.dict({ cwd: sz.string.optional(), path: sz.string.optional(), limit: sz.integer.optional() }).optional(),
778
+ ],
779
+ output: [sz.list(sz.string)],
780
+ provenance: "source",
781
+ requiresConfig: ["host"],
782
+ },
783
+ function (pattern, opts) {
784
+ return call(this, "glob", argsOf({ pattern, ...opts })) as Promise<string[]>;
785
+ },
786
+ ),
787
+
788
+ fff_search: symbol.alias`glob`,
789
+
790
+ write_file: symbol.rosetta`write_file: create a file, or replace one with overwriteExisting`(
791
+ {
792
+ input: [sz.string, sz.string],
793
+ inputRest: sz.dynamic,
794
+ output: [sz.string],
795
+ provenance: "source",
796
+ queries: (path: string) => fsPath(path),
797
+ effects: (path: string) => fsPath(path),
798
+ requiresConfig: ["host", "writable"],
799
+ },
800
+ function (path, content, ...rest: unknown[]) {
801
+ const { overwriteExisting } = keywordsOf("write_file", rest, {
802
+ overwriteExisting: sz.boolean.optional(),
803
+ });
804
+ return call(this, "write_file", argsOf({ path, content, overwriteExisting })) as Promise<string>;
805
+ },
806
+ ),
807
+
808
+ edit_file: symbol.rosetta`edit_file: replace an exact stretch of text in a file`(
809
+ {
810
+ input: [sz.string, sz.string, sz.string],
811
+ inputRest: sz.dynamic,
812
+ output: [sz.string],
813
+ provenance: "source",
814
+ queries: (path: string) => fsPath(path),
815
+ effects: (path: string) => fsPath(path),
816
+ requiresConfig: ["host", "writable"],
817
+ },
818
+ function (path, old_string, new_string, ...rest: unknown[]) {
819
+ const { replace_all } = keywordsOf("edit_file", rest, { replace_all: sz.boolean.optional() });
820
+ return call(this, "edit_file", argsOf({ path, old_string, new_string, replace_all })) as Promise<string>;
821
+ },
822
+ ),
823
+
824
+ [SHELL_TOOL_NAME]: symbol.rosetta`shell: run a command in the project workspace`(
825
+ {
826
+ input: [sz.string],
827
+ inputRest: sz.dynamic,
828
+ output: [sz.box],
829
+ provenance: "source",
830
+ queries: (_command: string, ...rest: unknown[]) => [["shell", shellCwd(rest)]],
831
+ effects: (_command: string, ...rest: unknown[]) => [["shell", shellCwd(rest)]],
832
+ requiresConfig: ["host"],
833
+ },
834
+ function (command, ...rest: unknown[]) {
835
+ const kwargs = keywordsOf(SHELL_TOOL_NAME, rest, {
836
+ cwd: sz.string.optional(),
837
+ timeoutMs: sz.integer.optional(),
838
+ });
839
+ return call(this, SHELL_TOOL_NAME, argsOf({ command, ...kwargs })) as Promise<object>;
840
+ },
841
+ ),
842
+
843
+ [WEB_SEARCH_TOOL_NAME]: symbol.rosetta`web_search: search the public web`(
844
+ {
845
+ input: [sz.string],
846
+ inputRest: sz.dynamic,
847
+ output: [sz.dynamic],
848
+ provenance: "source",
849
+ requiresConfig: ["host"],
850
+ },
851
+ function (query, ...rest: unknown[]) {
852
+ const { numResults } = keywordsOf(WEB_SEARCH_TOOL_NAME, rest, { numResults: sz.integer.optional() });
853
+ return call(this, WEB_SEARCH_TOOL_NAME, argsOf({ query, numResults }));
854
+ },
855
+ ),
856
+
857
+ [WEB_EXTRACT_TOOL_NAME]: symbol.rosetta`web_extract: fetch a public URL as markdown`(
858
+ {
859
+ input: [sz.string],
860
+ inputRest: sz.dynamic,
861
+ output: [sz.dynamic],
862
+ provenance: "source",
863
+ requiresConfig: ["host"],
864
+ },
865
+ function (url, ...rest: unknown[]) {
866
+ const { charLimit } = keywordsOf(WEB_EXTRACT_TOOL_NAME, rest, { charLimit: sz.integer.optional() });
867
+ return call(this, WEB_EXTRACT_TOOL_NAME, argsOf({ url, charLimit }));
868
+ },
869
+ ),
870
+
871
+ "mcp/call": symbol.rosetta`mcp/call: invoke a live MCP tool; connection and tool are data`(
872
+ {
873
+ input: [sz.foldName, sz.foldName],
874
+ inputRest: sz.dynamic,
875
+ output: [sz.dynamic],
876
+ provenance: "source",
877
+ requiresConfig: ["host"],
878
+ },
879
+ function (connection: string, tool: string, ...rest: unknown[]) {
880
+ const { host, tools } = replBag(this);
881
+ const args = restKwargs(rest, "mcp/call");
882
+ if (host.replayOnly === true) return call(this, "mcp/call", args, "mcp/call");
883
+ return call(this, resolveToolName(host, tools, connection, tool), args, "mcp/call");
884
+ },
885
+ ),
886
+
887
+ "bot/send": symbol.rosetta`bot/send: wait for a standing bot to respond or finish without a response`(
888
+ {
889
+ input: [sz.foldName, sz.string],
890
+ output: [sz.string],
891
+ provenance: "source",
892
+ queries: (spec: string) => [["agent", spec]],
893
+ requiresConfig: ["host"],
894
+ },
895
+ function (spec: string, message: string) {
896
+ return spawnCall(this, AGENT_SEND, true, spec, message, { verb: "bot/send" }) as Promise<string>;
897
+ },
898
+ ),
899
+
900
+ "bot/ask": symbol.rosetta`bot/ask: wait for a standing bot's response; finishing without a response is an error`(
901
+ {
902
+ input: [sz.dynamic],
903
+ inputRest: sz.dynamic,
904
+ output: [sz.dynamic],
905
+ provenance: "source",
906
+ requiresConfig: ["host"],
907
+ },
908
+ function (...slots) {
909
+ if (isKeyword(slots[0])) throw new Error("bot/ask: a message comes before keywords");
910
+ const { positionals, keywords } = splitKeywords("bot/ask", slots, { readonly: sz.boolean.optional() });
911
+ if (positionals.length > 3) throw new Error("bot/ask: at most spec, message, and schema before keywords");
912
+ const [specSlot, messageSlot, schemaSlot] = positionals;
913
+ const { readonly } = keywords;
914
+ const hasSpec = schemaSlot !== undefined || isStringLike(messageSlot);
915
+ const spec = hasSpec ? stringOf(specSlot) : undefined;
916
+ const message = stringOf(hasSpec ? messageSlot : specSlot);
917
+ const schema = hasSpec ? schemaSlot : messageSlot;
918
+ return spawnCall(this, AGENT_ASK, true, spec, message, {
919
+ verb: "bot/ask",
920
+ deskReadonly: readonly === true,
921
+ schema: compiledAskSchema(schema),
922
+ });
923
+ },
924
+ ),
925
+
926
+ "agent/call":
927
+ symbol.rosetta`agent/call: create a disposable worker and wait for it to finish, with or without a response`(
928
+ {
929
+ input: [sz.dynamic, sz.string],
930
+ inputRest: sz.dynamic,
931
+ output: [sz.string],
932
+ provenance: "source",
933
+ requiresConfig: ["host"],
934
+ },
935
+ function (slot, message, ...rest) {
936
+ const spec = workerSpec("agent/call", slot);
937
+ const { model } = keywordsOf("agent/call", rest, { model: sz.string.optional() });
938
+ return spawnCall(this, AGENT_SEND, false, spec, stringOf(message), {
939
+ verb: "agent/call",
940
+ model,
941
+ }) as Promise<string>;
942
+ },
943
+ ),
944
+
945
+ "agent/ask": symbol.rosetta`agent/ask: create a disposable worker and wait for its required response`(
946
+ {
947
+ input: [sz.dynamic, sz.string],
948
+ inputRest: sz.dynamic,
949
+ output: [sz.dynamic],
950
+ provenance: "source",
951
+ requiresConfig: ["host"],
952
+ },
953
+ function (slot, message, ...rest) {
954
+ const spec = workerSpec("agent/ask", slot);
955
+ const { positionals, keywords } = splitKeywords("agent/ask", rest, {
956
+ readonly: sz.boolean.optional(),
957
+ model: sz.string.optional(),
958
+ });
959
+ if (positionals.length > 1) throw new Error("agent/ask: at most a schema after the message");
960
+ const schemaSlot = positionals[0];
961
+ const { readonly, model } = keywords;
962
+ return spawnCall(this, AGENT_ASK, false, spec, stringOf(message), {
963
+ verb: "agent/ask",
964
+ deskReadonly: readonly === true,
965
+ schema: compiledAskSchema(schemaSlot),
966
+ model,
967
+ });
968
+ },
969
+ ),
970
+
971
+ "connection/models": symbol.rosetta`connection/models: live model ids on a completion connection`(
972
+ {
973
+ input: [sz.foldName],
974
+ output: [sz.list(sz.string)],
975
+ provenance: "source",
976
+ requiresConfig: ["host"],
977
+ },
978
+ function (connection: string) {
979
+ const { job, host } = replBag(this);
980
+ const emission = this.runCtx.emission;
981
+ if (job.trackMatch && emission != null) {
982
+ const path = claimCrossing(emission, crossingWindow(this));
983
+ const recorded = job.world[path];
984
+ if (recorded !== undefined) return stringList(recorded);
985
+ if (host.replayOnly === true || isReplaying(emission)) {
986
+ throw new Error("replay: connection/models was not recorded");
987
+ }
988
+ const list = host.connectionModels;
989
+ invariant(list != null, "connection/models: host is not configured");
990
+ const ids = [...list(stringOf(connection))];
991
+ runInAction(() => (job.world[path] = JSON.stringify(ids)));
992
+ return ids;
993
+ }
994
+ const list = host.connectionModels;
995
+ invariant(list != null, "connection/models: host is not configured");
996
+ return [...list(stringOf(connection))];
997
+ },
998
+ ),
999
+
1000
+ "agent/message": symbol.rosetta`agent/message: deliver a message; does not wait for a reply`(
1001
+ {
1002
+ input: [sz.foldName, sz.string],
1003
+ output: [sz.dynamic],
1004
+ provenance: "source",
1005
+ requiresConfig: ["host"],
1006
+ },
1007
+ function (target, message) {
1008
+ const id = UUID_RE.test(target);
1009
+ return call(this, AGENT_MESSAGE, argsOf(id ? { targetId: target, message } : { targetName: target, message }));
1010
+ },
1011
+ ),
1012
+
1013
+ "agent/respond": symbol.notImplemented`agent/respond: close an open worker call with the agent__respond JSON tool — this Scheme symbol is not invocable`,
1014
+
1015
+ "agent/void": symbol.rosetta`agent/void: complete a worker request without a response message`(
1016
+ {
1017
+ input: [sz.string],
1018
+ output: [sz.dynamic],
1019
+ provenance: "source",
1020
+ requiresConfig: ["host"],
1021
+ },
1022
+ function (requestId) {
1023
+ return call(this, AGENT_VOID, argsOf({ requestId }));
1024
+ },
1025
+ ),
1026
+
1027
+ "agent/notify": symbol.rosetta`agent/notify: send an update without completing the worker request`(
1028
+ {
1029
+ input: [sz.string, sz.string],
1030
+ output: [sz.dynamic],
1031
+ provenance: "source",
1032
+ requiresConfig: ["host"],
1033
+ },
1034
+ function (requestId, message) {
1035
+ return call(this, AGENT_NOTIFY, argsOf({ requestId, message }));
1036
+ },
1037
+ ),
1038
+
1039
+ "self/notify":
1040
+ symbol.rosetta`self/notify: send an update from this program or worker request without completing it`(
1041
+ {
1042
+ input: [sz.string],
1043
+ output: [sz.undefinedResult],
1044
+ provenance: "source",
1045
+ requiresConfig: ["host"],
1046
+ },
1047
+ function (message) {
1048
+ const { job, host } = replBag(this);
1049
+ if (host.replayOnly === true) return undefined;
1050
+ appendNotify(job, host, stringOf(message));
1051
+ return undefined;
1052
+ },
1053
+ ),
1054
+ }),
1055
+ });
1056
+
1057
+ function extraToolNames(tools: ReadonlyMap<string, BoundTool>): string[] {
1058
+ const skip = new Set<string>([UPDATE_METADATA_TOOL_NAME]);
1059
+ const own = schemeReplCapability.spec.symbols;
1060
+ if (own != null && typeof own !== "function") for (const name of Object.keys(own)) skip.add(name);
1061
+ const schemaSymbols = schemaCapability.spec.symbols;
1062
+ if (schemaSymbols != null && typeof schemaSymbols !== "function") {
1063
+ for (const name of Object.keys(schemaSymbols)) skip.add(name);
1064
+ }
1065
+ const names: string[] = [];
1066
+ for (const name of tools.keys()) if (!skip.has(name)) names.push(name);
1067
+ return names;
1068
+ }
1069
+
1070
+ function deskTools(host: SchemeReplHost): ReadonlyMap<string, BoundTool> {
1071
+ if (host.replayTools != null) return host.replayTools;
1072
+ const catalog = host.catalog;
1073
+ invariant(catalog != null, "scheme-repl: catalog is not configured");
1074
+ return boundToolsOf(catalog, { readonly: host.readonly === true });
1075
+ }
1076
+
1077
+ /** Pack tools bind as their own symbols for this eval. MCP stays on mcp/call. */
1078
+ function packToolCapability(tools: ReadonlyMap<string, BoundTool>): EnvCapability | undefined {
1079
+ const names = extraToolNames(tools);
1080
+ if (names.length === 0) return undefined;
1081
+ return EnvCapability.define("harness/scheme-repl-pack", {
1082
+ configuration: replConfiguration,
1083
+ symbols: (symbol, sz) => {
1084
+ const jsonDict = sz.dictRecord(sz.foldName, sz.dynamic).optional();
1085
+ const verbs: Record<string, SymbolDeclaration> = {};
1086
+ for (const name of names) {
1087
+ verbs[name] = symbol.rosetta`${name}: pack tool`(
1088
+ {
1089
+ input: [jsonDict],
1090
+ output: [sz.dynamic],
1091
+ provenance: "source",
1092
+ requiresConfig: ["host"],
1093
+ },
1094
+ function (args?: Record<string, unknown>) {
1095
+ return call(this, name, jsRecord(args));
1096
+ },
1097
+ );
1098
+ }
1099
+ return verbs;
1100
+ },
1101
+ });
1102
+ }
1103
+
1104
+ function vocabulary(
1105
+ host: SchemeReplHost,
1106
+ moduleRoot: string | undefined,
1107
+ job: ProgrammaticJob,
1108
+ ): {
1109
+ readonly capabilities: readonly EnvCapability[];
1110
+ readonly config: object;
1111
+ } {
1112
+ const tools = deskTools(host);
1113
+ const pack = packToolCapability(tools);
1114
+ if (host.replayOnly !== true) {
1115
+ const names = extraToolNames(tools);
1116
+ runInAction(() => {
1117
+ job.trackMatch = true;
1118
+ for (const name of names) job.boundTools[name] = "";
1119
+ });
1120
+ }
1121
+ const loader =
1122
+ moduleRoot == null
1123
+ ? undefined
1124
+ : host.replayOnly === true || host.loadSource == null
1125
+ ? recordedModuleLoader(job)
1126
+ : host.loadSource(moduleRoot, job);
1127
+ return {
1128
+ capabilities: [
1129
+ schemeReplCapability,
1130
+ ...(pack == null ? [] : [pack]),
1131
+ ...(moduleRoot == null ? [] : [arrivalYamlCapability, promptProgramCapability, schemeLoadAliasCapability]),
1132
+ ],
1133
+ config: {
1134
+ ...(host.readonly === true ? {} : { writable: true }),
1135
+ host,
1136
+ tools,
1137
+ ...(host.keyOf === undefined ? {} : { keyOf: host.keyOf }),
1138
+ ...(loader == null
1139
+ ? {}
1140
+ : {
1141
+ loader,
1142
+ dirname: "",
1143
+ connectionId: host.connectionId,
1144
+ completionModel: host.completionModel,
1145
+ complete: host.complete,
1146
+ readonly: host.readonly === true,
1147
+ }),
1148
+ },
1149
+ };
1150
+ }
1151
+
1152
+ /** Identity is the host object, so a park remint still compares SchemeReplHost identity. */
1153
+ export function programHostOf(host: SchemeReplHost): ProgramHost {
1154
+ return {
1155
+ identity: host,
1156
+ signal: host.signal,
1157
+ doors: host.doors,
1158
+ root: host.root,
1159
+ abort: host.abort,
1160
+ notify: host.notify,
1161
+ evalProgram: host.evalProgram,
1162
+ moduleRoot: host.moduleRoot,
1163
+ observe: host.observe,
1164
+ replayOnly: host.replayOnly,
1165
+ tools: () => deskTools(host),
1166
+
1167
+ prepare(opts) {
1168
+ return vocabulary(host, opts.moduleRoot, opts.job);
1169
+ },
1170
+ };
1171
+ }
1172
+
1173
+ export function replRuntimeOf(job: ProgrammaticJob, host: SchemeReplHost): ReplRuntime {
1174
+ return programRuntimeOf(job, programHostOf(host));
1175
+ }
1176
+
1177
+ /** R7RS `load` is a host door. Last-write-wins over it; expands to `require`. */
1178
+ export const schemeLoadAliasCapability = EnvCapability.define("harness/scheme-repl-load", {
1179
+ deps: [arrivalLoaderCapability],
1180
+ symbols: (symbol) => ({
1181
+ load: symbol.defineSyntax`load: alias of require; spills .scm defines into this run`(
1182
+ "(lambda (specifier) `(require ,specifier))",
1183
+ ),
1184
+ }),
1185
+ });