@temporalio/agent-harness 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.
- package/LICENSE +21 -0
- package/README.md +5 -2
- package/dist/src/ai-sdks/google-genai-plugin/index.d.ts +6 -0
- package/dist/src/ai-sdks/google-genai-plugin/index.js +24 -0
- package/dist/src/ai-sdks/google-genai-plugin/interactions-conversation.d.ts +35 -0
- package/dist/src/ai-sdks/google-genai-plugin/interactions-conversation.js +178 -0
- package/dist/src/ai-sdks/google-genai-plugin/interactions-models.d.ts +8 -0
- package/dist/src/ai-sdks/google-genai-plugin/interactions-models.js +4 -0
- package/dist/src/ai-sdks/google-genai-plugin/interactions-workflow.d.ts +8 -0
- package/dist/src/ai-sdks/google-genai-plugin/interactions-workflow.js +15 -0
- package/dist/src/ai-sdks/google-genai-plugin/temporal-async-client.d.ts +10 -0
- package/dist/src/ai-sdks/google-genai-plugin/temporal-async-client.js +9 -0
- package/dist/src/ai-sdks/google-genai-plugin/temporal-interactions.d.ts +31 -0
- package/dist/src/ai-sdks/google-genai-plugin/temporal-interactions.js +43 -0
- package/dist/src/ai-sdks/google-genai-plugin/workflow.d.ts +16 -0
- package/dist/src/ai-sdks/google-genai-plugin/workflow.js +14 -0
- package/dist/src/ai-sdks/jev/evaluator.d.ts +38 -0
- package/dist/src/ai-sdks/jev/evaluator.js +155 -0
- package/dist/src/ai-sdks/openai-agents-harness.d.ts +94 -0
- package/dist/src/ai-sdks/openai-agents-harness.js +347 -0
- package/dist/src/harness/agent-protocol/agent-interface.d.ts +369 -0
- package/dist/src/harness/agent-protocol/agent-interface.js +222 -0
- package/dist/src/harness/agent-protocol/events.d.ts +10 -0
- package/dist/src/harness/agent-protocol/events.js +6 -0
- package/dist/src/harness/agent-protocol/index.d.ts +5 -0
- package/dist/src/harness/agent-protocol/index.js +7 -0
- package/dist/src/harness/agent-protocol/schema.d.ts +34 -0
- package/dist/src/harness/agent-protocol/schema.js +53 -0
- package/dist/src/harness/agent-protocol/subagent-interface.d.ts +25 -0
- package/dist/src/harness/agent-protocol/subagent-interface.js +17 -0
- package/dist/src/harness/agent-schema.d.ts +4 -0
- package/dist/src/harness/agent-schema.js +106 -0
- package/dist/src/harness/agent-workflow/errors.d.ts +26 -0
- package/dist/src/harness/agent-workflow/errors.js +67 -0
- package/dist/src/harness/agent-workflow/types.d.ts +88 -0
- package/dist/src/harness/agent-workflow/types.js +1 -0
- package/dist/src/harness/agent-workflow.d.ts +79 -0
- package/dist/src/harness/agent-workflow.js +824 -0
- package/dist/src/harness/agent.d.ts +137 -0
- package/dist/src/harness/agent.js +272 -0
- package/dist/src/harness/code-mode/activity-fs.d.ts +46 -0
- package/dist/src/harness/code-mode/activity-fs.js +102 -0
- package/dist/src/harness/code-mode/base64.d.ts +2 -0
- package/dist/src/harness/code-mode/base64.js +41 -0
- package/dist/src/harness/code-mode/driver.d.ts +78 -0
- package/dist/src/harness/code-mode/driver.js +266 -0
- package/dist/src/harness/code-mode/index.d.ts +4 -0
- package/dist/src/harness/code-mode/index.js +6 -0
- package/dist/src/harness/code-mode/mount-decl.d.ts +43 -0
- package/dist/src/harness/code-mode/mount-decl.js +100 -0
- package/dist/src/harness/code-mode/okf-tool.d.ts +10 -0
- package/dist/src/harness/code-mode/okf-tool.js +89 -0
- package/dist/src/harness/code-mode/okf.d.ts +91 -0
- package/dist/src/harness/code-mode/okf.js +315 -0
- package/dist/src/harness/code-mode/stubs.d.ts +13 -0
- package/dist/src/harness/code-mode/stubs.js +203 -0
- package/dist/src/harness/code-mode/texts.d.ts +4 -0
- package/dist/src/harness/code-mode/texts.js +6 -0
- package/dist/src/harness/code-mode/tool.d.ts +37 -0
- package/dist/src/harness/code-mode/tool.js +79 -0
- package/dist/src/harness/code-mode/vfs.d.ts +209 -0
- package/dist/src/harness/code-mode/vfs.js +862 -0
- package/dist/src/harness/py-repr.d.ts +2 -0
- package/dist/src/harness/py-repr.js +24 -0
- package/dist/src/harness/state/index.d.ts +40 -0
- package/dist/src/harness/state/index.js +142 -0
- package/dist/src/harness/stream-context.d.ts +8 -0
- package/dist/src/harness/stream-context.js +9 -0
- package/dist/src/harness/subagent-toolset.d.ts +29 -0
- package/dist/src/harness/subagent-toolset.js +87 -0
- package/dist/src/index.d.ts +12 -0
- package/dist/src/index.js +13 -0
- package/dist/src/sandbox.d.ts +12 -0
- package/package.json +95 -3
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
// Open Knowledge Format (OKF v0.2) as workflow-safe models and text.
|
|
2
|
+
// Parsing YAML and walking a bundle need a YAML library and a filesystem, so both stay in
|
|
3
|
+
// worker activities. Rendering a concept runs in the workflow, with a small block-style emitter
|
|
4
|
+
// instead of a Node YAML package.
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
export const OKF_VERSION = "0.2";
|
|
7
|
+
export const RESERVED_NAMES = new Set(["index.md", "log.md"]);
|
|
8
|
+
/** The most a walk reads, so a huge bundle can't stall the activity or the console. */
|
|
9
|
+
export const MAX_OKF_CONCEPTS = 1_000;
|
|
10
|
+
export const MAX_OKF_FILE_BYTES = 256_000;
|
|
11
|
+
export const MAX_OKF_TOTAL_BYTES = 8_000_000;
|
|
12
|
+
/** Without links only frontmatter is needed, so a walk reads each file's head first. */
|
|
13
|
+
export const OKF_HEAD_BYTES = 8_192;
|
|
14
|
+
export const UNCLOSED_FRONTMATTER = "frontmatter is never closed with a `---` line";
|
|
15
|
+
const TrustTier = z.enum(["unverified", "machine-confirmed", "human-reviewed"]);
|
|
16
|
+
export const OKFConcept = z
|
|
17
|
+
.object({
|
|
18
|
+
id: z.string(),
|
|
19
|
+
path: z.string(),
|
|
20
|
+
type: z.string(),
|
|
21
|
+
title: z.string(),
|
|
22
|
+
description: z.string().default(""),
|
|
23
|
+
tags: z.array(z.string()).default([]),
|
|
24
|
+
status: z.string().default("stable"),
|
|
25
|
+
trust_tier: TrustTier.default("unverified"),
|
|
26
|
+
stale: z.boolean().default(false),
|
|
27
|
+
size: z.number(),
|
|
28
|
+
problem: z.string().nullable().default(null),
|
|
29
|
+
})
|
|
30
|
+
.meta({ title: "OKFConcept" });
|
|
31
|
+
export const OKFLink = z
|
|
32
|
+
.object({
|
|
33
|
+
source: z.string(),
|
|
34
|
+
target: z.string(),
|
|
35
|
+
dangling: z.boolean().default(false),
|
|
36
|
+
})
|
|
37
|
+
.meta({ title: "OKFLink" });
|
|
38
|
+
export const OKFLinks = z
|
|
39
|
+
.object({
|
|
40
|
+
id: z.string(),
|
|
41
|
+
links_to: z.array(OKFLink),
|
|
42
|
+
cited_by: z.array(OKFLink),
|
|
43
|
+
})
|
|
44
|
+
.meta({ title: "OKFLinks" });
|
|
45
|
+
export const OKFGraph = z
|
|
46
|
+
.object({
|
|
47
|
+
concepts: z.array(OKFConcept),
|
|
48
|
+
links: z.array(OKFLink).default([]),
|
|
49
|
+
truncated: z.boolean().default(false),
|
|
50
|
+
walked_at: z.number(),
|
|
51
|
+
})
|
|
52
|
+
.meta({ title: "OKFGraph" });
|
|
53
|
+
/** `(frontmatter, body, problem)`. `frontmatter` is null when there is none. */
|
|
54
|
+
export function splitFrontmatter(text) {
|
|
55
|
+
const lines = splitKeep(text);
|
|
56
|
+
if (!lines.length || stripLine(lines[0]) !== "---")
|
|
57
|
+
return [null, text, "no frontmatter: the file doesn't start with a `---` line"];
|
|
58
|
+
for (let i = 1; i < lines.length; i++) {
|
|
59
|
+
if (stripLine(lines[i]) === "---")
|
|
60
|
+
return [lines.slice(1, i).join(""), lines.slice(i + 1).join(""), null];
|
|
61
|
+
}
|
|
62
|
+
return [null, text, UNCLOSED_FRONTMATTER];
|
|
63
|
+
}
|
|
64
|
+
const FENCE = /^\s{0,3}(```|~~~)/;
|
|
65
|
+
const SCHEME = /^[A-Za-z][A-Za-z0-9+.-]*:/;
|
|
66
|
+
const SPACE = /\s/;
|
|
67
|
+
const TARGET_END = /[)\s>]/;
|
|
68
|
+
/**
|
|
69
|
+
* The targets of the `](target "title")` links in `line`, exactly as
|
|
70
|
+
* `/\]\(\s*<?([^)\s>]+)>?(?:\s+"[^"]*")?\s*\)/g` finds them, in one pass: that regex retries at
|
|
71
|
+
* every `](`, which is quadratic in a long line of them.
|
|
72
|
+
*/
|
|
73
|
+
function linkTargetsIn(line) {
|
|
74
|
+
const found = [];
|
|
75
|
+
const skipSpace = (at) => {
|
|
76
|
+
while (at < line.length && SPACE.test(line[at]))
|
|
77
|
+
at++;
|
|
78
|
+
return at;
|
|
79
|
+
};
|
|
80
|
+
for (let open = line.indexOf("]("); open >= 0;) {
|
|
81
|
+
let at = skipSpace(open + 2);
|
|
82
|
+
const angle = line[at] === "<";
|
|
83
|
+
if (angle)
|
|
84
|
+
at++;
|
|
85
|
+
let start = at;
|
|
86
|
+
while (at < line.length && !TARGET_END.test(line[at]))
|
|
87
|
+
at++;
|
|
88
|
+
// A `<` with nothing after it is the target itself.
|
|
89
|
+
if (at === start && angle)
|
|
90
|
+
start--;
|
|
91
|
+
let end = -1;
|
|
92
|
+
if (at > start) {
|
|
93
|
+
const afterTarget = line[at] === ">" ? at + 1 : at;
|
|
94
|
+
const titleAt = skipSpace(afterTarget);
|
|
95
|
+
if (titleAt > afterTarget && line[titleAt] === '"') {
|
|
96
|
+
const close = line.indexOf('"', titleAt + 1);
|
|
97
|
+
if (close >= 0) {
|
|
98
|
+
const paren = skipSpace(close + 1);
|
|
99
|
+
if (line[paren] === ")")
|
|
100
|
+
end = paren + 1;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
if (end < 0) {
|
|
104
|
+
const paren = skipSpace(afterTarget);
|
|
105
|
+
if (line[paren] === ")")
|
|
106
|
+
end = paren + 1;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
if (end >= 0)
|
|
110
|
+
found.push(line.slice(start, at));
|
|
111
|
+
// After a failed `](`, every later one whose target starts inside this target ends where it
|
|
112
|
+
// did and fails too; the next to try is the one that ends just before that end.
|
|
113
|
+
open = line.indexOf("](", end >= 0 ? end : Math.max(open + 1, at - 2));
|
|
114
|
+
}
|
|
115
|
+
return found;
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Concept ids `body` links to, in order, without repeats. Relative targets resolve against
|
|
119
|
+
* `conceptPath`; a target starting with `/` is relative to the bundle root. Links inside
|
|
120
|
+
* fenced code blocks, and links to the concept itself, are left out.
|
|
121
|
+
*/
|
|
122
|
+
export function linkTargets(body, conceptPath) {
|
|
123
|
+
const found = [];
|
|
124
|
+
const here = parentPosix(conceptPath);
|
|
125
|
+
let fence = null;
|
|
126
|
+
for (const line of body.split(/\r\n|\n|\r/)) {
|
|
127
|
+
const opened = FENCE.exec(line);
|
|
128
|
+
if (opened) {
|
|
129
|
+
const marker = opened[1];
|
|
130
|
+
fence = fence === marker ? null : (fence ?? marker);
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
if (fence)
|
|
134
|
+
continue;
|
|
135
|
+
for (const raw of linkTargetsIn(line)) {
|
|
136
|
+
const target = resolveLink(decodePercent(raw), here);
|
|
137
|
+
if (target !== null && !found.includes(target))
|
|
138
|
+
found.push(target);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
const selfId = conceptPath.endsWith(".md") ? conceptPath.slice(0, -3) : conceptPath;
|
|
142
|
+
return found.filter((id) => id !== selfId);
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* A concept's full text: `frontmatter` as YAML between `---` lines, then `body`.
|
|
146
|
+
* Records `generated` unless it is already present, and stamps a `verified` entry that has no `at`.
|
|
147
|
+
*/
|
|
148
|
+
export function renderConcept(frontmatter, body, generated) {
|
|
149
|
+
const stamp = generated.at.toISOString().replace(/\.\d{3}Z$/, "Z");
|
|
150
|
+
const fields = { ...frontmatter };
|
|
151
|
+
if (!Object.hasOwn(fields, "generated"))
|
|
152
|
+
fields.generated = { by: generated.by, at: stamp };
|
|
153
|
+
const verified = fields.verified;
|
|
154
|
+
if (isPlainObject(verified))
|
|
155
|
+
fields.verified = stampVerified(verified, stamp);
|
|
156
|
+
else if (Array.isArray(verified))
|
|
157
|
+
fields.verified = verified.map((entry) => isPlainObject(entry) ? stampVerified(entry, stamp) : entry);
|
|
158
|
+
const text = body.replace(/^\n+/, "");
|
|
159
|
+
const block = dumpMap(fields, 0);
|
|
160
|
+
return `---\n${block}\n---\n\n${text}${text.endsWith("\n") ? "" : "\n"}`;
|
|
161
|
+
}
|
|
162
|
+
function stampVerified(entry, stamp) {
|
|
163
|
+
return { ...entry, at: Object.hasOwn(entry, "at") ? entry.at : stamp };
|
|
164
|
+
}
|
|
165
|
+
function splitKeep(text) {
|
|
166
|
+
const lines = [];
|
|
167
|
+
let start = 0;
|
|
168
|
+
for (let i = 0; i < text.length; i++) {
|
|
169
|
+
const ch = text[i];
|
|
170
|
+
if (ch !== "\n" && ch !== "\r")
|
|
171
|
+
continue;
|
|
172
|
+
const end = ch === "\r" && text[i + 1] === "\n" ? i + 2 : i + 1;
|
|
173
|
+
lines.push(text.slice(start, end));
|
|
174
|
+
start = end;
|
|
175
|
+
i = end - 1;
|
|
176
|
+
}
|
|
177
|
+
if (start < text.length || text.length === 0)
|
|
178
|
+
lines.push(text.slice(start));
|
|
179
|
+
if (text.length === 0)
|
|
180
|
+
return [];
|
|
181
|
+
return lines;
|
|
182
|
+
}
|
|
183
|
+
const stripLine = (line) => line.replace(/[\r\n]+$/, "").trim();
|
|
184
|
+
function parentPosix(path) {
|
|
185
|
+
const i = path.lastIndexOf("/");
|
|
186
|
+
return i === -1 ? "." : path.slice(0, i);
|
|
187
|
+
}
|
|
188
|
+
function decodePercent(value) {
|
|
189
|
+
try {
|
|
190
|
+
return decodeURIComponent(value);
|
|
191
|
+
}
|
|
192
|
+
catch {
|
|
193
|
+
return value;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
function resolveLink(target, here) {
|
|
197
|
+
const hash = target.indexOf("#");
|
|
198
|
+
const bare = hash === -1 ? target : target.slice(0, hash);
|
|
199
|
+
if (!bare || SCHEME.test(bare) || !bare.endsWith(".md"))
|
|
200
|
+
return null;
|
|
201
|
+
const joined = bare.startsWith("/")
|
|
202
|
+
? bare.replace(/^\/+/, "")
|
|
203
|
+
: here === "."
|
|
204
|
+
? bare
|
|
205
|
+
: `${here}/${bare}`;
|
|
206
|
+
const normal = posixNorm(joined);
|
|
207
|
+
if (normal === "." || normal === "" || normal === ".." || normal.startsWith("../"))
|
|
208
|
+
return null;
|
|
209
|
+
const name = normal.slice(normal.lastIndexOf("/") + 1);
|
|
210
|
+
if (RESERVED_NAMES.has(name))
|
|
211
|
+
return null;
|
|
212
|
+
return normal.slice(0, -3);
|
|
213
|
+
}
|
|
214
|
+
function posixNorm(path) {
|
|
215
|
+
const parts = [];
|
|
216
|
+
let up = 0;
|
|
217
|
+
for (const part of path.split("/")) {
|
|
218
|
+
if (part === "" || part === ".")
|
|
219
|
+
continue;
|
|
220
|
+
if (part === "..") {
|
|
221
|
+
if (parts.length)
|
|
222
|
+
parts.pop();
|
|
223
|
+
else
|
|
224
|
+
up++;
|
|
225
|
+
}
|
|
226
|
+
else
|
|
227
|
+
parts.push(part);
|
|
228
|
+
}
|
|
229
|
+
if (!parts.length)
|
|
230
|
+
return up ? `..${"/..".repeat(up - 1)}` : ".";
|
|
231
|
+
return `${up ? "../".repeat(up) : ""}${parts.join("/")}`;
|
|
232
|
+
}
|
|
233
|
+
function isPlainObject(value) {
|
|
234
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
235
|
+
}
|
|
236
|
+
// Quoted when YAML 1.1 would otherwise retype the string (a date, a bool, a number).
|
|
237
|
+
const BOOL_OR_NULL = /^(?:~|null|Null|NULL|y|Y|yes|Yes|YES|n|N|no|No|NO|true|True|TRUE|false|False|FALSE|on|On|ON|off|Off|OFF)$/;
|
|
238
|
+
const NUMBER = /^[+-]?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?$/;
|
|
239
|
+
const TIMESTAMP = /^\d{4}-\d{2}-\d{2}(?:[Tt ]|$)/;
|
|
240
|
+
function needsQuote(value) {
|
|
241
|
+
if (value === "" || value !== value.trim())
|
|
242
|
+
return true;
|
|
243
|
+
if (/[\n\r\t#{}[\],&*!|>'"%@`]/.test(value))
|
|
244
|
+
return true;
|
|
245
|
+
if (/: /.test(value) || /^[-?:]/.test(value))
|
|
246
|
+
return true;
|
|
247
|
+
return BOOL_OR_NULL.test(value) || NUMBER.test(value) || TIMESTAMP.test(value);
|
|
248
|
+
}
|
|
249
|
+
function emitString(value) {
|
|
250
|
+
if (!needsQuote(value))
|
|
251
|
+
return value;
|
|
252
|
+
return `'${value.replaceAll("'", "''")}'`;
|
|
253
|
+
}
|
|
254
|
+
function dumpScalar(value) {
|
|
255
|
+
if (value === null || value === undefined)
|
|
256
|
+
return "null";
|
|
257
|
+
if (typeof value === "boolean")
|
|
258
|
+
return value ? "true" : "false";
|
|
259
|
+
if (typeof value === "number")
|
|
260
|
+
return Number.isFinite(value) ? String(value) : "null";
|
|
261
|
+
if (typeof value === "string")
|
|
262
|
+
return emitString(value);
|
|
263
|
+
return emitString(String(value));
|
|
264
|
+
}
|
|
265
|
+
function dumpMap(obj, indent) {
|
|
266
|
+
const pad = " ".repeat(indent);
|
|
267
|
+
const lines = [];
|
|
268
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
269
|
+
if (value === undefined)
|
|
270
|
+
continue;
|
|
271
|
+
const name = emitString(key);
|
|
272
|
+
if (Array.isArray(value)) {
|
|
273
|
+
lines.push(value.length ? `${pad}${name}:\n${dumpList(value, indent)}` : `${pad}${name}: []`);
|
|
274
|
+
}
|
|
275
|
+
else if (isPlainObject(value)) {
|
|
276
|
+
const inner = dumpMap(value, indent + 2);
|
|
277
|
+
lines.push(inner ? `${pad}${name}:\n${inner}` : `${pad}${name}: {}`);
|
|
278
|
+
}
|
|
279
|
+
else
|
|
280
|
+
lines.push(`${pad}${name}: ${dumpScalar(value)}`);
|
|
281
|
+
}
|
|
282
|
+
return lines.join("\n");
|
|
283
|
+
}
|
|
284
|
+
function dumpList(items, indent) {
|
|
285
|
+
const pad = " ".repeat(indent);
|
|
286
|
+
const lines = [];
|
|
287
|
+
for (const item of items) {
|
|
288
|
+
if (isPlainObject(item)) {
|
|
289
|
+
const entries = Object.entries(item).filter(([, value]) => value !== undefined);
|
|
290
|
+
if (!entries.length) {
|
|
291
|
+
lines.push(`${pad}- {}`);
|
|
292
|
+
continue;
|
|
293
|
+
}
|
|
294
|
+
const [firstKey, firstValue] = entries[0];
|
|
295
|
+
lines.push(`${pad}- ${emitString(firstKey)}:${inlineOrBreak(firstValue, indent + 2)}`);
|
|
296
|
+
for (const [key, value] of entries.slice(1))
|
|
297
|
+
lines.push(`${" ".repeat(indent + 2)}${emitString(key)}:${inlineOrBreak(value, indent + 4)}`);
|
|
298
|
+
}
|
|
299
|
+
else if (Array.isArray(item)) {
|
|
300
|
+
lines.push(item.length ? `${pad}-\n${dumpList(item, indent + 2)}` : `${pad}- []`);
|
|
301
|
+
}
|
|
302
|
+
else
|
|
303
|
+
lines.push(`${pad}- ${dumpScalar(item)}`);
|
|
304
|
+
}
|
|
305
|
+
return lines.join("\n");
|
|
306
|
+
}
|
|
307
|
+
function inlineOrBreak(value, indent) {
|
|
308
|
+
if (Array.isArray(value))
|
|
309
|
+
return value.length ? `\n${dumpList(value, indent - 2)}` : " []";
|
|
310
|
+
if (isPlainObject(value)) {
|
|
311
|
+
const inner = dumpMap(value, indent);
|
|
312
|
+
return inner ? `\n${inner}` : " {}";
|
|
313
|
+
}
|
|
314
|
+
return ` ${dumpScalar(value)}`;
|
|
315
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { AnyHarnessTool } from "../agent-workflow/types.ts";
|
|
2
|
+
import { pyRepr } from "../py-repr.ts";
|
|
3
|
+
export { pyRepr };
|
|
4
|
+
export interface TypeCheckStubs {
|
|
5
|
+
source: string;
|
|
6
|
+
type_names: string[];
|
|
7
|
+
}
|
|
8
|
+
export declare class CodeModeStubError extends Error {
|
|
9
|
+
name: string;
|
|
10
|
+
}
|
|
11
|
+
export type Schema = Record<string, unknown>;
|
|
12
|
+
export declare function renderTypeCheckStubs(tools: readonly AnyHarnessTool[]): TypeCheckStubs;
|
|
13
|
+
export declare function renderHostInterface(tools: readonly AnyHarnessTool[]): string;
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
// The host functions as the model reads them, and as Python stubs Monty type-checks scripts
|
|
2
|
+
// against.
|
|
3
|
+
import { pyRepr } from "../py-repr.js";
|
|
4
|
+
export { pyRepr };
|
|
5
|
+
import { toJsonSchema } from "../agent-protocol/index.js";
|
|
6
|
+
// ---------------------------------------------------------------------------------------------
|
|
7
|
+
// Python stubs from JSON Schema
|
|
8
|
+
// ---------------------------------------------------------------------------------------------
|
|
9
|
+
export class CodeModeStubError extends Error {
|
|
10
|
+
name = "CodeModeStubError";
|
|
11
|
+
}
|
|
12
|
+
const SEPARATOR = /[_\-\s(). ]/;
|
|
13
|
+
const WORD = /\w/;
|
|
14
|
+
/**
|
|
15
|
+
* `s` in PascalCase without non-word characters: what
|
|
16
|
+
* `s.replace(/(^|[_\-\s(). ]+)(\w)/g, upper).replace(/\W/g, "")` makes, in one pass (that regex
|
|
17
|
+
* retries inside every run of separators, which is quadratic).
|
|
18
|
+
*/
|
|
19
|
+
function pascal(s) {
|
|
20
|
+
let out = "";
|
|
21
|
+
let i = 0;
|
|
22
|
+
if (s.length > 0 && WORD.test(s[0]))
|
|
23
|
+
out += s[i++].toUpperCase();
|
|
24
|
+
while (i < s.length) {
|
|
25
|
+
let run = i;
|
|
26
|
+
while (run < s.length && SEPARATOR.test(s[run]))
|
|
27
|
+
run++;
|
|
28
|
+
if (run === i) {
|
|
29
|
+
out += s[i++];
|
|
30
|
+
}
|
|
31
|
+
else if (run < s.length && WORD.test(s[run])) {
|
|
32
|
+
out += s[run].toUpperCase();
|
|
33
|
+
i = run + 1;
|
|
34
|
+
}
|
|
35
|
+
else {
|
|
36
|
+
// No word follows the run: the regex backs off to the run's last `_` after its first
|
|
37
|
+
// character, which is a word character too, and replaces the span with it.
|
|
38
|
+
let underscore = run - 1;
|
|
39
|
+
while (underscore > i && s[underscore] !== "_")
|
|
40
|
+
underscore--;
|
|
41
|
+
if (underscore > i) {
|
|
42
|
+
out += "_";
|
|
43
|
+
i = underscore + 1;
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
out += s.slice(i, run);
|
|
47
|
+
i = run;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return out.replace(/\W/g, "");
|
|
52
|
+
}
|
|
53
|
+
const pyLiteral = (v) => typeof v === "string"
|
|
54
|
+
? JSON.stringify(v)
|
|
55
|
+
: v === true
|
|
56
|
+
? "True"
|
|
57
|
+
: v === false
|
|
58
|
+
? "False"
|
|
59
|
+
: v === null
|
|
60
|
+
? "None"
|
|
61
|
+
: String(v);
|
|
62
|
+
class StubBuilder {
|
|
63
|
+
#classes = [];
|
|
64
|
+
#byKey = new Map();
|
|
65
|
+
#used = new Set();
|
|
66
|
+
#imports = new Set();
|
|
67
|
+
build(tools, withDoc) {
|
|
68
|
+
const funcs = tools.map((t) => this.#renderTool(t, withDoc));
|
|
69
|
+
if (this.#classes.length)
|
|
70
|
+
this.#imports.add("TypedDict");
|
|
71
|
+
const header = ["from __future__ import annotations"];
|
|
72
|
+
if (this.#imports.size)
|
|
73
|
+
header.push(`from typing import ${[...this.#imports].sort().join(", ")}`);
|
|
74
|
+
const sections = [header.join("\n")];
|
|
75
|
+
if (this.#classes.length) {
|
|
76
|
+
sections.push(this.#classes
|
|
77
|
+
.map((c) => `class ${c.name}(TypedDict):\n${c.fields.length ? c.fields.join("\n") : " pass"}`)
|
|
78
|
+
.join("\n\n"));
|
|
79
|
+
}
|
|
80
|
+
if (funcs.length)
|
|
81
|
+
sections.push(funcs.join("\n\n"));
|
|
82
|
+
return `${sections.join("\n\n\n")}\n`;
|
|
83
|
+
}
|
|
84
|
+
typeNames() {
|
|
85
|
+
return this.#classes.map((c) => c.name);
|
|
86
|
+
}
|
|
87
|
+
#renderTool(tool, withDoc) {
|
|
88
|
+
if (tool.outputSchema === undefined) {
|
|
89
|
+
throw new CodeModeStubError(`tool '${tool.name}' has no output schema; Code Mode requires a typed result so scripts can be checked against it.`);
|
|
90
|
+
}
|
|
91
|
+
const input = tool.parameters;
|
|
92
|
+
const required = new Set(input.required ?? []);
|
|
93
|
+
const props = Object.entries(input.properties ?? {});
|
|
94
|
+
const params = [
|
|
95
|
+
...props
|
|
96
|
+
.filter(([n]) => required.has(n))
|
|
97
|
+
.map(([n, s]) => `${n}: ${this.#type(s, input, `${tool.name}(${n})`)}`),
|
|
98
|
+
...props
|
|
99
|
+
.filter(([n]) => !required.has(n))
|
|
100
|
+
.map(([n, s]) => `${n}: ${this.#type(s, input, `${tool.name}(${n})`)} = ...`),
|
|
101
|
+
];
|
|
102
|
+
const outSchema = toJsonSchema(tool.outputSchema, "output");
|
|
103
|
+
const signature = `async def ${tool.name}(${params.join(", ")}) -> ${this.#type(outSchema, outSchema, `${tool.name} return`)}`;
|
|
104
|
+
const text = tool.description.trim().replaceAll('"""', '\\"\\"\\"');
|
|
105
|
+
if (!withDoc || !text)
|
|
106
|
+
return `${signature}: ...`;
|
|
107
|
+
const doc = text
|
|
108
|
+
.split("\n")
|
|
109
|
+
.map((line) => ` ${line}`.trimEnd())
|
|
110
|
+
.join("\n");
|
|
111
|
+
return `${signature}:\n """\n${doc}\n """`;
|
|
112
|
+
}
|
|
113
|
+
#type(schema, root, ctx) {
|
|
114
|
+
if (typeof schema.$ref === "string") {
|
|
115
|
+
const key = schema.$ref.replace(/^#\/(\$defs|definitions)\//, "");
|
|
116
|
+
const target = (root.$defs ?? root.definitions)?.[key];
|
|
117
|
+
if (target === undefined)
|
|
118
|
+
throw new CodeModeStubError(`${ctx}: unresolvable $ref ${schema.$ref}`);
|
|
119
|
+
return this.#type({ title: key, ...target }, root, ctx);
|
|
120
|
+
}
|
|
121
|
+
const union = (schema.anyOf ?? schema.oneOf);
|
|
122
|
+
if (union)
|
|
123
|
+
return [...new Set(union.map((s) => this.#type(s, root, ctx)))].join(" | ");
|
|
124
|
+
if (schema.const !== undefined) {
|
|
125
|
+
this.#imports.add("Literal");
|
|
126
|
+
return `Literal[${pyLiteral(schema.const)}]`;
|
|
127
|
+
}
|
|
128
|
+
if (Array.isArray(schema.enum)) {
|
|
129
|
+
this.#imports.add("Literal");
|
|
130
|
+
return `Literal[${schema.enum.map(pyLiteral).join(", ")}]`;
|
|
131
|
+
}
|
|
132
|
+
const type = schema.type;
|
|
133
|
+
if (Array.isArray(type))
|
|
134
|
+
return type.map((t) => this.#type({ ...schema, type: t }, root, ctx)).join(" | ");
|
|
135
|
+
switch (type) {
|
|
136
|
+
case "string":
|
|
137
|
+
return "str";
|
|
138
|
+
case "integer":
|
|
139
|
+
return "int";
|
|
140
|
+
case "number":
|
|
141
|
+
return "float";
|
|
142
|
+
case "boolean":
|
|
143
|
+
return "bool";
|
|
144
|
+
case "null":
|
|
145
|
+
return "None";
|
|
146
|
+
case "array": {
|
|
147
|
+
if (typeof schema.items !== "object" || schema.items === null) {
|
|
148
|
+
throw new CodeModeStubError(`${ctx}: array without an element type; give its items a schema for static validation.`);
|
|
149
|
+
}
|
|
150
|
+
return `list[${this.#type(schema.items, root, ctx)}]`;
|
|
151
|
+
}
|
|
152
|
+
case "object": {
|
|
153
|
+
if (schema.properties !== undefined)
|
|
154
|
+
return this.#typedDict(schema, root, ctx);
|
|
155
|
+
const extra = schema.additionalProperties;
|
|
156
|
+
if (typeof extra === "object" && extra !== null)
|
|
157
|
+
return `dict[str, ${this.#type(extra, root, ctx)}]`;
|
|
158
|
+
this.#imports.add("Any");
|
|
159
|
+
return "dict[str, Any]";
|
|
160
|
+
}
|
|
161
|
+
case undefined:
|
|
162
|
+
this.#imports.add("Any");
|
|
163
|
+
return "Any";
|
|
164
|
+
default:
|
|
165
|
+
throw new CodeModeStubError(`${ctx}: cannot represent JSON Schema type ${JSON.stringify(type)} in Code Mode stubs.`);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
#typedDict(schema, root, ctx) {
|
|
169
|
+
// A model's input and output schemas differ only in these, and render to the same TypedDict.
|
|
170
|
+
const { $schema: _s, additionalProperties: _a, ...shape } = schema;
|
|
171
|
+
const key = JSON.stringify(shape);
|
|
172
|
+
const known = this.#byKey.get(key);
|
|
173
|
+
if (known !== undefined)
|
|
174
|
+
return known;
|
|
175
|
+
let name = typeof schema.title === "string"
|
|
176
|
+
? pascal(schema.title)
|
|
177
|
+
: pascal(ctx.replace(/ return$/, "_result"));
|
|
178
|
+
for (let i = 2; this.#used.has(name); i++)
|
|
179
|
+
name = `${name.replace(/_\d+$/, "")}_${i}`;
|
|
180
|
+
this.#used.add(name);
|
|
181
|
+
this.#byKey.set(key, name);
|
|
182
|
+
const entry = { name, fields: [] };
|
|
183
|
+
this.#classes.push(entry);
|
|
184
|
+
const required = new Set(schema.required ?? []);
|
|
185
|
+
for (const [field, fieldSchema] of Object.entries(schema.properties)) {
|
|
186
|
+
let rendered = this.#type(fieldSchema, root, `${name}.${field}`);
|
|
187
|
+
if (!required.has(field)) {
|
|
188
|
+
this.#imports.add("NotRequired");
|
|
189
|
+
rendered = `NotRequired[${rendered}]`;
|
|
190
|
+
}
|
|
191
|
+
entry.fields.push(` ${field}: ${rendered}`);
|
|
192
|
+
}
|
|
193
|
+
return name;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
export function renderTypeCheckStubs(tools) {
|
|
197
|
+
const builder = new StubBuilder();
|
|
198
|
+
const source = builder.build(tools, false);
|
|
199
|
+
return { source, type_names: builder.typeNames() };
|
|
200
|
+
}
|
|
201
|
+
export function renderHostInterface(tools) {
|
|
202
|
+
return new StubBuilder().build(tools, true);
|
|
203
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export declare const CONTRACT = "Run a Python `script` in a sandbox and return its output. Accomplish tasks by WRITING CODE that calls the host functions listed below \u2014 using variables, loops, conditionals, comprehensions and arithmetic to combine many calls in one go \u2014 instead of calling tools one at a time.\n\n{sandbox} The host functions and the types below are ALREADY IN SCOPE \u2014 never import them (there is no module to import them from). Calling a `TypedDict` type builds a plain dict, as in Python. The host functions are ASYNC \u2014 you MUST `await` them \u2014 so structure every script like this:\n\n import asyncio\n async def main():\n ... # await host functions here\n return <final value>\n asyncio.run(main())\n\nThe value of the script's LAST EXPRESSION becomes the result (along with anything you `print`). Run INDEPENDENT host calls CONCURRENTLY with `asyncio.gather(...)`; only await sequentially when a later call needs an earlier call's result. Host results come back as plain dicts/lists/scalars \u2014 index into them with normal Python (e.g. `results[0][\"field\"]`). A host function that fails RAISES at its `await`: catch it with `try`/`except Exception` to handle the failure, or let it propagate to end the script with an error.\n\nYour script is STATICALLY TYPE-CHECKED against the host-function signatures below BEFORE it runs: a wrong argument type, or reading a result key that doesn't exist, comes back as an error to fix rather than a result. The signatures and result shapes below are exact \u2014 only the listed keys exist.\n\nHost functions available:\n\n{interface}\n{filesystem}{guide}";
|
|
2
|
+
export declare const FILESYSTEM_STUB = "import os as _os\nimport typing as _typing\ndef open(file: str | _os.PathLike[str], mode: str = \"r\", encoding: str | None = None) -> _typing.IO[_typing.Any]: ...\nclass FileNotFoundError(OSError): ...\nclass FileExistsError(OSError): ...\nclass IsADirectoryError(OSError): ...\nclass NotADirectoryError(OSError): ...\nclass PermissionError(OSError): ...";
|
|
3
|
+
export declare const NO_FILESYSTEM = "The sandbox has no filesystem, no network, and no imports except `asyncio` and the host functions below.";
|
|
4
|
+
export declare const WITH_FILESYSTEM = "The sandbox has no network, and no imports except `asyncio`, `pathlib` and the host functions below. It has the filesystem listed under Filesystem below: read and write it with `open()` and `pathlib.Path`. File operations are synchronous (never `await` them) and work on whole files; read lines with `f.read().splitlines()`, not `for line in f`. A failed file operation raises an `OSError` subclass (`FileNotFoundError`, `FileExistsError`, `IsADirectoryError`, `NotADirectoryError`, `PermissionError`) whose message names the path; it has no `errno`.";
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
// GENERATED from contract/code_mode/ by //typescript/packages/core/src/harness/code-mode:contract_texts (harness_texts).
|
|
2
|
+
// Do not edit: change the text files and run `bazel run //typescript/packages/core/src/harness/code-mode:contract_texts_update`.
|
|
3
|
+
export const CONTRACT = "Run a Python `script` in a sandbox and return its output. Accomplish tasks by WRITING CODE that calls the host functions listed below — using variables, loops, conditionals, comprehensions and arithmetic to combine many calls in one go — instead of calling tools one at a time.\n\n{sandbox} The host functions and the types below are ALREADY IN SCOPE — never import them (there is no module to import them from). Calling a `TypedDict` type builds a plain dict, as in Python. The host functions are ASYNC — you MUST `await` them — so structure every script like this:\n\n import asyncio\n async def main():\n ... # await host functions here\n return <final value>\n asyncio.run(main())\n\nThe value of the script's LAST EXPRESSION becomes the result (along with anything you `print`). Run INDEPENDENT host calls CONCURRENTLY with `asyncio.gather(...)`; only await sequentially when a later call needs an earlier call's result. Host results come back as plain dicts/lists/scalars — index into them with normal Python (e.g. `results[0][\"field\"]`). A host function that fails RAISES at its `await`: catch it with `try`/`except Exception` to handle the failure, or let it propagate to end the script with an error.\n\nYour script is STATICALLY TYPE-CHECKED against the host-function signatures below BEFORE it runs: a wrong argument type, or reading a result key that doesn't exist, comes back as an error to fix rather than a result. The signatures and result shapes below are exact — only the listed keys exist.\n\nHost functions available:\n\n{interface}\n{filesystem}{guide}";
|
|
4
|
+
export const FILESYSTEM_STUB = "import os as _os\nimport typing as _typing\ndef open(file: str | _os.PathLike[str], mode: str = \"r\", encoding: str | None = None) -> _typing.IO[_typing.Any]: ...\nclass FileNotFoundError(OSError): ...\nclass FileExistsError(OSError): ...\nclass IsADirectoryError(OSError): ...\nclass NotADirectoryError(OSError): ...\nclass PermissionError(OSError): ...";
|
|
5
|
+
export const NO_FILESYSTEM = "The sandbox has no filesystem, no network, and no imports except `asyncio` and the host functions below.";
|
|
6
|
+
export const WITH_FILESYSTEM = "The sandbox has no network, and no imports except `asyncio`, `pathlib` and the host functions below. It has the filesystem listed under Filesystem below: read and write it with `open()` and `pathlib.Path`. File operations are synchronous (never `await` them) and work on whole files; read lines with `f.read().splitlines()`, not `for line in f`. A failed file operation raises an `OSError` subclass (`FileNotFoundError`, `FileExistsError`, `IsADirectoryError`, `NotADirectoryError`, `PermissionError`) whose message names the path; it has no `errno`.";
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { type AnyHarnessTool, type HarnessTool } from "../agent.ts";
|
|
2
|
+
import { type TypeCheckStubs } from "./stubs.ts";
|
|
3
|
+
import { type Mount } from "./vfs.ts";
|
|
4
|
+
/** The model-facing listing of the mounts. */
|
|
5
|
+
export declare function renderFilesystem(mounts: readonly Mount[]): string;
|
|
6
|
+
export interface CodeModeOptions {
|
|
7
|
+
name: string;
|
|
8
|
+
/** Applies to the run-code tool only; each host call re-enters the gate under its own tool. */
|
|
9
|
+
inherentlySafe?: boolean;
|
|
10
|
+
autoApprovalCriteria?: string;
|
|
11
|
+
/** Values for the tools' injected parameters, supplied to every host call. */
|
|
12
|
+
injections?: Record<string, unknown>;
|
|
13
|
+
/** Bounds one sandbox step's activity (compile-to-first-batch or resume-to-next-batch). */
|
|
14
|
+
stepTimeout?: string | number;
|
|
15
|
+
/**
|
|
16
|
+
* Extra contract text appended for the model (Python's OKF guide). Empty leaves the
|
|
17
|
+
* description unchanged.
|
|
18
|
+
*/
|
|
19
|
+
guide?: string;
|
|
20
|
+
/**
|
|
21
|
+
* The sandbox's virtual filesystem, as mounts at distinct, non-nested paths. Each
|
|
22
|
+
* file operation a script makes is carried out as fs_* tool calls through the runner, so it is
|
|
23
|
+
* subject to the approval policy and publishes tool lifecycle events. Build the mounts' backends
|
|
24
|
+
* in `create`, so each workflow has its own.
|
|
25
|
+
*/
|
|
26
|
+
mounts?: readonly Mount[];
|
|
27
|
+
}
|
|
28
|
+
export type CodeModeTool = HarnessTool<{
|
|
29
|
+
script: string;
|
|
30
|
+
}, string> & {
|
|
31
|
+
/** The type-check stubs scripts are checked against. */
|
|
32
|
+
readonly codeModeStubs: TypeCheckStubs;
|
|
33
|
+
readonly stepTimeout: string | number;
|
|
34
|
+
};
|
|
35
|
+
export declare function codeModeTool(tools: readonly AnyHarnessTool[], options: CodeModeOptions): CodeModeTool;
|
|
36
|
+
/** Type-check a `script` against a Code Mode tool's host functions without running it. */
|
|
37
|
+
export declare function codeModeTypeCheck(codeTool: CodeModeTool, script: string): Promise<string | null>;
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
// Code Mode: expose a set of harness tools to a model as ONE tool that runs a model-written Python
|
|
2
|
+
// script in Monty, the Python harness's sandbox, where the tools are async host functions.
|
|
3
|
+
// Independent host calls awaited together run concurrently as durable tool calls, and every host
|
|
4
|
+
// call goes through its own tool's approval gate and lifecycle. The script runs to completion in
|
|
5
|
+
// a `CodeModeDriver` (driver.ts). What the model is told is contract/code_mode/'s text
|
|
6
|
+
// (texts.ts).
|
|
7
|
+
import { proxyActivities } from "@temporalio/workflow";
|
|
8
|
+
import { z } from "zod";
|
|
9
|
+
import { tool as harnessTool } from "../agent.js";
|
|
10
|
+
import { currentToolCall } from "../agent-workflow.js";
|
|
11
|
+
import { CodeModeDriver, DEFAULT_STEP_TIMEOUT, STEP_RETRY, } from "./driver.js";
|
|
12
|
+
import { renderHostInterface, renderTypeCheckStubs } from "./stubs.js";
|
|
13
|
+
import { CONTRACT, FILESYSTEM_STUB, NO_FILESYSTEM, WITH_FILESYSTEM } from "./texts.js";
|
|
14
|
+
import { validateMounts } from "./vfs.js";
|
|
15
|
+
/** The model-facing listing of the mounts. */
|
|
16
|
+
export function renderFilesystem(mounts) {
|
|
17
|
+
if (!mounts.length)
|
|
18
|
+
return "";
|
|
19
|
+
const lines = mounts.map((m) => `- \`${m.path}\` (${m.readOnly ? "read-only" : "writable"})${m.description ? `: ${m.description}` : ""}`);
|
|
20
|
+
return `\nFilesystem:\n\n${lines.join("\n")}\n`;
|
|
21
|
+
}
|
|
22
|
+
function validateTools(tools) {
|
|
23
|
+
const byName = new Map();
|
|
24
|
+
for (const tool of tools) {
|
|
25
|
+
if (byName.has(tool.name))
|
|
26
|
+
throw new TypeError(`duplicate host-function name '${tool.name}' in this codeModeTool.`);
|
|
27
|
+
byName.set(tool.name, tool);
|
|
28
|
+
}
|
|
29
|
+
return byName;
|
|
30
|
+
}
|
|
31
|
+
export function codeModeTool(tools, options) {
|
|
32
|
+
const mounts = validateMounts(options.mounts ?? []);
|
|
33
|
+
if (tools.length === 0 && mounts.length === 0)
|
|
34
|
+
throw new TypeError("codeModeTool requires at least one tool or mount.");
|
|
35
|
+
const toolsByName = validateTools(tools);
|
|
36
|
+
const stepTimeout = options.stepTimeout ?? DEFAULT_STEP_TIMEOUT;
|
|
37
|
+
// Generated once at construction: an unrenderable type throws now, not at script time.
|
|
38
|
+
const rendered = renderTypeCheckStubs(tools);
|
|
39
|
+
const stubs = mounts.length
|
|
40
|
+
? { ...rendered, source: `${rendered.source}\n${FILESYSTEM_STUB}\n` }
|
|
41
|
+
: rendered;
|
|
42
|
+
const fill = {
|
|
43
|
+
sandbox: mounts.length ? WITH_FILESYSTEM : NO_FILESYSTEM,
|
|
44
|
+
interface: tools.length ? renderHostInterface(tools) : "(none)",
|
|
45
|
+
filesystem: renderFilesystem(mounts),
|
|
46
|
+
guide: options.guide ?? "",
|
|
47
|
+
};
|
|
48
|
+
// One pass, so a placeholder inside a filled-in value is left as it is.
|
|
49
|
+
const description = CONTRACT.replace(/\{(sandbox|interface|filesystem|guide)\}/g, (_, key) => fill[key]);
|
|
50
|
+
const injections = options.injections ?? {};
|
|
51
|
+
const tool = harnessTool({
|
|
52
|
+
name: options.name,
|
|
53
|
+
description,
|
|
54
|
+
inputSchema: z.object({ script: z.string() }),
|
|
55
|
+
outputSchema: z.string(),
|
|
56
|
+
inherentlySafe: options.inherentlySafe ?? true,
|
|
57
|
+
...(options.autoApprovalCriteria ? { autoApprovalCriteria: options.autoApprovalCriteria } : {}),
|
|
58
|
+
execute: async ({ script }) => {
|
|
59
|
+
const { runner } = currentToolCall(`codeModeTool '${options.name}'`);
|
|
60
|
+
const driver = new CodeModeDriver(runner, toolsByName, {
|
|
61
|
+
injections,
|
|
62
|
+
stubs,
|
|
63
|
+
mounts,
|
|
64
|
+
stepTimeout,
|
|
65
|
+
});
|
|
66
|
+
return driver.runScript(script);
|
|
67
|
+
},
|
|
68
|
+
});
|
|
69
|
+
// Read back by codeModeTypeCheck, so a script can be checked against these exact stubs.
|
|
70
|
+
return Object.assign(tool, { codeModeStubs: stubs, stepTimeout });
|
|
71
|
+
}
|
|
72
|
+
/** Type-check a `script` against a Code Mode tool's host functions without running it. */
|
|
73
|
+
export async function codeModeTypeCheck(codeTool, script) {
|
|
74
|
+
const { code_type_check } = proxyActivities({
|
|
75
|
+
startToCloseTimeout: codeTool.stepTimeout,
|
|
76
|
+
retry: STEP_RETRY,
|
|
77
|
+
});
|
|
78
|
+
return (await code_type_check(script, codeTool.codeModeStubs)) || null;
|
|
79
|
+
}
|