@celestea/tools 2.7.1

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 (140) hide show
  1. package/LICENSE +21 -0
  2. package/dist/args.d.ts +15 -0
  3. package/dist/args.js +55 -0
  4. package/dist/attachments/image-header.d.ts +28 -0
  5. package/dist/attachments/image-header.js +148 -0
  6. package/dist/attachments/store.d.ts +56 -0
  7. package/dist/attachments/store.js +148 -0
  8. package/dist/browser/cdp.d.ts +162 -0
  9. package/dist/browser/cdp.js +269 -0
  10. package/dist/browser/launch.d.ts +92 -0
  11. package/dist/browser/launch.js +232 -0
  12. package/dist/browser/memory-guard.d.ts +55 -0
  13. package/dist/browser/memory-guard.js +210 -0
  14. package/dist/browser/session.d.ts +146 -0
  15. package/dist/browser/session.js +371 -0
  16. package/dist/browser/snapshot.d.ts +69 -0
  17. package/dist/browser/snapshot.js +163 -0
  18. package/dist/browser/types.d.ts +51 -0
  19. package/dist/browser/types.js +8 -0
  20. package/dist/builtin.d.ts +54 -0
  21. package/dist/builtin.js +77 -0
  22. package/dist/desc.d.ts +20 -0
  23. package/dist/desc.js +22 -0
  24. package/dist/disclosure.d.ts +113 -0
  25. package/dist/disclosure.js +141 -0
  26. package/dist/env.d.ts +10 -0
  27. package/dist/env.js +33 -0
  28. package/dist/errors.d.ts +25 -0
  29. package/dist/errors.js +60 -0
  30. package/dist/exposure.d.ts +145 -0
  31. package/dist/exposure.js +244 -0
  32. package/dist/fn-tool.d.ts +14 -0
  33. package/dist/fn-tool.js +14 -0
  34. package/dist/fs/file-io.d.ts +78 -0
  35. package/dist/fs/file-io.js +239 -0
  36. package/dist/guard/path-guard.d.ts +144 -0
  37. package/dist/guard/path-guard.js +289 -0
  38. package/dist/guard/paths.d.ts +35 -0
  39. package/dist/guard/paths.js +100 -0
  40. package/dist/http/errors.d.ts +24 -0
  41. package/dist/http/errors.js +64 -0
  42. package/dist/http/headers.d.ts +19 -0
  43. package/dist/http/headers.js +62 -0
  44. package/dist/http/redirects.d.ts +31 -0
  45. package/dist/http/redirects.js +76 -0
  46. package/dist/http/ssrf.d.ts +105 -0
  47. package/dist/http/ssrf.js +272 -0
  48. package/dist/http/transport.d.ts +50 -0
  49. package/dist/http/transport.js +130 -0
  50. package/dist/index.d.ts +114 -0
  51. package/dist/index.js +129 -0
  52. package/dist/memory/log.d.ts +75 -0
  53. package/dist/memory/log.js +157 -0
  54. package/dist/memory/store.d.ts +47 -0
  55. package/dist/memory/store.js +61 -0
  56. package/dist/platform/exec.d.ts +79 -0
  57. package/dist/platform/exec.js +218 -0
  58. package/dist/platform/index.d.ts +12 -0
  59. package/dist/platform/index.js +12 -0
  60. package/dist/platform/paths.d.ts +51 -0
  61. package/dist/platform/paths.js +60 -0
  62. package/dist/platform/quote.d.ts +72 -0
  63. package/dist/platform/quote.js +102 -0
  64. package/dist/plugin.d.ts +96 -0
  65. package/dist/plugin.js +101 -0
  66. package/dist/process/buffers.d.ts +33 -0
  67. package/dist/process/buffers.js +86 -0
  68. package/dist/process/registry.d.ts +98 -0
  69. package/dist/process/registry.js +282 -0
  70. package/dist/registry.d.ts +52 -0
  71. package/dist/registry.js +161 -0
  72. package/dist/run-code/broker.d.ts +68 -0
  73. package/dist/run-code/broker.js +465 -0
  74. package/dist/run-code/limits.d.ts +69 -0
  75. package/dist/run-code/limits.js +88 -0
  76. package/dist/run-code/lines.d.ts +69 -0
  77. package/dist/run-code/lines.js +199 -0
  78. package/dist/run-code/sdk-ts.d.ts +34 -0
  79. package/dist/run-code/sdk-ts.js +276 -0
  80. package/dist/run-code/sdk.d.ts +39 -0
  81. package/dist/run-code/sdk.js +294 -0
  82. package/dist/sandbox/async.d.ts +10 -0
  83. package/dist/sandbox/async.js +26 -0
  84. package/dist/sandbox/bwrap-argv.d.ts +62 -0
  85. package/dist/sandbox/bwrap-argv.js +113 -0
  86. package/dist/sandbox/bwrap.d.ts +94 -0
  87. package/dist/sandbox/bwrap.js +159 -0
  88. package/dist/sandbox/child.d.ts +38 -0
  89. package/dist/sandbox/child.js +98 -0
  90. package/dist/sandbox/config.d.ts +89 -0
  91. package/dist/sandbox/config.js +149 -0
  92. package/dist/sandbox/fake-sandbox.d.ts +57 -0
  93. package/dist/sandbox/fake-sandbox.js +110 -0
  94. package/dist/sandbox/launch.d.ts +51 -0
  95. package/dist/sandbox/launch.js +134 -0
  96. package/dist/sandbox/limits.d.ts +63 -0
  97. package/dist/sandbox/limits.js +113 -0
  98. package/dist/sandbox/probe.d.ts +46 -0
  99. package/dist/sandbox/probe.js +102 -0
  100. package/dist/sandbox/provider.d.ts +83 -0
  101. package/dist/sandbox/provider.js +126 -0
  102. package/dist/sandbox/rlimit.d.ts +60 -0
  103. package/dist/sandbox/rlimit.js +76 -0
  104. package/dist/sandbox/seccomp.d.ts +48 -0
  105. package/dist/sandbox/seccomp.js +115 -0
  106. package/dist/sandbox/userspace.d.ts +65 -0
  107. package/dist/sandbox/userspace.js +107 -0
  108. package/dist/sandbox/workdir.d.ts +13 -0
  109. package/dist/sandbox/workdir.js +44 -0
  110. package/dist/schema.d.ts +20 -0
  111. package/dist/schema.js +135 -0
  112. package/dist/testing/platform-gates.d.ts +54 -0
  113. package/dist/testing/platform-gates.js +62 -0
  114. package/dist/tool-failure.d.ts +13 -0
  115. package/dist/tool-failure.js +19 -0
  116. package/dist/tools/ask-user.d.ts +32 -0
  117. package/dist/tools/ask-user.js +145 -0
  118. package/dist/tools/browser.d.ts +24 -0
  119. package/dist/tools/browser.js +132 -0
  120. package/dist/tools/http-request.d.ts +31 -0
  121. package/dist/tools/http-request.js +117 -0
  122. package/dist/tools/list-dir.d.ts +9 -0
  123. package/dist/tools/list-dir.js +45 -0
  124. package/dist/tools/load-skill.d.ts +37 -0
  125. package/dist/tools/load-skill.js +76 -0
  126. package/dist/tools/memory.d.ts +46 -0
  127. package/dist/tools/memory.js +131 -0
  128. package/dist/tools/process-control.d.ts +14 -0
  129. package/dist/tools/process-control.js +49 -0
  130. package/dist/tools/read-file.d.ts +11 -0
  131. package/dist/tools/read-file.js +81 -0
  132. package/dist/tools/read-image.d.ts +33 -0
  133. package/dist/tools/read-image.js +144 -0
  134. package/dist/tools/run-code.d.ts +48 -0
  135. package/dist/tools/run-code.js +115 -0
  136. package/dist/tools/run-shell.d.ts +22 -0
  137. package/dist/tools/run-shell.js +81 -0
  138. package/dist/tools/write-file.d.ts +8 -0
  139. package/dist/tools/write-file.js +31 -0
  140. package/package.json +28 -0
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Line framing and byte budgets for the `run_code` broker
3
+ * (`crates/tools/src/run_code.rs:488-560`).
4
+ *
5
+ * The child speaks a line protocol on stdout, so the parent needs three things
6
+ * the sandbox's capped whole-stream captures cannot give it:
7
+ * - **one `\n`-terminated line at a time, with a deadline** — the wall clock is
8
+ * enforced while waiting, not after the stream ends;
9
+ * - **a per-line byte budget** — an over-long line is drained and dropped, never
10
+ * buffered and never parsed as protocol (a truncated JSON line is not JSON);
11
+ * - **UTF-8 safe cuts** — a budget never splits a multi-byte character, so a
12
+ * truncated value or log stays valid text.
13
+ */
14
+ import type { Readable } from "node:stream";
15
+ import { TIMED_OUT } from "../sandbox/async.js";
16
+ /** One framed stdout line plus whether its byte budget cut it. */
17
+ export interface BoundedLine {
18
+ text: string;
19
+ truncated: boolean;
20
+ }
21
+ /**
22
+ * `\n`-framed reader over a byte stream with a per-line cap and a deadline.
23
+ * Feed-driven (no async iterators): the broker asks for one line at a time and
24
+ * always re-arms the *remaining* wall clock, so a silent child cannot stall it.
25
+ */
26
+ export declare class LineReader {
27
+ private readonly stream;
28
+ private readonly maxLineBytes;
29
+ private readonly queued;
30
+ private readonly parts;
31
+ private bytes;
32
+ private cut;
33
+ private done;
34
+ private wake;
35
+ constructor(stream: Readable | null, maxLineBytes: number);
36
+ /**
37
+ * The next complete line, `null` at EOF, or [`TIMED_OUT`] when `remainingMs`
38
+ * elapses first. A partial tail at EOF is dropped (parity: only
39
+ * `\n`-terminated lines are protocol candidates).
40
+ */
41
+ next(remainingMs: number): Promise<BoundedLine | null | typeof TIMED_OUT>;
42
+ /** Stop consuming: detach and pause so post-final output is not buffered. */
43
+ stop(): void;
44
+ private arrival;
45
+ private signal;
46
+ private feed;
47
+ private push;
48
+ private complete;
49
+ }
50
+ /**
51
+ * Decode UTF-8 bytes, dropping a trailing **incomplete** sequence (the budget
52
+ * cut may land inside a multi-byte character: `"hé"[:3]` must not become
53
+ * `"h\uFFFD"`).
54
+ */
55
+ export declare function safeUtf8(buffer: Buffer): string;
56
+ /** UTF-8 safe prefix of `text` at most `maxBytes` bytes long. */
57
+ export declare function utf8Prefix(text: string, maxBytes: number): string;
58
+ /** Serialized size of a value in bytes, or `null` when it is not JSON-able. */
59
+ export declare function jsonByteLength(value: unknown): number | null;
60
+ /** Append up to `maxBytes` of `chunk` to `current` (UTF-8 safe, parity). */
61
+ export declare function appendBounded(current: string, chunk: string, maxBytes: number): BoundedLine;
62
+ /**
63
+ * Truncate a sub-call result to `budget` serialized bytes: strings keep a
64
+ * UTF-8 safe prefix; non-strings collapse to a placeholder (there is no
65
+ * lossless way to cut an object/array). Legacy `truncate_value`.
66
+ */
67
+ export declare function truncateValue(value: unknown, budget: number): unknown;
68
+ /** The last `maxChars` characters of `text` (failure-message log tail). */
69
+ export declare function tail(text: string, maxChars: number): string;
@@ -0,0 +1,199 @@
1
+ /**
2
+ * Line framing and byte budgets for the `run_code` broker
3
+ * (`crates/tools/src/run_code.rs:488-560`).
4
+ *
5
+ * The child speaks a line protocol on stdout, so the parent needs three things
6
+ * the sandbox's capped whole-stream captures cannot give it:
7
+ * - **one `\n`-terminated line at a time, with a deadline** — the wall clock is
8
+ * enforced while waiting, not after the stream ends;
9
+ * - **a per-line byte budget** — an over-long line is drained and dropped, never
10
+ * buffered and never parsed as protocol (a truncated JSON line is not JSON);
11
+ * - **UTF-8 safe cuts** — a budget never splits a multi-byte character, so a
12
+ * truncated value or log stays valid text.
13
+ */
14
+ import { TIMED_OUT } from "../sandbox/async.js";
15
+ const NEWLINE = 0x0a;
16
+ /** Longest UTF-8 sequence (a 4-byte code point) — the cut look-back window. */
17
+ const MAX_UTF8_SEQUENCE = 4;
18
+ /**
19
+ * `\n`-framed reader over a byte stream with a per-line cap and a deadline.
20
+ * Feed-driven (no async iterators): the broker asks for one line at a time and
21
+ * always re-arms the *remaining* wall clock, so a silent child cannot stall it.
22
+ */
23
+ export class LineReader {
24
+ stream;
25
+ maxLineBytes;
26
+ queued = [];
27
+ parts = [];
28
+ bytes = 0;
29
+ cut = false;
30
+ done = false;
31
+ wake = null;
32
+ constructor(stream, maxLineBytes) {
33
+ this.stream = stream;
34
+ this.maxLineBytes = maxLineBytes;
35
+ if (stream === null) {
36
+ this.done = true;
37
+ return;
38
+ }
39
+ stream.on("data", (chunk) => {
40
+ this.feed(typeof chunk === "string" ? Buffer.from(chunk, "utf8") : chunk);
41
+ this.signal();
42
+ });
43
+ const end = () => {
44
+ this.done = true;
45
+ this.signal();
46
+ };
47
+ stream.on("end", end);
48
+ stream.on("close", end);
49
+ stream.on("error", end);
50
+ }
51
+ /**
52
+ * The next complete line, `null` at EOF, or [`TIMED_OUT`] when `remainingMs`
53
+ * elapses first. A partial tail at EOF is dropped (parity: only
54
+ * `\n`-terminated lines are protocol candidates).
55
+ */
56
+ async next(remainingMs) {
57
+ const deadline = Date.now() + remainingMs;
58
+ for (;;) {
59
+ const line = this.queued.shift();
60
+ if (line !== undefined)
61
+ return line;
62
+ if (this.done)
63
+ return null;
64
+ const left = deadline - Date.now();
65
+ if (left <= 0)
66
+ return TIMED_OUT;
67
+ if (!(await this.arrival(left)))
68
+ return TIMED_OUT;
69
+ }
70
+ }
71
+ /** Stop consuming: detach and pause so post-final output is not buffered. */
72
+ stop() {
73
+ this.done = true;
74
+ if (this.stream === null)
75
+ return;
76
+ this.stream.removeAllListeners("data");
77
+ this.stream.pause();
78
+ }
79
+ arrival(ms) {
80
+ return new Promise((resolve) => {
81
+ let settled = false;
82
+ const timer = setTimeout(() => {
83
+ if (settled)
84
+ return;
85
+ settled = true;
86
+ this.wake = null;
87
+ resolve(false);
88
+ }, ms);
89
+ this.wake = () => {
90
+ if (settled)
91
+ return;
92
+ settled = true;
93
+ clearTimeout(timer);
94
+ this.wake = null;
95
+ resolve(true);
96
+ };
97
+ });
98
+ }
99
+ signal() {
100
+ this.wake?.();
101
+ }
102
+ feed(chunk) {
103
+ let start = 0;
104
+ for (;;) {
105
+ const end = chunk.indexOf(NEWLINE, start);
106
+ if (end === -1) {
107
+ this.push(chunk.subarray(start));
108
+ return;
109
+ }
110
+ this.push(chunk.subarray(start, end));
111
+ this.complete();
112
+ start = end + 1;
113
+ }
114
+ }
115
+ push(segment) {
116
+ if (this.cut || segment.length === 0)
117
+ return;
118
+ const room = this.maxLineBytes - this.bytes;
119
+ if (segment.length <= room) {
120
+ this.parts.push(segment);
121
+ this.bytes += segment.length;
122
+ return;
123
+ }
124
+ this.parts.push(segment.subarray(0, Math.max(room, 0)));
125
+ this.bytes = this.maxLineBytes;
126
+ this.cut = true;
127
+ }
128
+ complete() {
129
+ this.queued.push({ text: safeUtf8(Buffer.concat(this.parts)), truncated: this.cut });
130
+ this.parts.length = 0;
131
+ this.bytes = 0;
132
+ this.cut = false;
133
+ }
134
+ }
135
+ /**
136
+ * Decode UTF-8 bytes, dropping a trailing **incomplete** sequence (the budget
137
+ * cut may land inside a multi-byte character: `"hé"[:3]` must not become
138
+ * `"h\uFFFD"`).
139
+ */
140
+ export function safeUtf8(buffer) {
141
+ for (let back = 0; back < MAX_UTF8_SEQUENCE && back < buffer.length; back += 1) {
142
+ const byte = buffer[buffer.length - 1 - back] ?? 0;
143
+ if ((byte & 0xc0) === 0x80)
144
+ continue; // continuation byte: keep looking back
145
+ if ((byte & 0x80) === 0)
146
+ return buffer.toString("utf8"); // ASCII tail: complete
147
+ const need = byte >= 0xf0 ? 4 : byte >= 0xe0 ? 3 : 2;
148
+ if (back + 1 === need)
149
+ return buffer.toString("utf8"); // complete code point
150
+ return buffer.subarray(0, buffer.length - 1 - back).toString("utf8"); // cut inside it
151
+ }
152
+ return buffer.toString("utf8");
153
+ }
154
+ /** UTF-8 safe prefix of `text` at most `maxBytes` bytes long. */
155
+ export function utf8Prefix(text, maxBytes) {
156
+ if (maxBytes <= 0)
157
+ return "";
158
+ const bytes = Buffer.from(text, "utf8");
159
+ return bytes.length <= maxBytes ? text : safeUtf8(bytes.subarray(0, maxBytes));
160
+ }
161
+ /** Serialized size of a value in bytes, or `null` when it is not JSON-able. */
162
+ export function jsonByteLength(value) {
163
+ let encoded;
164
+ try {
165
+ encoded = JSON.stringify(value);
166
+ }
167
+ catch {
168
+ return null;
169
+ }
170
+ return encoded === undefined ? null : Buffer.byteLength(encoded, "utf8");
171
+ }
172
+ /** Append up to `maxBytes` of `chunk` to `current` (UTF-8 safe, parity). */
173
+ export function appendBounded(current, chunk, maxBytes) {
174
+ if (chunk === "")
175
+ return { text: current, truncated: false };
176
+ const room = maxBytes - Buffer.byteLength(current, "utf8");
177
+ const size = Buffer.byteLength(chunk, "utf8");
178
+ if (size <= room)
179
+ return { text: current + chunk, truncated: false };
180
+ return { text: current + utf8Prefix(chunk, Math.max(room, 0)), truncated: true };
181
+ }
182
+ /**
183
+ * Truncate a sub-call result to `budget` serialized bytes: strings keep a
184
+ * UTF-8 safe prefix; non-strings collapse to a placeholder (there is no
185
+ * lossless way to cut an object/array). Legacy `truncate_value`.
186
+ */
187
+ export function truncateValue(value, budget) {
188
+ if (typeof value === "string")
189
+ return utf8Prefix(value, budget);
190
+ const size = jsonByteLength(value);
191
+ if (size !== null && size <= budget)
192
+ return value;
193
+ return "[run_code] sub-call output exceeded the budget; value dropped";
194
+ }
195
+ /** The last `maxChars` characters of `text` (failure-message log tail). */
196
+ export function tail(text, maxChars) {
197
+ const chars = [...text];
198
+ return chars.length <= maxChars ? text : chars.slice(chars.length - maxChars).join("");
199
+ }
@@ -0,0 +1,34 @@
1
+ /**
2
+ * The TypeScript SDK preamble + runner (W774) — split out of `sdk.ts` (which is
3
+ * the Python parity file and stays at its own size budget).
4
+ *
5
+ * Same protocol, same four bridges, but executed by Node's NATIVE type stripping
6
+ * (`node .celestea/<file>.ts`): no build step, no `node_modules`, no dependency.
7
+ *
8
+ * Deliberate differences from the Python preamble (both are pinned by tests):
9
+ * - attribute access is native (`r.stdout` and `r["stdout"]` both just work),
10
+ * so the Python `_AttrDict` / `_Value` dual-interface wrappers have no
11
+ * counterpart;
12
+ * - `await tools.read_file(...)` works because awaiting a non-promise value
13
+ * returns it unchanged — no awaitable wrapper needed;
14
+ * - the reply channel is read SYNCHRONOUSLY with `fs.readSync(0, …)` plus a
15
+ * half-line buffer (the equivalent of Python's `sys.stdin.readline()`);
16
+ * after EOF a further bridge call THROWS instead of hanging.
17
+ *
18
+ * Only ERASABLE TypeScript is allowed in the assembled file (Node strips types,
19
+ * it never compiles them): no `enum`, no `namespace`, no parameter properties.
20
+ */
21
+ export declare const RUN_CODE_SDK_TS = "// =============================================================================\n// celestea run_code SDK (W774) - engine-injected preamble (TypeScript).\n// Node standard library only. Runs under `node .celestea/<file>.ts` inside the\n// Celestea execution sandbox; the parent engine (the \"broker\") reads our\n// protocol lines from stdout and writes replies to stdin.\n//\n// Protocol (one JSON object per line, no other framing) - identical to the\n// Python SDK on purpose: the broker is language-neutral.\n// child -> parent {\"id\": <int>, \"tool\": \"<name>\", \"args\": {...}}\n// parent -> child {\"id\": <int>, \"ok\": true, \"value\": <json>, \"truncated\": <bool>}\n// {\"id\": <int>, \"ok\": false, \"error\": \"<message>\"}\n// child -> parent {\"__final__\": <json>} (normal end: main()'s return)\n// {\"__error__\": \"<message>\"} (uncaught exception)\n//\n// Contract:\n// - Write the program as a `function main()` BODY (an indented body is wrapped\n// for you), or as a complete script that defines main. main() MAY be async:\n// the harness awaits it. Its resolved value (lossless JSON) is the result.\n// - `tools.<name>({...})` is a SYNCHRONOUS bridge: every sub-call is dispatched\n// by the parent through its normal tool pipeline (guards, schema checks,\n// limits). A failure throws ToolCallError - catch it and continue.\n// `await tools.<name>({...})` is the same thing (awaiting a value is free).\n// - Only console.log what the model needs: a line that is not protocol JSON is\n// a LOG line (budgeted at 64KiB); intermediate results never enter the chat.\n// - Sub-calls run SERIALLY, one at a time, in request order.\n// - Exposed tools: read_file / write_file / list_dir / run_shell. Any other\n// name (including run_code itself) is rejected by the parent.\n// - TYPE STRIPPING ONLY: this file is executed by Node's native type stripping,\n// so only ERASABLE TypeScript is allowed - no `enum`, no `namespace`, no\n// parameter properties, no `declare`. Plain JavaScript always works.\n// - Do not read process.stdin and do not call process.exit(): stdin is the\n// protocol reply channel and this harness is the entry point.\n// =============================================================================\nimport { readSync as _readSync } from \"node:fs\";\nimport { StringDecoder as _StringDecoder } from \"node:string_decoder\";\nimport { createRequire as _createRequire } from \"node:module\";\n\n// Models sometimes reach for CommonJS: in an ES module `require` is undefined,\n// so expose the real one (the same resolution rules as this file).\n(globalThis as { require?: unknown }).require ??= _createRequire(import.meta.url);\n\nclass ToolCallError extends Error {\n readonly toolName: string;\n\n constructor(toolName: string, message: string) {\n super(\"tool '\" + toolName + \"' failed: \" + message);\n this.name = \"ToolCallError\";\n this.toolName = toolName;\n }\n}\n\nlet _subCallId = 0;\nlet _pending = \"\";\nlet _eof = false;\nconst _decoder = new _StringDecoder(\"utf8\");\n\n/** Synchronous sleep (only needed if the reply channel is non-blocking). */\nfunction _sleepMs(ms: number): void {\n try {\n Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);\n } catch {\n const until = Date.now() + ms;\n while (Date.now() < until) {\n /* last-resort spin: the bridge must never hang */\n }\n }\n}\n\n/**\n * Read ONE line from the protocol channel, blocking until it arrives (the\n * equivalent of Python's `sys.stdin.readline()`). Returns null at EOF.\n *\n * The buffer may already hold several lines (the parent writes replies back to\n * back) and one read may split a UTF-8 sequence, which is why bytes go through\n * a StringDecoder rather than `chunk.toString()`. CRLF is accepted.\n */\nfunction _readProtocolLine(): string | null {\n for (;;) {\n const nl = _pending.indexOf(\"\\n\");\n if (nl >= 0) {\n const line = _pending.slice(0, nl);\n _pending = _pending.slice(nl + 1);\n return line.endsWith(\"\\r\") ? line.slice(0, -1) : line;\n }\n if (_eof) {\n // A trailing fragment without a newline is still a line (readline does the\n // same); only a fully drained buffer means EOF.\n if (_pending === \"\") return null;\n const rest = _pending;\n _pending = \"\";\n return rest;\n }\n const chunk = Buffer.allocUnsafe(65536);\n let read = 0;\n try {\n read = _readSync(0, chunk, 0, chunk.length, null);\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code === \"EAGAIN\") {\n _sleepMs(5); // non-blocking stdin: wait, then retry (never fail the call)\n continue;\n }\n throw new ToolCallError(\"protocol\", \"cannot read the reply channel: \" + String((error as Error).message));\n }\n if (read === 0) {\n _eof = true;\n _pending += _decoder.end();\n continue;\n }\n _pending += _decoder.write(chunk.subarray(0, read));\n }\n}\n\n/** One bridge call: request line out, reply line in, value or ToolCallError. */\nfunction _bridgeCall(tool: string, args: unknown): unknown {\n const requestId = ++_subCallId;\n let payload: string;\n try {\n payload = JSON.stringify({ id: requestId, tool: tool, args: args === undefined ? {} : args });\n } catch (error) {\n throw new ToolCallError(tool, \"arguments are not JSON-serializable: \" + String((error as Error).message));\n }\n process.stdout.write(payload + \"\\n\");\n const line = _readProtocolLine();\n if (line === null) {\n throw new ToolCallError(tool, \"the parent broker closed the reply channel (run aborted)\");\n }\n let reply: Record<string, unknown>;\n try {\n const decoded: unknown = JSON.parse(line);\n if (typeof decoded !== \"object\" || decoded === null || Array.isArray(decoded)) {\n throw new Error(\"reply is not a JSON object\");\n }\n reply = decoded as Record<string, unknown>;\n } catch (error) {\n throw new ToolCallError(tool, \"malformed reply from the parent broker: \" + String((error as Error).message));\n }\n if (reply[\"id\"] !== requestId) {\n throw new ToolCallError(tool, \"reply id mismatch (expected \" + requestId + \", got \" + String(reply[\"id\"]) + \")\");\n }\n if (reply[\"ok\"] !== true) {\n const message = typeof reply[\"error\"] === \"string\" ? (reply[\"error\"] as string) : \"unknown error\";\n throw new ToolCallError(tool, message);\n }\n return reply[\"value\"] === undefined ? null : reply[\"value\"];\n}\n\n/** Canonical single-argument name per bridged tool (for the positional shortcut). */\nconst _primaryArg: Record<string, string> = {\n read_file: \"path\",\n write_file: \"path\",\n list_dir: \"path\",\n run_shell: \"command\",\n};\n\n/**\n * Both call styles work: the documented object form\n * (`tools.read_file({ path: ... })`) and the positional shorthand models write\n * anyway (`tools.read_file(\"x\")`, `tools.write_file(\"x\", \"content\")`).\n */\nfunction _mergeArgs(tool: string, received: readonly unknown[]): Record<string, unknown> {\n const first = received[0];\n const second = received[1];\n if (typeof first === \"string\") {\n const args: Record<string, unknown> = {};\n args[_primaryArg[tool] as string] = first;\n if (tool === \"write_file\" && typeof second === \"string\") args[\"content\"] = second;\n return args;\n }\n if (first === undefined) return {};\n if (typeof first !== \"object\" || first === null || Array.isArray(first)) {\n throw new ToolCallError(\n tool,\n \"expected an arguments object, e.g. tools.\" + tool + \"({ \" + (_primaryArg[tool] as string) + \": ... })\",\n );\n }\n const args: Record<string, unknown> = { ...(first as Record<string, unknown>) };\n if (tool === \"write_file\" && typeof second === \"string\") args[\"content\"] = second;\n return args;\n}\n\nclass _Tools {\n read_file(...args: unknown[]): unknown {\n return _bridgeCall(\"read_file\", _mergeArgs(\"read_file\", args));\n }\n\n write_file(...args: unknown[]): unknown {\n return _bridgeCall(\"write_file\", _mergeArgs(\"write_file\", args));\n }\n\n list_dir(...args: unknown[]): unknown {\n return _bridgeCall(\"list_dir\", _mergeArgs(\"list_dir\", args));\n }\n\n run_shell(...args: unknown[]): unknown {\n // The full result object: {\"exit_code\", \"stdout\", \"stderr\", ...}.\n return _bridgeCall(\"run_shell\", _mergeArgs(\"run_shell\", args));\n }\n}\n\nconst tools = new _Tools();\n";
22
+ /**
23
+ * The TypeScript runner appended after the user program (W774).
24
+ *
25
+ * Python parity, line for line: run `main()`, await it, emit `{__final__: …}`;
26
+ * on any failure print the stack (the traceback equivalent) and then
27
+ * `{__error__: "<Name>: <message>"}`, and exit 1. `undefined` becomes `null`
28
+ * because `JSON.stringify` drops `undefined`, while Python's `main()` returning
29
+ * None reports `{"__final__": null}` — the broker must see the same shape.
30
+ *
31
+ * `process.exitCode` is set instead of calling `process.exit()`: the protocol
32
+ * line is written to a PIPE, and an immediate exit can truncate it.
33
+ */
34
+ export declare const RUN_CODE_RUNNER_TS = "\n// ======================= harness entry point (injected) =======================\nfunction _celesteaErrorLine(error: unknown): string {\n const named = error as { name?: unknown; message?: unknown };\n const name = typeof named?.name === \"string\" ? named.name : \"Error\";\n const message = named?.message === undefined ? String(error) : String(named.message);\n return name + \": \" + message;\n}\n\nfunction _celesteaFail(error: unknown): void {\n const stacked = error as { stack?: unknown };\n const text = typeof stacked?.stack === \"string\" ? stacked.stack : _celesteaErrorLine(error);\n for (const line of text.split(\"\\n\")) process.stdout.write(line + \"\\n\");\n process.stdout.write(JSON.stringify({ __error__: _celesteaErrorLine(error) }) + \"\\n\");\n process.exitCode = 1;\n}\n\nasync function _celesteaRunMain(): Promise<unknown> {\n // `typeof` on an undeclared identifier is safe, so a program without main()\n // gets a clear message instead of a ReferenceError.\n const entry: unknown = typeof main === \"function\" ? main : undefined;\n if (entry === undefined) {\n throw new Error(\n \"run_code: no 'main' defined - write the program as a function body, or as a \" +\n \"complete script defining function main() (it may be async)\",\n );\n }\n return await (entry as () => unknown)();\n}\n\n_celesteaRunMain().then(\n function (value: unknown): void {\n try {\n process.stdout.write(JSON.stringify({ __final__: value === undefined ? null : value }) + \"\\n\");\n } catch (error) {\n _celesteaFail(error);\n }\n },\n function (error: unknown): void {\n _celesteaFail(error);\n },\n);\n";
@@ -0,0 +1,276 @@
1
+ /**
2
+ * The TypeScript SDK preamble + runner (W774) — split out of `sdk.ts` (which is
3
+ * the Python parity file and stays at its own size budget).
4
+ *
5
+ * Same protocol, same four bridges, but executed by Node's NATIVE type stripping
6
+ * (`node .celestea/<file>.ts`): no build step, no `node_modules`, no dependency.
7
+ *
8
+ * Deliberate differences from the Python preamble (both are pinned by tests):
9
+ * - attribute access is native (`r.stdout` and `r["stdout"]` both just work),
10
+ * so the Python `_AttrDict` / `_Value` dual-interface wrappers have no
11
+ * counterpart;
12
+ * - `await tools.read_file(...)` works because awaiting a non-promise value
13
+ * returns it unchanged — no awaitable wrapper needed;
14
+ * - the reply channel is read SYNCHRONOUSLY with `fs.readSync(0, …)` plus a
15
+ * half-line buffer (the equivalent of Python's `sys.stdin.readline()`);
16
+ * after EOF a further bridge call THROWS instead of hanging.
17
+ *
18
+ * Only ERASABLE TypeScript is allowed in the assembled file (Node strips types,
19
+ * it never compiles them): no `enum`, no `namespace`, no parameter properties.
20
+ */
21
+ export const RUN_CODE_SDK_TS = `// =============================================================================
22
+ // celestea run_code SDK (W774) - engine-injected preamble (TypeScript).
23
+ // Node standard library only. Runs under \`node .celestea/<file>.ts\` inside the
24
+ // Celestea execution sandbox; the parent engine (the "broker") reads our
25
+ // protocol lines from stdout and writes replies to stdin.
26
+ //
27
+ // Protocol (one JSON object per line, no other framing) - identical to the
28
+ // Python SDK on purpose: the broker is language-neutral.
29
+ // child -> parent {"id": <int>, "tool": "<name>", "args": {...}}
30
+ // parent -> child {"id": <int>, "ok": true, "value": <json>, "truncated": <bool>}
31
+ // {"id": <int>, "ok": false, "error": "<message>"}
32
+ // child -> parent {"__final__": <json>} (normal end: main()'s return)
33
+ // {"__error__": "<message>"} (uncaught exception)
34
+ //
35
+ // Contract:
36
+ // - Write the program as a \`function main()\` BODY (an indented body is wrapped
37
+ // for you), or as a complete script that defines main. main() MAY be async:
38
+ // the harness awaits it. Its resolved value (lossless JSON) is the result.
39
+ // - \`tools.<name>({...})\` is a SYNCHRONOUS bridge: every sub-call is dispatched
40
+ // by the parent through its normal tool pipeline (guards, schema checks,
41
+ // limits). A failure throws ToolCallError - catch it and continue.
42
+ // \`await tools.<name>({...})\` is the same thing (awaiting a value is free).
43
+ // - Only console.log what the model needs: a line that is not protocol JSON is
44
+ // a LOG line (budgeted at 64KiB); intermediate results never enter the chat.
45
+ // - Sub-calls run SERIALLY, one at a time, in request order.
46
+ // - Exposed tools: read_file / write_file / list_dir / run_shell. Any other
47
+ // name (including run_code itself) is rejected by the parent.
48
+ // - TYPE STRIPPING ONLY: this file is executed by Node's native type stripping,
49
+ // so only ERASABLE TypeScript is allowed - no \`enum\`, no \`namespace\`, no
50
+ // parameter properties, no \`declare\`. Plain JavaScript always works.
51
+ // - Do not read process.stdin and do not call process.exit(): stdin is the
52
+ // protocol reply channel and this harness is the entry point.
53
+ // =============================================================================
54
+ import { readSync as _readSync } from "node:fs";
55
+ import { StringDecoder as _StringDecoder } from "node:string_decoder";
56
+ import { createRequire as _createRequire } from "node:module";
57
+
58
+ // Models sometimes reach for CommonJS: in an ES module \`require\` is undefined,
59
+ // so expose the real one (the same resolution rules as this file).
60
+ (globalThis as { require?: unknown }).require ??= _createRequire(import.meta.url);
61
+
62
+ class ToolCallError extends Error {
63
+ readonly toolName: string;
64
+
65
+ constructor(toolName: string, message: string) {
66
+ super("tool '" + toolName + "' failed: " + message);
67
+ this.name = "ToolCallError";
68
+ this.toolName = toolName;
69
+ }
70
+ }
71
+
72
+ let _subCallId = 0;
73
+ let _pending = "";
74
+ let _eof = false;
75
+ const _decoder = new _StringDecoder("utf8");
76
+
77
+ /** Synchronous sleep (only needed if the reply channel is non-blocking). */
78
+ function _sleepMs(ms: number): void {
79
+ try {
80
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
81
+ } catch {
82
+ const until = Date.now() + ms;
83
+ while (Date.now() < until) {
84
+ /* last-resort spin: the bridge must never hang */
85
+ }
86
+ }
87
+ }
88
+
89
+ /**
90
+ * Read ONE line from the protocol channel, blocking until it arrives (the
91
+ * equivalent of Python's \`sys.stdin.readline()\`). Returns null at EOF.
92
+ *
93
+ * The buffer may already hold several lines (the parent writes replies back to
94
+ * back) and one read may split a UTF-8 sequence, which is why bytes go through
95
+ * a StringDecoder rather than \`chunk.toString()\`. CRLF is accepted.
96
+ */
97
+ function _readProtocolLine(): string | null {
98
+ for (;;) {
99
+ const nl = _pending.indexOf("\\n");
100
+ if (nl >= 0) {
101
+ const line = _pending.slice(0, nl);
102
+ _pending = _pending.slice(nl + 1);
103
+ return line.endsWith("\\r") ? line.slice(0, -1) : line;
104
+ }
105
+ if (_eof) {
106
+ // A trailing fragment without a newline is still a line (readline does the
107
+ // same); only a fully drained buffer means EOF.
108
+ if (_pending === "") return null;
109
+ const rest = _pending;
110
+ _pending = "";
111
+ return rest;
112
+ }
113
+ const chunk = Buffer.allocUnsafe(65536);
114
+ let read = 0;
115
+ try {
116
+ read = _readSync(0, chunk, 0, chunk.length, null);
117
+ } catch (error) {
118
+ if ((error as NodeJS.ErrnoException).code === "EAGAIN") {
119
+ _sleepMs(5); // non-blocking stdin: wait, then retry (never fail the call)
120
+ continue;
121
+ }
122
+ throw new ToolCallError("protocol", "cannot read the reply channel: " + String((error as Error).message));
123
+ }
124
+ if (read === 0) {
125
+ _eof = true;
126
+ _pending += _decoder.end();
127
+ continue;
128
+ }
129
+ _pending += _decoder.write(chunk.subarray(0, read));
130
+ }
131
+ }
132
+
133
+ /** One bridge call: request line out, reply line in, value or ToolCallError. */
134
+ function _bridgeCall(tool: string, args: unknown): unknown {
135
+ const requestId = ++_subCallId;
136
+ let payload: string;
137
+ try {
138
+ payload = JSON.stringify({ id: requestId, tool: tool, args: args === undefined ? {} : args });
139
+ } catch (error) {
140
+ throw new ToolCallError(tool, "arguments are not JSON-serializable: " + String((error as Error).message));
141
+ }
142
+ process.stdout.write(payload + "\\n");
143
+ const line = _readProtocolLine();
144
+ if (line === null) {
145
+ throw new ToolCallError(tool, "the parent broker closed the reply channel (run aborted)");
146
+ }
147
+ let reply: Record<string, unknown>;
148
+ try {
149
+ const decoded: unknown = JSON.parse(line);
150
+ if (typeof decoded !== "object" || decoded === null || Array.isArray(decoded)) {
151
+ throw new Error("reply is not a JSON object");
152
+ }
153
+ reply = decoded as Record<string, unknown>;
154
+ } catch (error) {
155
+ throw new ToolCallError(tool, "malformed reply from the parent broker: " + String((error as Error).message));
156
+ }
157
+ if (reply["id"] !== requestId) {
158
+ throw new ToolCallError(tool, "reply id mismatch (expected " + requestId + ", got " + String(reply["id"]) + ")");
159
+ }
160
+ if (reply["ok"] !== true) {
161
+ const message = typeof reply["error"] === "string" ? (reply["error"] as string) : "unknown error";
162
+ throw new ToolCallError(tool, message);
163
+ }
164
+ return reply["value"] === undefined ? null : reply["value"];
165
+ }
166
+
167
+ /** Canonical single-argument name per bridged tool (for the positional shortcut). */
168
+ const _primaryArg: Record<string, string> = {
169
+ read_file: "path",
170
+ write_file: "path",
171
+ list_dir: "path",
172
+ run_shell: "command",
173
+ };
174
+
175
+ /**
176
+ * Both call styles work: the documented object form
177
+ * (\`tools.read_file({ path: ... })\`) and the positional shorthand models write
178
+ * anyway (\`tools.read_file("x")\`, \`tools.write_file("x", "content")\`).
179
+ */
180
+ function _mergeArgs(tool: string, received: readonly unknown[]): Record<string, unknown> {
181
+ const first = received[0];
182
+ const second = received[1];
183
+ if (typeof first === "string") {
184
+ const args: Record<string, unknown> = {};
185
+ args[_primaryArg[tool] as string] = first;
186
+ if (tool === "write_file" && typeof second === "string") args["content"] = second;
187
+ return args;
188
+ }
189
+ if (first === undefined) return {};
190
+ if (typeof first !== "object" || first === null || Array.isArray(first)) {
191
+ throw new ToolCallError(
192
+ tool,
193
+ "expected an arguments object, e.g. tools." + tool + "({ " + (_primaryArg[tool] as string) + ": ... })",
194
+ );
195
+ }
196
+ const args: Record<string, unknown> = { ...(first as Record<string, unknown>) };
197
+ if (tool === "write_file" && typeof second === "string") args["content"] = second;
198
+ return args;
199
+ }
200
+
201
+ class _Tools {
202
+ read_file(...args: unknown[]): unknown {
203
+ return _bridgeCall("read_file", _mergeArgs("read_file", args));
204
+ }
205
+
206
+ write_file(...args: unknown[]): unknown {
207
+ return _bridgeCall("write_file", _mergeArgs("write_file", args));
208
+ }
209
+
210
+ list_dir(...args: unknown[]): unknown {
211
+ return _bridgeCall("list_dir", _mergeArgs("list_dir", args));
212
+ }
213
+
214
+ run_shell(...args: unknown[]): unknown {
215
+ // The full result object: {"exit_code", "stdout", "stderr", ...}.
216
+ return _bridgeCall("run_shell", _mergeArgs("run_shell", args));
217
+ }
218
+ }
219
+
220
+ const tools = new _Tools();
221
+ `;
222
+ /**
223
+ * The TypeScript runner appended after the user program (W774).
224
+ *
225
+ * Python parity, line for line: run `main()`, await it, emit `{__final__: …}`;
226
+ * on any failure print the stack (the traceback equivalent) and then
227
+ * `{__error__: "<Name>: <message>"}`, and exit 1. `undefined` becomes `null`
228
+ * because `JSON.stringify` drops `undefined`, while Python's `main()` returning
229
+ * None reports `{"__final__": null}` — the broker must see the same shape.
230
+ *
231
+ * `process.exitCode` is set instead of calling `process.exit()`: the protocol
232
+ * line is written to a PIPE, and an immediate exit can truncate it.
233
+ */
234
+ export const RUN_CODE_RUNNER_TS = `
235
+ // ======================= harness entry point (injected) =======================
236
+ function _celesteaErrorLine(error: unknown): string {
237
+ const named = error as { name?: unknown; message?: unknown };
238
+ const name = typeof named?.name === "string" ? named.name : "Error";
239
+ const message = named?.message === undefined ? String(error) : String(named.message);
240
+ return name + ": " + message;
241
+ }
242
+
243
+ function _celesteaFail(error: unknown): void {
244
+ const stacked = error as { stack?: unknown };
245
+ const text = typeof stacked?.stack === "string" ? stacked.stack : _celesteaErrorLine(error);
246
+ for (const line of text.split("\\n")) process.stdout.write(line + "\\n");
247
+ process.stdout.write(JSON.stringify({ __error__: _celesteaErrorLine(error) }) + "\\n");
248
+ process.exitCode = 1;
249
+ }
250
+
251
+ async function _celesteaRunMain(): Promise<unknown> {
252
+ // \`typeof\` on an undeclared identifier is safe, so a program without main()
253
+ // gets a clear message instead of a ReferenceError.
254
+ const entry: unknown = typeof main === "function" ? main : undefined;
255
+ if (entry === undefined) {
256
+ throw new Error(
257
+ "run_code: no 'main' defined - write the program as a function body, or as a " +
258
+ "complete script defining function main() (it may be async)",
259
+ );
260
+ }
261
+ return await (entry as () => unknown)();
262
+ }
263
+
264
+ _celesteaRunMain().then(
265
+ function (value: unknown): void {
266
+ try {
267
+ process.stdout.write(JSON.stringify({ __final__: value === undefined ? null : value }) + "\\n");
268
+ } catch (error) {
269
+ _celesteaFail(error);
270
+ }
271
+ },
272
+ function (error: unknown): void {
273
+ _celesteaFail(error);
274
+ },
275
+ );
276
+ `;
@@ -0,0 +1,39 @@
1
+ /**
2
+ * The engine-injected Python SDK preamble + runner (W255).
3
+ *
4
+ * Byte-for-byte port of `RUN_CODE_SDK` / `RUN_CODE_RUNNER` in
5
+ * `crates/tools/src/run_code.rs` (the raw strings are re-encoded here as
6
+ * escape-safe template literals; `run-code/sdk.test.ts` re-extracts the
7
+ * blocks and diffs them, so drift cannot hide).
8
+ *
9
+ * The program the broker actually runs is `SDK + user code + RUNNER`:
10
+ * - the SDK exposes exactly one object `tools` (four synchronous bridges) and
11
+ * the catchable `ToolCallError`; every bridge call is a one-line JSON
12
+ * request on stdout, answered by the parent on stdin;
13
+ * - the runner calls `main()` (awaiting it when it is a coroutine) and emits
14
+ * `{"__final__": <json>}` — or `{"__error__": "<Type>: <msg>"}` after
15
+ * printing the traceback, exiting 1.
16
+ */
17
+ /** The runner appended after the user program. */
18
+ export declare const RUN_CODE_RUNNER = "\n# ======================= harness entry point (injected) =======================\ndef _plain(v):\n \"\"\"Unwrap dual-interface results before JSON serialization (dict/list deep).\"\"\"\n if isinstance(v, _Value):\n v = v._v\n if isinstance(v, dict):\n return {k: _plain(x) for k, x in v.items()}\n if isinstance(v, (list, tuple)):\n return [_plain(x) for x in v]\n return v\n\n\nimport asyncio as _asyncio\n\n\ndef _celestea_run_main():\n main = globals().get(\"main\")\n if main is None:\n raise RuntimeError(\n \"run_code: no 'main' defined - write the program as an async \"\n \"function body, or as a complete script defining async def main()\"\n )\n result = main()\n if _asyncio.iscoroutine(result):\n result = _asyncio.run(result)\n return result\n\n\ntry:\n _final_value = _celestea_run_main()\n print(_json.dumps({\"__final__\": _plain(_final_value)}, ensure_ascii=False), flush=True)\nexcept BaseException as _exc: # report ANY failure as __error__\n _tb = _traceback.format_exc()\n for _line in _tb.rstrip(\"\\n\").split(\"\\n\"):\n print(_line, flush=True)\n print(\n _json.dumps({\"__error__\": f\"{type(_exc).__name__}: {_exc}\"}, ensure_ascii=False),\n flush=True,\n )\n _sys.exit(1)\n";
19
+ export { RUN_CODE_RUNNER_TS, RUN_CODE_SDK_TS } from "./sdk-ts.js";
20
+ /** The SDK preamble (standard library only; zero pip). */
21
+ export declare const RUN_CODE_SDK = "# =============================================================================\n# celestea run_code SDK (W255 P0) - engine-injected preamble. Standard library\n# only (zero pip). Runs under `python3 -uB <file>` inside the Celestea\n# execution sandbox; the parent engine (the \"broker\") reads our protocol lines\n# from stdout and writes replies to stdin.\n#\n# Protocol (one JSON object per line, no other framing):\n# child -> parent {\"id\": <int>, \"tool\": \"<name>\", \"args\": {...}}\n# parent -> child {\"id\": <int>, \"ok\": true, \"value\": <json>, \"truncated\": <bool>}\n# {\"id\": <int>, \"ok\": false, \"error\": \"<message>\"}\n# child -> parent {\"__final__\": <json>} (normal end: main()'s return)\n# {\"__error__\": \"<message>\"} (uncaught exception)\n#\n# Contract:\n# - Write the program as an `async def main():` FUNCTION BODY, or as a\n# complete script that defines main. main()'s return value (lossless JSON)\n# is the final result of the run.\n# - `tools.<name>(**args)` (also `tools.<name>({\"path\": ...})`) is a\n# SYNCHRONOUS bridge: every sub-call is dispatched by the parent through\n# its normal tool pipeline (guards, schema checks, limits). A failure\n# raises ToolCallError - catch it and continue.\n# - Only print what the model needs (stdout logs are budgeted at 64KiB);\n# intermediate tool results never enter the conversation automatically.\n# - Sub-calls run SERIALLY in P0: Python-side concurrency (the Promise.all\n# equivalent for independent read-only calls) is a documented follow-up.\n# - Exposed tools: read_file / write_file / list_dir / run_shell. Any other\n# name (including run_code itself) is rejected by the parent.\n# - Do not read sys.stdin and do not add an `if __name__ == \"__main__\"`\n# block: stdin is the protocol reply channel and this harness is the\n# entry point.\n# =============================================================================\nimport sys as _sys\nimport json as _json\nimport traceback as _traceback\n\n\nclass ToolCallError(Exception):\n \"\"\"A bridged tool call failed (guard denial, unknown tool, dispatch\n error, limit exceeded). Catchable: the program may recover and continue.\"\"\"\n\n def __init__(self, tool_name, message):\n super().__init__(f\"tool '{tool_name}' failed: {message}\")\n self.tool_name = tool_name\n\n\n_sub_call_id = 0\n\n\ndef _bridge_call(tool, args):\n global _sub_call_id\n _sub_call_id += 1\n request_id = _sub_call_id\n try:\n payload = _json.dumps(\n {\"id\": request_id, \"tool\": tool, \"args\": args}, ensure_ascii=False\n )\n except (TypeError, ValueError) as exc:\n raise ToolCallError(tool, f\"arguments are not JSON-serializable: {exc}\")\n print(payload, flush=True)\n line = _sys.stdin.readline()\n if not line:\n raise ToolCallError(tool, \"the parent broker closed the reply channel (run aborted)\")\n try:\n reply = _json.loads(line)\n except ValueError as exc:\n raise ToolCallError(tool, f\"malformed reply from the parent broker: {exc}\")\n if reply.get(\"id\") != request_id:\n raise ToolCallError(\n tool, f\"reply id mismatch (expected {request_id}, got {reply.get('id')})\"\n )\n if not reply.get(\"ok\", False):\n raise ToolCallError(tool, reply.get(\"error\", \"unknown error\"))\n return _Value(_attr(reply.get(\"value\")))\n\n\nclass _AttrDict(dict):\n \"\"\"dict whose keys are also attributes: s.stdout == s['stdout'].\n Models write both styles; both must work, including AFTER `await`.\"\"\"\n\n def __getattr__(self, name):\n try:\n return self[name]\n except KeyError:\n raise AttributeError(name)\n\n\ndef _attr(v):\n \"\"\"Recursively convert decoded JSON dicts/lists so every dict in a tool\n result is an _AttrDict (attribute access everywhere, at any depth).\"\"\"\n if isinstance(v, dict):\n return _AttrDict({k: _attr(x) for k, x in v.items()})\n if isinstance(v, list):\n return [_attr(x) for x in v]\n return v\n\n\nclass _Value:\n \"\"\"Dual-interface tool result: usable directly (indexing/iteration/str)\n AND awaitable. Models write both styles \u2014 `await tools.list_dir(...)`\n and `tools.list_dir(...)` must behave identically.\"\"\"\n\n def __init__(self, v):\n self._v = v\n\n def __await__(self):\n # Correct awaitable protocol: yield NOTHING and RETURN the value.\n # (Yielding the value itself makes asyncio treat it as an awaitable\n # and dict/list results blow up with \"Task got bad yield\".)\n if False:\n yield\n return self._v\n\n def __iter__(self):\n return iter(self._v)\n\n def __getitem__(self, k):\n return self._v[k]\n\n def __len__(self):\n return len(self._v)\n\n def __bool__(self):\n return bool(self._v)\n\n def __str__(self):\n return str(self._v)\n\n def __repr__(self):\n return repr(self._v)\n\n def __eq__(self, other):\n if isinstance(other, _Value):\n other = other._v\n return self._v == other\n\n def get(self, *a, **k):\n return self._v.get(*a, **k) if hasattr(self._v, \"get\") else None\n\n def __getattr__(self, name):\n # Method passthrough (splitlines, keys, ...) so the wrapper behaves\n # exactly like the wrapped value in normal code paths.\n return getattr(self._v, name)\n\n\ndef _merge_args(positional, kwargs, tool):\n if positional:\n if len(positional) == 1 and isinstance(positional[0], dict):\n merged = dict(positional[0])\n merged.update(kwargs)\n return merged\n raise ToolCallError(tool, \"expected a single dict argument and/or keyword arguments\")\n return dict(kwargs)\n\n\nclass _Tools:\n \"\"\"The SDK tool surface: only these four tools are bridged to the parent\n (read_file / write_file / list_dir / run_shell).\"\"\"\n\n def read_file(self, *args, **kwargs):\n return _bridge_call(\"read_file\", _merge_args(args, kwargs, \"read_file\"))\n\n def write_file(self, *args, **kwargs):\n return _bridge_call(\"write_file\", _merge_args(args, kwargs, \"write_file\"))\n\n def list_dir(self, *args, **kwargs):\n return _bridge_call(\"list_dir\", _merge_args(args, kwargs, \"list_dir\"))\n\n def run_shell(self, *args, **kwargs):\n # Returns the full result dict: {\"exit_code\", \"stdout\", \"stderr\", ...}.\n # Fields are readable both ways: s['stdout'] AND s.stdout, before or\n # after `await` (results are _AttrDict at any depth).\n return _bridge_call(\"run_shell\", _merge_args(args, kwargs, \"run_shell\"))\n\n\ntools = _Tools()\n";
22
+ /** The two languages `run_code` can run (W774: TypeScript is the default). */
23
+ export type RunCodeLanguage = "typescript" | "python";
24
+ /** What `run_code` runs when the call omits `language`. */
25
+ export declare const DEFAULT_RUN_CODE_LANGUAGE: RunCodeLanguage;
26
+ /**
27
+ * Assemble the program file for one language: SDK preamble + user code + runner.
28
+ *
29
+ * When the first non-blank line of the user code is indented it is treated as a
30
+ * **function body** and wrapped (`async def main():` / `async function main()`);
31
+ * otherwise it must be a complete script defining `main` itself (legacy
32
+ * `assemble_program`). The Python path is byte-for-byte what it always was; the
33
+ * TypeScript path lays the file out the same way and deliberately does NOT
34
+ * re-indent a wrapped body — JavaScript does not need it, and re-indenting would
35
+ * rewrite the contents of a template literal.
36
+ */
37
+ export declare function assembleProgram(userCode: string, language: RunCodeLanguage): string;
38
+ /** True when the first non-blank line starts with whitespace (body form). */
39
+ export declare function firstNonblankLineIndented(code: string): boolean;