@inhuman.tools/harness-scheme 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.
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
@@ -0,0 +1,150 @@
1
+ /**
2
+ * Live `scheme_repl` tools[] description.
3
+ *
4
+ * Manifold law (arrival-manifold `buildCatalog`): world beat + derived slug
5
+ * inventory + one signature line per bound tool. A cold model that never
6
+ * calls cannot be taught by a door. Inventory names only bound slugs —
7
+ * never capability kinds ("files", "repositories").
8
+ *
9
+ * Not the manifold renderer: kwargs `(server/tool :k v)` and "this is the
10
+ * only tool" are the MCP-proxy world. Signatures come from `boundToolsOf`
11
+ * (positional toolkit, pack symbols, `mcp/call`, `agent/*`).
12
+ *
13
+ * Completion faces: <12 live ids enumerate (prefix on the first only).
14
+ * 12+ list operator recency, then "and N more".
15
+ */
16
+ import { SCHEME_REPL_NAMING } from "@inhuman.tools/harness-model";
17
+ import { type CatalogRecipe, type KernelToolCatalog } from "@inhuman.tools/turn/catalog";
18
+ import dedent from "dedent";
19
+
20
+ import { boundToolsOf } from "./bound-tools.js";
21
+ import type { SchemeReplFaces } from "./connection-models.js";
22
+
23
+ const HOST_SLUGS = new Set(["toolkit", "shell", "named-agents", "web-search"]);
24
+ const MODEL_INVENTORY_ENUMERATE_MAX = 12;
25
+
26
+ function namedAgentInventory(recipes: readonly CatalogRecipe[]): string {
27
+ const rows = recipes.filter((recipe) => recipe.spec.description.trim() !== "");
28
+ if (rows.length === 0) return "";
29
+ const width = Math.max(...rows.map((recipe) => recipe.spec.name.length));
30
+ return dedent`
31
+ Bots — (bot/ask spec message) reuses the worker for this session; (agent/ask spec message) creates a disposable worker for that request. A schema is legal on (agent/ask spec message schema …) and on omitted-spec (bot/ask message schema), not on a standing named bot/ask.
32
+ ${rows.map((recipe) => `${recipe.spec.name.padEnd(width)} ${recipe.spec.description.trim()}`).join("\n")}
33
+ (bot/ask message [schema]) omits spec for an anonymous worker this session reuses. agent/ask requires spec.
34
+ `;
35
+ }
36
+
37
+ export { SCHEME_REPL_DISPLAY_NOTE } from "@inhuman.tools/scheme-program";
38
+
39
+ export function schemeReplDescription(catalog: KernelToolCatalog, faces?: SchemeReplFaces): string {
40
+ const tools = boundToolsOf(catalog);
41
+ const seen = new Set<string>();
42
+ const slugs: string[] = [];
43
+ for (const tool of tools.values()) {
44
+ const slug = tool.slug.trim();
45
+ if (slug === "" || HOST_SLUGS.has(slug) || seen.has(slug)) continue;
46
+ seen.add(slug);
47
+ slugs.push(slug);
48
+ }
49
+ let models = "";
50
+ if (faces != null && faces.connections.length > 0) {
51
+ const usage = faces.modelsUsage;
52
+ const ordered = [...faces.connections].sort((a, b) => {
53
+ const aLarge = a.models.length >= MODEL_INVENTORY_ENUMERATE_MAX;
54
+ const bLarge = b.models.length >= MODEL_INVENTORY_ENUMERATE_MAX;
55
+ if (aLarge !== bLarge) return aLarge ? 1 : -1;
56
+ return a.id.localeCompare(b.id);
57
+ });
58
+ const lines = ordered.map((face) => {
59
+ if (face.models.length === 0) return face.id;
60
+ if (face.models.length < MODEL_INVENTORY_ENUMERATE_MAX) {
61
+ return `${face.id} — ${face.models.map((id, index) => (index === 0 ? `${face.id}/${id}` : `/${id}`)).join(", ")}`;
62
+ }
63
+ const live = new Set(face.models);
64
+ const prefix = `${face.id}/`;
65
+ const recent = Object.entries(usage)
66
+ .flatMap(([key, at]) => {
67
+ if (!key.startsWith(prefix)) return [];
68
+ const model = key.slice(prefix.length);
69
+ if (model === "" || !live.has(model)) return [];
70
+ return [{ model, at: Number(at) }];
71
+ })
72
+ .sort((a, b) => b.at - a.at || a.model.localeCompare(b.model))
73
+ .map((row) => row.model)
74
+ .slice(0, MODEL_INVENTORY_ENUMERATE_MAX);
75
+ const more = face.models.length - recent.length;
76
+ const refs = recent.map((id) => `${face.id}/${id}`).join(", ");
77
+ if (refs === "") return `${face.id} — ${face.id} and ${more} more`;
78
+ if (more <= 0) return `${face.id} — ${refs}`;
79
+ return `${face.id} — ${refs}, and ${more} more`;
80
+ });
81
+ const firstLarge = ordered.find((face) => face.models.length >= MODEL_INVENTORY_ENUMERATE_MAX);
82
+ models = [
83
+ "Available models:",
84
+ ...lines,
85
+ ...(firstLarge == null
86
+ ? []
87
+ : [
88
+ `to get the list with names of all models available for a specific connection, use (connection/models '${firstLarge.id})`,
89
+ ]),
90
+ ].join("\n");
91
+ }
92
+ return dedent`
93
+ Evaluate a sandboxed Scheme program once in \`${SCHEME_REPL_NAMING.argName}\`.
94
+ Multiple top-level forms run in one fresh scope (defines do not persist across calls).
95
+ Do not wrap them in (begin …), (list …), or [] — those wrappers are spliced and each statement is returned as its own result.
96
+ Scheme evaluation is synchronous except when waiting for tool calls.
97
+ No inner-call or time limit unless maxInternalCalls or timeoutMs is set.
98
+ Use scheme_watch only for requested recurring monitoring, not for ordinary agent calls or background work.
99
+ Autonomous host JSON tools cannot be called from Scheme.
100
+ ${
101
+ slugs.length === 0
102
+ ? undefined
103
+ : dedent`
104
+ The functions below span: ${slugs.join(", ")}.
105
+ A task that mentions resources you have not seen in this conversation may be reachable through them — explore with these functions before asking the user.
106
+ `
107
+ }
108
+ Toolkit functions take positional arguments: (read_file path), (list_dir path), (grep pattern), (glob pattern), (fff_search query), (write_file path contents), (edit_file path old new), (shell command), (web_search query), (web_extract url).
109
+ Closed toolkit optionals are Arrival kwargs after the positionals: (read_file path :offset 2 :limit 10), (list_dir path :depth 2), (grep pattern :path "src" :glob "*.ts"), (write_file path contents :overwriteExisting #t), (edit_file path old new :replace_all #t), (shell command :cwd "bin" :timeoutMs 5000), (web_search query :numResults 5), (web_extract url :charLimit 4000).
110
+ glob / fff_search take an optional trailing dict instead: (glob pattern {cwd: "src"}).
111
+ MCP: (mcp/call connection tool :k v …) or a final dict — unknown keys must survive.
112
+ Replacing an existing file needs (write_file path contents :overwriteExisting #t).
113
+ (require path) and (load path) are the same: they read a workspace file through the program loader, not read_file.
114
+ .scm spills defines into this run.
115
+ .json / .yaml return data.
116
+ .prompt returns a callable.
117
+ Relative paths jail at the project root.
118
+ Pack tools bind as their own symbols with the same positional/opts shape; the live catalog lists them.
119
+ (update_metadata title [description]) records this turn's session-list label. A keyword tail or one dict carries the same fields.
120
+ Bots: (bot/send spec message) waits until the standing bot responds or finishes without a response; the latter returns no value.
121
+ (bot/ask spec message :readonly) waits for a standing bot's response. (bot/ask message [schema] :readonly) omits spec for an anonymous worker this session reuses. A schema is legal on (agent/ask spec message schema …) and on omitted-spec (bot/ask message schema), not on a standing named bot/ask.
122
+ :readonly removes write_file, edit_file, and write-class MCP tools, but leaves shell available; it is not a guarantee against all mutations.
123
+ (agent/call spec message :model) and (agent/ask spec message [schema] :readonly :model) create a new disposable worker for each call. Spec is required; keywords follow the message, not a trailing dict. Schema is positional after the message.
124
+ :model is "connection/model" and pins that worker's completion wire.
125
+ (connection/models connection) lists live model ids on that connection.
126
+ (agent/message target message) delivers without waiting for a response.
127
+ Finish a worker request with the agent__respond JSON tool, not (agent/respond); (agent/void id) still voids a send.
128
+ Ask requires a response and rejects void.
129
+ (agent/notify id message) sends an update without completing the request.
130
+ Response schemas are Arrival s/* tags: (s/enum …), (s/string), (s/object (s/field …)), and the rest of that vocabulary.
131
+ (self/notify message) sends an update without completing execution.
132
+ Updates from a background program arrive as notifications in the conversation; when running inside a worker request, updates are attached to that request.
133
+ Results are values — nest and compose in one program instead of round-tripping.
134
+ Errors explain the expected form.
135
+ In a single execution, later top-level forms still run after an error.
136
+ Independent inner calls inside \`map\` run together; send/ask calls that reuse the same worker run sequentially on that worker.
137
+ ${namedAgentInventory(catalog.allRecipes())}
138
+ ${models}
139
+ Examples:${"" /*examples are needed to both show model that it's real scheme, and not quazi-language, and to explain on the symbol usage patterns*/}
140
+ (map (lambda (path) (agent/ask 'quick-search (string-append "who imports " path))) (glob "src/**/*.ts"))
141
+ (bot/send 'reviewer "review the diff at HEAD")
142
+ (bot/ask "ok?" (dict :answer (s/enum "yes" "no")))
143
+ (agent/ask 'explore "how does auth start" :readonly #t :model "openai/chatgpt-5.6-luna")
144
+ (mcp/call 'mail 'search :q "inbox")
145
+ (mcp/call 'mail 'search (dict :q "inbox"))
146
+ (write_file "notes.md" "# notes" :overwriteExisting #t)
147
+ (require "skill-to-program/skill-to-program.scm")
148
+
149
+ ${[...tools.values()].map((tool) => tool.signature().signatureText).join("\n")}`;
150
+ }
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Live model ids on a connection, and `connection/model` parse.
3
+ * One-shot `:model` and scheme_repl faces use the same ref.
4
+ */
5
+
6
+ import type { ConnectionSpec } from "@inhuman.tools/plexus-expectations";
7
+
8
+ export type CompletionFace = {
9
+ readonly id: string;
10
+ readonly models: readonly string[];
11
+ };
12
+
13
+ export type SchemeReplFaces = {
14
+ readonly connections: readonly CompletionFace[];
15
+ readonly modelsUsage: Readonly<Record<string, number>>;
16
+ };
17
+
18
+ export function liveModelsOf(spec: ConnectionSpec): string[] {
19
+ const raw = spec.live.models;
20
+ if (!Array.isArray(raw)) return [];
21
+ return raw.map((row) => String(row).trim()).filter((id) => id !== "");
22
+ }
23
+
24
+ export function parseModelRef(raw: string): { readonly connection: string; readonly model: string } | undefined {
25
+ const trimmed = raw.trim();
26
+ const slash = trimmed.indexOf("/");
27
+ if (slash <= 0 || slash === trimmed.length - 1) return undefined;
28
+ return { connection: trimmed.slice(0, slash), model: trimmed.slice(slash + 1) };
29
+ }
30
+
31
+ export function oneShotModelError(ref: string, connections: Record<string, ConnectionSpec | undefined>): string {
32
+ const parsed = parseModelRef(ref);
33
+ if (parsed == null) return 'model must be "connection/model"';
34
+ const spec = connections[parsed.connection];
35
+ if (spec == null) return `unknown connection ${parsed.connection}`;
36
+ if (!spec.driver.abilityClaim.includes("completion")) return `${parsed.connection} is not a completion connection`;
37
+ if (!liveModelsOf(spec).includes(parsed.model)) return `unknown model ${parsed.model} on ${parsed.connection}`;
38
+ return "";
39
+ }