char-slop/berry

multipurpose self-modifiable agentic harness

git clone https://git.t4t.associates/char-slop/berry

Charlotte Somcodemode: flatten the generated tool catalogf4dfe65

main
10.8 KiB313 linesraw
1// The harness's one direct tool: model-written javascript that calls every other tool.
2
3import type { JsonValue } from "@earendil-works/chord";
4import type { ImageContent, TextContent, TSchema } from "@earendil-works/pi-ai";
5import {
6  type CodemodeJsonSchema,
7  type CodemodeOutputItem,
8  type CodemodeResult,
9  CodemodeSandbox,
10  CodemodeSourceError,
11  parseCodemodeSource,
12  renderToolOutputType,
13  renderToolSignature,
14  toCodemodeIdentifier,
15} from "@earendil-works/pi-codemode";
16
17export const DESCRIPTION =
18  `Run raw JavaScript (not JSON, no code fence) as an async function body in a QuickJS sandbox, without Node, file system, network, or timers.
19- \`await tools.<name>(args)\` calls a tool declared in the system prompt, rejecting on failure; calls still running when the script ends are cancelled.
20- Only \`text(value)\`, \`image(imageBlock)\`, \`console.log(...)\`, and \`return value\` reach you; \`exit()\` ends the script.
21- \`store(key, value)\` and \`load(key)\` keep JSON values across calls.
22- Optional first line: \`// @options: {"max_output_tokens": 10000, "timeout_ms": 60000}\`
23- Batch independent calls (\`Promise.allSettled\`) and filter large values in one script rather than many calls.`;
24
25export type StoreWrite = { set: Record<string, JsonValue>; delete: string[] };
26
27/** How a script's `store()` writes fold into the values `load()` reads. */
28export function applyStoreWrite(
29  store: Record<string, JsonValue>,
30  write: StoreWrite,
31): Record<string, JsonValue> {
32  const next = { ...store };
33  for (const key of write.delete) delete next[key];
34  return Object.assign(next, write.set);
35}
36
37/** A tool as scripts see it. */
38export type Declared = {
39  readonly name: string;
40  readonly extension: string;
41  readonly description: string;
42  readonly parameters: TSchema;
43  readonly returns?: TSchema;
44  /** Whether the model can also call it directly, and so already has its description. */
45  readonly direct?: boolean;
46};
47
48export type NestedTool = Declared & {
49  call(args: JsonValue, options: {
50    signal: AbortSignal;
51    output(chunk: string): void;
52    progress(details: JsonValue): void;
53  }): Promise<JsonValue | undefined>;
54};
55
56/** One nested call as the ui sees it. Values, output, and errors are clipped; the model never sees any of it. */
57export type NestedCall = {
58  name: string;
59  extension: string;
60  args: JsonValue;
61  status: "running" | "ok" | "error" | "cancelled";
62  /** Wall-clock milliseconds, so a reader that comes back mid-call can still tell how long it has run. */
63  startedAt: number;
64  durationMs?: number;
65  output?: string;
66  details?: JsonValue;
67  value?: JsonValue;
68  error?: string;
69};
70
71export type CodemodeDetails = {
72  /** Wall-clock milliseconds, as a nested call's. */
73  startedAt: number;
74  /** Once the script has ended. */
75  durationMs?: number;
76  calls: NestedCall[];
77  fullOutputPath?: string;
78};
79
80const MEMORY_LIMIT_BYTES = 256 * 1024 * 1024;
81const DEFAULT_MAX_OUTPUT_TOKENS = 10_000;
82const CHARS_PER_TOKEN = 4;
83const DETAIL_CHARS = 16 * 1024;
84
85const declaration = (tool: Declared) => ({
86  name: tool.name,
87  description: tool.description,
88  inputSchema: tool.parameters as CodemodeJsonSchema,
89  outputSchema: (tool.returns ?? { type: "string" }) as CodemodeJsonSchema,
90});
91
92/** The codemode section of the system prompt: direct tools' results and codemode-only declarations. */
93export function renderCatalog(tools: readonly Declared[]): string | undefined {
94  if (tools.length === 0) return undefined;
95  const direct: string[] = [];
96  const nested: string[] = [];
97  for (const tool of tools) {
98    const declared = declaration(tool);
99    if (tool.direct) {
100      direct.push(
101        `  ${toCodemodeIdentifier(tool.name)}: ${
102          renderToolOutputType(declared.outputSchema).replaceAll("\n", "\n  ")
103        };`,
104      );
105    } else {
106      const doc = tool.description.trim().replaceAll("*/", "*\\/").replaceAll("\n", "\n   * ");
107      nested.push(
108        `${doc ? `  /** ${doc} */\n` : ""}  ${
109          renderToolSignature(declared).replaceAll("\n", "\n  ")
110        }`,
111      );
112    }
113  }
114  const blocks = [
115    ...(direct.length ? [`type DirectToolResults = {\n${direct.join("\n")}\n};`] : []),
116    ...(nested.length ? [`declare const tools: {\n${nested.join("\n")}\n};`] : []),
117  ];
118  return "Codemode tools: DirectToolResults lists direct tools callable as `await tools.<name>(args)` " +
119    "with their native arguments and the resolved return types. Additional tools are declared below.\n\n" +
120    `\`\`\`ts\n${blocks.join("\n\n")}\n\`\`\``;
121}
122
123const clip = (
124  text: string,
125) => (text.length > DETAIL_CHARS ? `${text.slice(0, DETAIL_CHARS)}…` : text);
126const messageOf = (error: unknown) => (error instanceof Error ? error.message : String(error));
127
128/**
129 * With more than one text item, each starts with a `==> text N/M <==` line, since providers join adjacent text
130 * blocks; `console.*` lines follow everything else in one `<console_output>` block.
131 */
132function layout(output: readonly CodemodeOutputItem[]): (TextContent | ImageContent)[] {
133  const total = output.filter((item) => item.type === "text" && !item.console).length;
134  const items: (TextContent | ImageContent)[] = [];
135  const consoleLines: string[] = [];
136  let index = 0;
137  for (const item of output) {
138    if (item.type === "image") items.push(item);
139    else if (item.console) consoleLines.push(item.text);
140    else {items.push({
141        type: "text",
142        text: total > 1 ? `==> text ${++index}/${total} <==\n${item.text}` : item.text,
143      });}
144  }
145  if (consoleLines.length > 0) {
146    items.push({
147      type: "text",
148      text: `<console_output>\n${consoleLines.join("\n")}\n</console_output>`,
149    });
150  }
151  return items;
152}
153
154/** Over budget, the text keeps its start and end, images follow it, and the whole text goes to a file. */
155async function budget(
156  items: (TextContent | ImageContent)[],
157  maxTokens: number,
158): Promise<{ items: (TextContent | ImageContent)[]; fullOutputPath?: string }> {
159  const text = items.flatMap((item) => (item.type === "text" ? [item.text] : [])).join("\n");
160  const chars = maxTokens * CHARS_PER_TOKEN;
161  if (text.length <= chars) return { items };
162  const head = text.slice(0, Math.floor(chars / 2));
163  const tail = text.slice(text.length - Math.ceil(chars / 2));
164  const omitted = Math.ceil((text.length - head.length - tail.length) / CHARS_PER_TOKEN);
165  const fullOutputPath = await Deno.makeTempFile({ prefix: "berry-codemode-", suffix: ".txt" });
166  await Deno.writeTextFile(fullOutputPath, text);
167  return {
168    items: [
169      {
170        type: "text",
171        text: `${head}…${omitted} tokens truncated…${tail}\n\n[Full output: ${fullOutputPath}]`,
172      },
173      ...items.filter((item) => item.type === "image"),
174    ],
175    fullOutputPath,
176  };
177}
178
179function failure(
180  result: Extract<CodemodeResult, { ok: false }>,
181  calls: readonly NestedCall[],
182): string {
183  const { error } = result;
184  const head = error.kind === "script"
185    ? (error.stack ?? `${error.name ?? "Error"}: ${error.message}`)
186    : `Script ${
187      error.kind === "sandbox"
188        ? "sandbox failed"
189        : error.kind === "timeout"
190        ? "timed out"
191        : "aborted"
192    }: ${error.message}`;
193  const made = calls.length === 0
194    ? "No tool calls were made."
195    : `Tool calls made before the failure (they are not undone): ${
196      calls.map((c) => `${c.name} (${c.status})`).join(", ")
197    }`;
198  return `Script error:\n${head}\n\n${made}`;
199}
200
201export type ScriptResult = {
202  content: (TextContent | ImageContent)[];
203  isError: boolean;
204  details: CodemodeDetails;
205  /** Only successful scripts write. */
206  storeWrites?: StoreWrite;
207};
208
209export async function runScript(source: string, { tools, store, signal, progress }: {
210  tools: readonly NestedTool[];
211  store: Readonly<Record<string, JsonValue>>;
212  signal: AbortSignal;
213  progress(details: CodemodeDetails): void;
214}): Promise<ScriptResult> {
215  let parsed;
216  try {
217    parsed = parseCodemodeSource(source);
218  } catch (error) {
219    if (!(error instanceof CodemodeSourceError)) throw error;
220    return {
221      content: [{ type: "text", text: `Script failed\n\n${error.message}` }],
222      isError: true,
223      details: { startedAt: Date.now(), durationMs: 0, calls: [] },
224    };
225  }
226
227  const startedAt = Date.now();
228  const calls: NestedCall[] = [];
229  const publish = () => progress({ startedAt, calls: structuredClone(calls) });
230  publish();
231  const sandbox = new CodemodeSandbox({
232    tools: tools.map((tool) => ({
233      ...declaration(tool),
234      execute: async (args, { signal }) => {
235        const call: NestedCall = {
236          name: tool.name,
237          extension: tool.extension,
238          args: args as JsonValue,
239          status: "running",
240          startedAt: Date.now(),
241        };
242        calls.push(call);
243        publish();
244        try {
245          const value = await tool.call(args as JsonValue, {
246            signal,
247            output: (chunk) => {
248              call.output = clip((call.output ?? "") + chunk);
249              publish();
250            },
251            progress: (details) => {
252              call.details = details;
253              publish();
254            },
255          });
256          call.status = "ok";
257          if (value !== undefined && JSON.stringify(value).length <= DETAIL_CHARS) {
258            call.value = value;
259          }
260          return value;
261        } catch (error) {
262          call.status = signal.aborted ? "cancelled" : "error";
263          call.error = clip(messageOf(error));
264          throw error;
265        } finally {
266          call.durationMs = Date.now() - call.startedAt;
267          publish();
268        }
269      },
270    })),
271    timeoutMs: parsed.options.timeoutMs ?? Number.POSITIVE_INFINITY,
272    memoryLimitBytes: MEMORY_LIMIT_BYTES,
273  });
274
275  let result: CodemodeResult;
276  try {
277    result = await sandbox.execute(parsed.code, { signal, store });
278  } finally {
279    await sandbox.close();
280  }
281  // still running: cut off by the script ending, a timeout, or an abort
282  for (const call of calls) if (call.status === "running") call.status = "cancelled";
283
284  const output = [...result.output];
285  if (result.ok && result.value !== undefined) {
286    output.push({
287      type: "text",
288      text: typeof result.value === "string" ? result.value : JSON.stringify(result.value),
289    });
290  }
291  const items = layout(output);
292  if (!result.ok) items.push({ type: "text", text: failure(result, calls) });
293  const { items: shown, fullOutputPath } = await budget(
294    items,
295    parsed.options.maxOutputTokens ?? DEFAULT_MAX_OUTPUT_TOKENS,
296  );
297
298  const writes = result.ok ? result.storeWrites : undefined;
299  const wrote = writes && (Object.keys(writes.set).length > 0 || writes.delete.length > 0);
300  return {
301    content: [{ type: "text", text: result.ok ? "Script completed" : "Script failed" }, ...shown],
302    isError: !result.ok,
303    details: {
304      startedAt,
305      durationMs: Date.now() - startedAt,
306      calls,
307      ...(fullOutputPath ? { fullOutputPath } : {}),
308    },
309    ...(wrote
310      ? { storeWrites: { set: writes.set as Record<string, JsonValue>, delete: writes.delete } }
311      : {}),
312  };
313}