@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.
Files changed (74) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +5 -2
  3. package/dist/src/ai-sdks/google-genai-plugin/index.d.ts +6 -0
  4. package/dist/src/ai-sdks/google-genai-plugin/index.js +24 -0
  5. package/dist/src/ai-sdks/google-genai-plugin/interactions-conversation.d.ts +35 -0
  6. package/dist/src/ai-sdks/google-genai-plugin/interactions-conversation.js +178 -0
  7. package/dist/src/ai-sdks/google-genai-plugin/interactions-models.d.ts +8 -0
  8. package/dist/src/ai-sdks/google-genai-plugin/interactions-models.js +4 -0
  9. package/dist/src/ai-sdks/google-genai-plugin/interactions-workflow.d.ts +8 -0
  10. package/dist/src/ai-sdks/google-genai-plugin/interactions-workflow.js +15 -0
  11. package/dist/src/ai-sdks/google-genai-plugin/temporal-async-client.d.ts +10 -0
  12. package/dist/src/ai-sdks/google-genai-plugin/temporal-async-client.js +9 -0
  13. package/dist/src/ai-sdks/google-genai-plugin/temporal-interactions.d.ts +31 -0
  14. package/dist/src/ai-sdks/google-genai-plugin/temporal-interactions.js +43 -0
  15. package/dist/src/ai-sdks/google-genai-plugin/workflow.d.ts +16 -0
  16. package/dist/src/ai-sdks/google-genai-plugin/workflow.js +14 -0
  17. package/dist/src/ai-sdks/jev/evaluator.d.ts +38 -0
  18. package/dist/src/ai-sdks/jev/evaluator.js +155 -0
  19. package/dist/src/ai-sdks/openai-agents-harness.d.ts +94 -0
  20. package/dist/src/ai-sdks/openai-agents-harness.js +347 -0
  21. package/dist/src/harness/agent-protocol/agent-interface.d.ts +369 -0
  22. package/dist/src/harness/agent-protocol/agent-interface.js +222 -0
  23. package/dist/src/harness/agent-protocol/events.d.ts +10 -0
  24. package/dist/src/harness/agent-protocol/events.js +6 -0
  25. package/dist/src/harness/agent-protocol/index.d.ts +5 -0
  26. package/dist/src/harness/agent-protocol/index.js +7 -0
  27. package/dist/src/harness/agent-protocol/schema.d.ts +34 -0
  28. package/dist/src/harness/agent-protocol/schema.js +53 -0
  29. package/dist/src/harness/agent-protocol/subagent-interface.d.ts +25 -0
  30. package/dist/src/harness/agent-protocol/subagent-interface.js +17 -0
  31. package/dist/src/harness/agent-schema.d.ts +4 -0
  32. package/dist/src/harness/agent-schema.js +106 -0
  33. package/dist/src/harness/agent-workflow/errors.d.ts +26 -0
  34. package/dist/src/harness/agent-workflow/errors.js +67 -0
  35. package/dist/src/harness/agent-workflow/types.d.ts +88 -0
  36. package/dist/src/harness/agent-workflow/types.js +1 -0
  37. package/dist/src/harness/agent-workflow.d.ts +79 -0
  38. package/dist/src/harness/agent-workflow.js +824 -0
  39. package/dist/src/harness/agent.d.ts +137 -0
  40. package/dist/src/harness/agent.js +272 -0
  41. package/dist/src/harness/code-mode/activity-fs.d.ts +46 -0
  42. package/dist/src/harness/code-mode/activity-fs.js +102 -0
  43. package/dist/src/harness/code-mode/base64.d.ts +2 -0
  44. package/dist/src/harness/code-mode/base64.js +41 -0
  45. package/dist/src/harness/code-mode/driver.d.ts +78 -0
  46. package/dist/src/harness/code-mode/driver.js +266 -0
  47. package/dist/src/harness/code-mode/index.d.ts +4 -0
  48. package/dist/src/harness/code-mode/index.js +6 -0
  49. package/dist/src/harness/code-mode/mount-decl.d.ts +43 -0
  50. package/dist/src/harness/code-mode/mount-decl.js +100 -0
  51. package/dist/src/harness/code-mode/okf-tool.d.ts +10 -0
  52. package/dist/src/harness/code-mode/okf-tool.js +89 -0
  53. package/dist/src/harness/code-mode/okf.d.ts +91 -0
  54. package/dist/src/harness/code-mode/okf.js +315 -0
  55. package/dist/src/harness/code-mode/stubs.d.ts +13 -0
  56. package/dist/src/harness/code-mode/stubs.js +203 -0
  57. package/dist/src/harness/code-mode/texts.d.ts +4 -0
  58. package/dist/src/harness/code-mode/texts.js +6 -0
  59. package/dist/src/harness/code-mode/tool.d.ts +37 -0
  60. package/dist/src/harness/code-mode/tool.js +79 -0
  61. package/dist/src/harness/code-mode/vfs.d.ts +209 -0
  62. package/dist/src/harness/code-mode/vfs.js +862 -0
  63. package/dist/src/harness/py-repr.d.ts +2 -0
  64. package/dist/src/harness/py-repr.js +24 -0
  65. package/dist/src/harness/state/index.d.ts +40 -0
  66. package/dist/src/harness/state/index.js +142 -0
  67. package/dist/src/harness/stream-context.d.ts +8 -0
  68. package/dist/src/harness/stream-context.js +9 -0
  69. package/dist/src/harness/subagent-toolset.d.ts +29 -0
  70. package/dist/src/harness/subagent-toolset.js +87 -0
  71. package/dist/src/index.d.ts +12 -0
  72. package/dist/src/index.js +13 -0
  73. package/dist/src/sandbox.d.ts +12 -0
  74. package/package.json +95 -3
@@ -0,0 +1,266 @@
1
+ // The workflow-side host-call driver that runs one Code Mode script to completion.
2
+ //
3
+ // Each step runs in the sandbox as an activity (the worker package's code-mode/monty-stepper.ts):
4
+ // the first type-checks the script and runs it to its first awaited batch of host calls, and each
5
+ // later one resumes the suspended script with that batch's results, until the script finishes. Its
6
+ // output and final value (or a script error) come back as text.
7
+ //
8
+ // The host-call surface is generic: the driver dispatches each call the script makes to the
9
+ // matching harness tool via `runner.runTool`, so every host call inherits that tool's approval
10
+ // policy and tool_start/tool_end lifecycle. A file operation on one of the tool's mounts is carried
11
+ // out the same way, as fs_* tool calls against the mount's backend (see vfs.ts). Calls the script
12
+ // made but has not awaited yet start as soon as a step hands them over, and keep running across
13
+ // steps until the script awaits them.
14
+ //
15
+ // Workflow-safe: the sandbox only runs in activities; this side drives it by name.
16
+ import { ActivityFailure, ApplicationFailure, CancellationScope, ChildWorkflowFailure, TemporalFailure, isCancellation, log, proxyActivities, sleep, uuid4, } from "@temporalio/workflow";
17
+ import { pyRepr } from "./stubs.js";
18
+ import { performFileOp } from "./vfs.js";
19
+ export const CODE_START_BATCH_ACTIVITY = "code_start_batch";
20
+ export const CODE_RESUME_BATCH_ACTIVITY = "code_resume_batch";
21
+ export const CODE_TYPE_CHECK_ACTIVITY = "code_type_check";
22
+ export const DEFAULT_STEP_TIMEOUT = "30 seconds";
23
+ // A step that times out three times won't finish on a fourth.
24
+ export const STEP_RETRY = { maximumAttempts: 3 };
25
+ /** A pending call that is the script's `await asyncio.sleep(seconds)`, answered with a durable timer. */
26
+ export const SLEEP_CALL = "asyncio.sleep";
27
+ // Python's built-in exceptions a host failure keeps as its own type inside the script.
28
+ const PYTHON_EXCEPTIONS = new Set([
29
+ "ArithmeticError",
30
+ "AssertionError",
31
+ "AttributeError",
32
+ "BufferError",
33
+ "ConnectionError",
34
+ "EOFError",
35
+ "Exception",
36
+ "FileExistsError",
37
+ "FileNotFoundError",
38
+ "FloatingPointError",
39
+ "ImportError",
40
+ "IndexError",
41
+ "IsADirectoryError",
42
+ "KeyError",
43
+ "LookupError",
44
+ "MemoryError",
45
+ "ModuleNotFoundError",
46
+ "NameError",
47
+ "NotADirectoryError",
48
+ "NotImplementedError",
49
+ "OSError",
50
+ "OverflowError",
51
+ "PermissionError",
52
+ "RecursionError",
53
+ "RuntimeError",
54
+ "TimeoutError",
55
+ "TypeError",
56
+ "UnicodeError",
57
+ "ValueError",
58
+ "ZeroDivisionError",
59
+ ]);
60
+ // The errors a filesystem raises across a Temporal boundary as an ApplicationFailure of that type,
61
+ // which the script sees as the built-in exception of that name.
62
+ const OS_ERRORS = new Set([
63
+ "OSError",
64
+ "FileNotFoundError",
65
+ "FileExistsError",
66
+ "IsADirectoryError",
67
+ "NotADirectoryError",
68
+ "PermissionError",
69
+ ]);
70
+ /**
71
+ * What a failed host call is to a script, as in the Python harness: a failed activity is unwrapped
72
+ * to its cause, which carries the tool's own message. A Python built-in keeps its type: an error
73
+ * named for one, or an ApplicationFailure of that type raised in workflow code (Python raises the
74
+ * built-in itself there). Anything else, an activity's failure included, arrives as
75
+ * `Exception("TypeName: message")` under its Python SDK name (`ApplicationError`, ...).
76
+ */
77
+ function scriptError(e) {
78
+ const fromActivity = e instanceof ActivityFailure && e.cause !== undefined;
79
+ const error = fromActivity ? e.cause : e;
80
+ if (!(error instanceof Error))
81
+ return { error: String(error), error_type: "Exception" };
82
+ const builtin = error instanceof ApplicationFailure
83
+ ? fromActivity
84
+ ? undefined
85
+ : error.type
86
+ : error instanceof TemporalFailure
87
+ ? undefined
88
+ : error.name;
89
+ if (builtin && PYTHON_EXCEPTIONS.has(builtin))
90
+ return { error: error.message, error_type: builtin };
91
+ // Temporal's failures are Python's `*Error`s: ApplicationFailure is ApplicationError.
92
+ const name = error instanceof TemporalFailure ? error.name.replace(/Failure$/, "Error") : error.name;
93
+ return { error: `${name}: ${error.message}`, error_type: "Exception" };
94
+ }
95
+ /**
96
+ * A failed file operation as the script sees it: an OS error its filesystem raised across an
97
+ * activity or child workflow boundary is the built-in of that name, as in Python; anything else
98
+ * is any host call's failure.
99
+ */
100
+ function fileOpError(e) {
101
+ let cause = e;
102
+ while (cause instanceof ActivityFailure || cause instanceof ChildWorkflowFailure)
103
+ cause = cause.cause;
104
+ if (cause instanceof ApplicationFailure && OS_ERRORS.has(cause.type ?? ""))
105
+ return { error: cause.message, error_type: cause.type };
106
+ return scriptError(e);
107
+ }
108
+ /** A host call's positional and keyword arguments, bound to the tool's parameter names. */
109
+ function bindArgs(tool, { args, kwargs }) {
110
+ const names = Object.keys(tool.parameters.properties ?? {});
111
+ if (args.length > names.length) {
112
+ throw ApplicationFailure.create({
113
+ type: "HostCallBinding",
114
+ nonRetryable: true,
115
+ message: `host call '${tool.name}': too many positional arguments`,
116
+ });
117
+ }
118
+ const bound = {};
119
+ args.forEach((value, i) => (bound[names[i]] = value));
120
+ for (const [k, v] of Object.entries(kwargs)) {
121
+ if (k in bound)
122
+ throw ApplicationFailure.create({
123
+ type: "HostCallBinding",
124
+ nonRetryable: true,
125
+ message: `host call '${tool.name}': multiple values for '${k}'`,
126
+ });
127
+ bound[k] = v;
128
+ }
129
+ return bound;
130
+ }
131
+ /**
132
+ * Runs a Code Mode script to completion, dispatching its host calls to real harness tools.
133
+ *
134
+ * Constructed per invocation by the `codeModeTool` closure, with the live runner. `toolsByName` is
135
+ * validated once by the factory; `stubs` are the generated stubs the sandbox type-checks the
136
+ * script against before running it.
137
+ */
138
+ export class CodeModeDriver {
139
+ #runner;
140
+ #toolsByName;
141
+ #injections;
142
+ #stubs;
143
+ #mounts;
144
+ #acts;
145
+ // Every call starts as soon as its step stops, so one awaited in a later step is already
146
+ // running, and calls the script never awaits are cancelled when it ends.
147
+ #running = new Map();
148
+ constructor(runner, toolsByName, options) {
149
+ this.#runner = runner;
150
+ this.#toolsByName = toolsByName;
151
+ this.#injections = options.injections;
152
+ this.#stubs = options.stubs;
153
+ this.#mounts = options.mounts;
154
+ this.#acts = proxyActivities({
155
+ startToCloseTimeout: options.stepTimeout,
156
+ retry: STEP_RETRY,
157
+ });
158
+ }
159
+ /** Drive `script` to completion; return its output and final value (or the error) as text. */
160
+ async runScript(script) {
161
+ const files = this.#mounts.length > 0;
162
+ log.info(`code_mode: starting batch run (script_len=${script.length})`);
163
+ // The script reads the workflow's clock and a seed drawn from the workflow's randomness.
164
+ const seed = Math.floor(Math.random() * 2 ** 32);
165
+ const clock = () => ({ now: Date.now(), seed });
166
+ const stdout = [];
167
+ let step = await (files
168
+ ? this.#acts.code_start_batch(script, this.#stubs, clock(), true)
169
+ : this.#acts.code_start_batch(script, this.#stubs, clock()));
170
+ try {
171
+ for (;;) {
172
+ stdout.push(step.stdout);
173
+ if (step.done)
174
+ break;
175
+ for (const call of step.calls)
176
+ if (!this.#running.has(call.call_id))
177
+ this.#start(call);
178
+ let results;
179
+ if (step.file_op) {
180
+ // A file operation: the calls made before it have started, and the operation runs now.
181
+ results = [await this.#runFileOp(step.file_op)];
182
+ }
183
+ else {
184
+ results = await Promise.all(step.awaiting.map((id) => {
185
+ const entry = this.#running.get(id);
186
+ if (entry === undefined)
187
+ throw ApplicationFailure.create({
188
+ type: "UnsupportedMontyFuture",
189
+ nonRetryable: true,
190
+ message: `the script awaits call ${id}, which it never made`,
191
+ });
192
+ this.#running.delete(id);
193
+ return entry.result;
194
+ }));
195
+ }
196
+ step = await this.#acts.code_resume_batch({
197
+ snapshot: step.snapshot,
198
+ results,
199
+ type_names: this.#stubs.type_names,
200
+ clock: clock(),
201
+ ...(files ? { files } : {}),
202
+ });
203
+ }
204
+ }
205
+ finally {
206
+ // Calls the script made but never awaited before it ended.
207
+ for (const { scope } of this.#running.values())
208
+ scope.cancel();
209
+ }
210
+ if (step.error)
211
+ return `Script error (${step.error})`;
212
+ const parts = [];
213
+ const combined = stdout.join("").trim();
214
+ if (combined)
215
+ parts.push(`output:\n${combined}`);
216
+ parts.push(`result: ${pyRepr(step.output_json ? JSON.parse(step.output_json) : null)}`);
217
+ return parts.join("\n");
218
+ }
219
+ #start(call) {
220
+ const scope = new CancellationScope();
221
+ const result = scope.run(() => this.#runCall(call));
222
+ result.catch(() => undefined);
223
+ this.#running.set(call.call_id, { scope, result });
224
+ }
225
+ async #runCall(call) {
226
+ if (call.function_name === SLEEP_CALL) {
227
+ await sleep(Number(call.args[0]) * 1000);
228
+ return { call_id: call.call_id, return_value: null };
229
+ }
230
+ try {
231
+ const target = this.#toolsByName.get(call.function_name);
232
+ if (target === undefined) {
233
+ // Type-checking against the stubs should already reject an unknown name; if not, the
234
+ // script sees the failure at its await, as in Python.
235
+ throw ApplicationFailure.create({
236
+ type: "UnknownHostFunction",
237
+ nonRetryable: true,
238
+ message: `script called unknown host function ${pyRepr(call.function_name)}`,
239
+ });
240
+ }
241
+ const result = await this.#runner.runTool(uuid4(), target, bindArgs(target, call), { injections: this.#injections });
242
+ return {
243
+ call_id: call.call_id,
244
+ return_value: JSON.parse(JSON.stringify(result ?? null)),
245
+ };
246
+ }
247
+ catch (e) {
248
+ if (isCancellation(e))
249
+ throw e;
250
+ return { call_id: call.call_id, return_value: null, ...scriptError(e) };
251
+ }
252
+ }
253
+ async #runFileOp(op) {
254
+ try {
255
+ const value = await performFileOp(this.#mounts, op, (t, input, backend) => this.#runner.runTool(uuid4(), t, input, {
256
+ injections: { backend },
257
+ }));
258
+ return { call_id: op.call_id, return_value: value };
259
+ }
260
+ catch (e) {
261
+ if (isCancellation(e))
262
+ throw e;
263
+ return { call_id: op.call_id, return_value: null, ...fileOpError(e) };
264
+ }
265
+ }
266
+ }
@@ -0,0 +1,4 @@
1
+ export * from "./driver.ts";
2
+ export * from "./tool.ts";
3
+ export { CodeModeStubError, pyRepr, renderHostInterface, renderTypeCheckStubs, type TypeCheckStubs, } from "./stubs.ts";
4
+ export { FileTree, FsError, InMemoryFileSystem, Mount, osError, type FileStat, type FileSystem, type OsErrorType, type Seed, } from "./vfs.ts";
@@ -0,0 +1,6 @@
1
+ // Code Mode (Python's code_mode package): one tool that runs a model-written script over a set of
2
+ // harness tools, the driver that steps it, and the sandbox's virtual filesystem.
3
+ export * from "./driver.js";
4
+ export * from "./tool.js";
5
+ export { CodeModeStubError, pyRepr, renderHostInterface, renderTypeCheckStubs, } from "./stubs.js";
6
+ export { FileTree, FsError, InMemoryFileSystem, Mount, osError, } from "./vfs.js";
@@ -0,0 +1,43 @@
1
+ import { z } from "zod";
2
+ import type { StateDecl, StateRef } from "../state/index.ts";
3
+ import { type ActivityFilesystem } from "./activity-fs.ts";
4
+ import { FileIndex } from "./vfs.ts";
5
+ import { InMemoryFileSystem, Mount, type Seed } from "./vfs.ts";
6
+ declare const DeclaredFileTree: z.ZodObject<{
7
+ files: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodObject<{
8
+ content: z.ZodString;
9
+ encoding: z.ZodDefault<z.ZodEnum<{
10
+ base64: "base64";
11
+ "utf-8": "utf-8";
12
+ }>>;
13
+ size: z.ZodNumber;
14
+ }, z.core.$strip>>>;
15
+ directories: z.ZodDefault<z.ZodArray<z.ZodString>>;
16
+ kind: z.ZodDefault<z.ZodLiteral<"file_tree">>;
17
+ mount: z.ZodDefault<z.ZodString>;
18
+ description: z.ZodDefault<z.ZodString>;
19
+ read_only: z.ZodDefault<z.ZodBoolean>;
20
+ }, z.core.$strip>;
21
+ type DeclaredFileTree = z.output<typeof DeclaredFileTree>;
22
+ export interface VfsMountDecl<S extends z.ZodType<Record<string, unknown>>, C> extends StateDecl<S> {
23
+ bind(ref: StateRef<z.output<S>>, config: C, options?: {
24
+ timeout?: string | number;
25
+ }): Mount;
26
+ }
27
+ interface MemoryBind {
28
+ seed?: Seed;
29
+ maxBytes?: number;
30
+ }
31
+ export declare function vfsMount(path: string, backend: typeof InMemoryFileSystem, options: {
32
+ description: string;
33
+ readOnly?: boolean;
34
+ }): VfsMountDecl<typeof DeclaredFileTree, MemoryBind | undefined>;
35
+ export declare function vfsMount<C>(path: string, backend: ActivityFilesystem<C>, options: {
36
+ description: string;
37
+ readOnly?: boolean;
38
+ }): VfsMountDecl<typeof FileIndex, C>;
39
+ export declare function okfBundleVfsMount<C>(path: string, backend: ActivityFilesystem<C>, options: {
40
+ description: string;
41
+ readOnly?: boolean;
42
+ }): VfsMountDecl<typeof FileIndex, C>;
43
+ export {};
@@ -0,0 +1,100 @@
1
+ // Declaring a Code Mode mount as agent state. An in-memory mount keeps its files in a FileTree.
2
+ // An activity-backed mount keeps a FileIndex of the paths the session has touched.
3
+ import { z } from "zod";
4
+ import {} from "./activity-fs.js";
5
+ import { FileIndex, indexTrack } from "./vfs.js";
6
+ import { OKF_VERSION } from "./okf.js";
7
+ import { FileTree, InMemoryFileSystem, Mount } from "./vfs.js";
8
+ const DeclaredFileTree = FileTree.extend({
9
+ kind: z.literal("file_tree").default("file_tree"),
10
+ mount: z.string().default(""),
11
+ description: z.string().default(""),
12
+ read_only: z.boolean().default(false),
13
+ });
14
+ function normalized(path) {
15
+ const parts = [];
16
+ for (const part of path.split("/")) {
17
+ if (part === "" || part === ".")
18
+ continue;
19
+ if (part === "..") {
20
+ if (!parts.length)
21
+ return path;
22
+ parts.pop();
23
+ }
24
+ else
25
+ parts.push(part);
26
+ }
27
+ return `/${parts.join("/")}`;
28
+ }
29
+ function checkPath(path) {
30
+ if (!path.startsWith("/") || normalized(path) !== path)
31
+ throw new TypeError(`mount path '${path}' must be an absolute, normalized POSIX path, like '/workspace'`);
32
+ if (path === "/")
33
+ throw new TypeError("a mount cannot be the root '/'; mount at a named directory");
34
+ }
35
+ function readOnlyOnReadOnlyFs(what, path, fs, readOnly) {
36
+ if (!readOnly && !fs.writable)
37
+ throw new TypeError(`${what}('${path}'): ${fs.name} is read-only (it implements no write methods); declare the mount with readOnly: true`);
38
+ }
39
+ function isActivityFilesystem(value) {
40
+ return (typeof value === "object" &&
41
+ value !== null &&
42
+ value.kind === "activity-filesystem" &&
43
+ typeof value.backend === "function");
44
+ }
45
+ export function vfsMount(path, backend, options) {
46
+ checkPath(path);
47
+ const readOnly = options.readOnly ?? false;
48
+ if (backend === InMemoryFileSystem) {
49
+ const decl = {
50
+ schema: DeclaredFileTree,
51
+ initial: () => ({
52
+ kind: "file_tree",
53
+ mount: path,
54
+ description: options.description,
55
+ read_only: readOnly,
56
+ files: {},
57
+ directories: [],
58
+ }),
59
+ bind(ref, config = {}) {
60
+ return new Mount(path, new InMemoryFileSystem({ state: ref, ...config }), {
61
+ readOnly,
62
+ description: options.description,
63
+ });
64
+ },
65
+ };
66
+ return decl;
67
+ }
68
+ if (!isActivityFilesystem(backend))
69
+ throw new TypeError(`vfsMount('${path}'): the backend must be InMemoryFileSystem or an ActivityFileSystem`);
70
+ return activityDecl("vfsMount", path, backend, options.description, readOnly, null);
71
+ }
72
+ export function okfBundleVfsMount(path, backend, options) {
73
+ checkPath(path);
74
+ if (!isActivityFilesystem(backend))
75
+ throw new TypeError(`okfBundleVfsMount('${path}'): the backend must be an ActivityFileSystem`);
76
+ return activityDecl("okfBundleVfsMount", path, backend, options.description, options.readOnly ?? false, OKF_VERSION);
77
+ }
78
+ function activityDecl(what, path, backend, description, readOnly, okfVersion) {
79
+ readOnlyOnReadOnlyFs(what, path, backend, readOnly);
80
+ return {
81
+ schema: FileIndex,
82
+ initial: () => ({
83
+ kind: "file_index",
84
+ mount: path,
85
+ description,
86
+ read_only: readOnly,
87
+ okf_version: okfVersion,
88
+ source: null,
89
+ entries: {},
90
+ }),
91
+ bind(ref, config, options) {
92
+ const fs = backend.backend(config, { timeout: options?.timeout, okfVersion });
93
+ return new Mount(path, fs, {
94
+ readOnly,
95
+ description,
96
+ tracked: indexTrack(ref, fs.source),
97
+ });
98
+ },
99
+ };
100
+ }
@@ -0,0 +1,10 @@
1
+ import { codeModeTool, type CodeModeTool } from "./tool.ts";
2
+ import type { Mount } from "./vfs.ts";
3
+ export declare function okfCodeModeTool(bundle: Mount, options: {
4
+ name: string;
5
+ tools?: Parameters<typeof codeModeTool>[0];
6
+ mounts?: readonly Mount[];
7
+ injections?: Record<string, unknown>;
8
+ inherentlySafe?: boolean;
9
+ autoApprovalCriteria?: string;
10
+ }): CodeModeTool;
@@ -0,0 +1,89 @@
1
+ // A Code Mode tool for one OKF bundle: the mount, three host functions, and a contract section
2
+ // that explains the format. Reads and writes stay ordinary file operations, so they are indexed.
3
+ import { workflowInfo } from "@temporalio/workflow";
4
+ import { z } from "zod";
5
+ import { injected, tool } from "../agent.js";
6
+ import { ActivityBackend } from "./activity-fs.js";
7
+ import { codeModeTool } from "./tool.js";
8
+ import { OKFConcept, OKFLinks, renderConcept } from "./okf.js";
9
+ const BUNDLE = "okf_bundle";
10
+ const bundleInjection = { [BUNDLE]: injected() };
11
+ const okfConcepts = tool({
12
+ name: "okf_concepts",
13
+ description: 'Every concept in the OKF bundle, from its frontmatter, without reading bodies. `prefix` limits it to the concepts under a folder, by concept id (e.g. "trips"). Each concept\'s `path` is the file to read; `problem` says what is wrong with a concept whose frontmatter is missing or broken.',
14
+ inputSchema: z.object({ prefix: z.string().default("") }),
15
+ outputSchema: z.array(OKFConcept),
16
+ inherentlySafe: true,
17
+ injected: bundleInjection,
18
+ execute: async ({ prefix }, { okf_bundle }) => {
19
+ const stripped = prefix.replace(/^\/+|\/+$/g, "");
20
+ const graph = await okf_bundle.backend.okfGraph(stripped === "" ? "." : stripped, false);
21
+ return graph.concepts.map((concept) => ({
22
+ ...concept,
23
+ path: `${okf_bundle.mount}/${concept.path}`,
24
+ }));
25
+ },
26
+ });
27
+ const okfLinks = tool({
28
+ name: "okf_links",
29
+ description: "The concepts the concept `id` links to, and the concepts that link to it (by concept id). `dangling` marks a link to a concept that doesn't exist yet.",
30
+ inputSchema: z.object({ id: z.string() }),
31
+ outputSchema: OKFLinks,
32
+ inherentlySafe: true,
33
+ injected: bundleInjection,
34
+ execute: async ({ id }, { okf_bundle }) => {
35
+ const graph = await okf_bundle.backend.okfGraph(".", true);
36
+ const conceptId = id.replace(/^\/+|\/+$/g, "").replace(/\.md$/, "");
37
+ return {
38
+ id: conceptId,
39
+ links_to: graph.links.filter((link) => link.source === conceptId),
40
+ cited_by: graph.links.filter((link) => link.target === conceptId),
41
+ };
42
+ },
43
+ });
44
+ const okfRender = tool({
45
+ name: "okf_render",
46
+ description: "A concept's full text: `frontmatter` as YAML between `---` lines, then the markdown `body`. Fills in `generated` (who wrote it, and when) unless you give one, and the time of a `verified` entry given without `at`. Write the result to the concept's file with `Path(...).write_text(...)`.",
47
+ inputSchema: z.object({
48
+ frontmatter: z.record(z.string(), z.any()),
49
+ body: z.string(),
50
+ }),
51
+ outputSchema: z.string(),
52
+ inherentlySafe: true,
53
+ execute: async ({ frontmatter, body }) => renderConcept(frontmatter, body, {
54
+ by: workflowInfo().workflowType,
55
+ at: new Date(),
56
+ }),
57
+ });
58
+ function guide(mount, version) {
59
+ return `
60
+ Knowledge bundle: \`${mount}\` is an Open Knowledge Format (OKF v${version}) bundle, a folder of markdown files you read and write with \`open()\` and \`pathlib\` like any other.
61
+
62
+ - Every \`.md\` file is a concept, except \`index.md\` and \`log.md\`. A concept's id is its path in the bundle without \`.md\` (\`${mount}/trips/denver.md\` is \`trips/denver\`).
63
+ - A concept starts with YAML frontmatter between \`---\` lines, then a markdown body. \`type\` is required: a short descriptive kind, such as \`Person\` or \`Plan\`. Also give \`title\` and \`description\` (ONE sentence). Optional: \`tags\` (a list), \`status\` (\`draft\`, \`stable\` or \`deprecated\`), \`stale_after\` (a UTC timestamp after which the concept is out of date), \`verified\` (a list of \`{by}\`, with \`by\` as \`human:<id>\` when a person confirmed it; \`okf_render\` adds the time), and \`sources\` (a list of \`{id, resource, title}\`).
64
+ - Write concepts with \`okf_render(frontmatter, body)\`, which formats the frontmatter and records who wrote the concept and when: \`Path(...).write_text(await okf_render({...}, body))\`.
65
+ - Link related concepts with ordinary markdown links relative to the linking file, e.g. \`[Ana](../people/ana.md)\`. Link only to concepts that exist, never to the concept itself, and once per mention.
66
+ - Each folder has an \`index.md\` listing its concepts, one line each: \`* [Title](slug.md) - description\`, and subfolders as \`* [folder](folder/index.md) - summary\`. Keep it up to date whenever you add, retitle or remove a concept.
67
+ - \`${mount}/log.md\` records changes, newest first, under \`## YYYY-MM-DD\` headings: \`* **Creation**: [Title](path.md) ...\`. Add an entry for every change.
68
+ - To find concepts, call \`okf_concepts()\` rather than reading files one by one; then read the files you need. \`okf_links(id)\` gives a concept's links and backlinks.
69
+ - Treat concepts marked \`stale\` or \`deprecated\` as out of date, and say so when you use them.
70
+ `;
71
+ }
72
+ export function okfCodeModeTool(bundle, options) {
73
+ const backend = bundle.backend;
74
+ if (!(backend instanceof ActivityBackend) || !backend.okfVersion || bundle.tracked === null)
75
+ throw new TypeError(`okfCodeModeTool takes a bound okfBundleVfsMount(...), got '${bundle.path}'`);
76
+ if (options.injections && Object.hasOwn(options.injections, BUNDLE))
77
+ throw new TypeError(`injection name '${BUNDLE}' is reserved by okfCodeModeTool`);
78
+ return codeModeTool([...(options.tools ?? []), okfConcepts, okfLinks, okfRender], {
79
+ name: options.name,
80
+ inherentlySafe: options.inherentlySafe,
81
+ ...(options.autoApprovalCriteria ? { autoApprovalCriteria: options.autoApprovalCriteria } : {}),
82
+ injections: {
83
+ ...options.injections,
84
+ [BUNDLE]: { backend, mount: bundle.path },
85
+ },
86
+ mounts: [...(options.mounts ?? []), bundle],
87
+ guide: guide(bundle.path, backend.okfVersion),
88
+ });
89
+ }
@@ -0,0 +1,91 @@
1
+ import { z } from "zod";
2
+ export declare const OKF_VERSION = "0.2";
3
+ export declare const RESERVED_NAMES: Set<string>;
4
+ /** The most a walk reads, so a huge bundle can't stall the activity or the console. */
5
+ export declare const MAX_OKF_CONCEPTS = 1000;
6
+ export declare const MAX_OKF_FILE_BYTES = 256000;
7
+ export declare const MAX_OKF_TOTAL_BYTES = 8000000;
8
+ /** Without links only frontmatter is needed, so a walk reads each file's head first. */
9
+ export declare const OKF_HEAD_BYTES = 8192;
10
+ export declare const UNCLOSED_FRONTMATTER = "frontmatter is never closed with a `---` line";
11
+ export declare const OKFConcept: z.ZodObject<{
12
+ id: z.ZodString;
13
+ path: z.ZodString;
14
+ type: z.ZodString;
15
+ title: z.ZodString;
16
+ description: z.ZodDefault<z.ZodString>;
17
+ tags: z.ZodDefault<z.ZodArray<z.ZodString>>;
18
+ status: z.ZodDefault<z.ZodString>;
19
+ trust_tier: z.ZodDefault<z.ZodEnum<{
20
+ "human-reviewed": "human-reviewed";
21
+ "machine-confirmed": "machine-confirmed";
22
+ unverified: "unverified";
23
+ }>>;
24
+ stale: z.ZodDefault<z.ZodBoolean>;
25
+ size: z.ZodNumber;
26
+ problem: z.ZodDefault<z.ZodNullable<z.ZodString>>;
27
+ }, z.core.$strip>;
28
+ export type OKFConcept = z.output<typeof OKFConcept>;
29
+ export declare const OKFLink: z.ZodObject<{
30
+ source: z.ZodString;
31
+ target: z.ZodString;
32
+ dangling: z.ZodDefault<z.ZodBoolean>;
33
+ }, z.core.$strip>;
34
+ export type OKFLink = z.output<typeof OKFLink>;
35
+ export declare const OKFLinks: z.ZodObject<{
36
+ id: z.ZodString;
37
+ links_to: z.ZodArray<z.ZodObject<{
38
+ source: z.ZodString;
39
+ target: z.ZodString;
40
+ dangling: z.ZodDefault<z.ZodBoolean>;
41
+ }, z.core.$strip>>;
42
+ cited_by: z.ZodArray<z.ZodObject<{
43
+ source: z.ZodString;
44
+ target: z.ZodString;
45
+ dangling: z.ZodDefault<z.ZodBoolean>;
46
+ }, z.core.$strip>>;
47
+ }, z.core.$strip>;
48
+ export type OKFLinks = z.output<typeof OKFLinks>;
49
+ export declare const OKFGraph: z.ZodObject<{
50
+ concepts: z.ZodArray<z.ZodObject<{
51
+ id: z.ZodString;
52
+ path: z.ZodString;
53
+ type: z.ZodString;
54
+ title: z.ZodString;
55
+ description: z.ZodDefault<z.ZodString>;
56
+ tags: z.ZodDefault<z.ZodArray<z.ZodString>>;
57
+ status: z.ZodDefault<z.ZodString>;
58
+ trust_tier: z.ZodDefault<z.ZodEnum<{
59
+ "human-reviewed": "human-reviewed";
60
+ "machine-confirmed": "machine-confirmed";
61
+ unverified: "unverified";
62
+ }>>;
63
+ stale: z.ZodDefault<z.ZodBoolean>;
64
+ size: z.ZodNumber;
65
+ problem: z.ZodDefault<z.ZodNullable<z.ZodString>>;
66
+ }, z.core.$strip>>;
67
+ links: z.ZodDefault<z.ZodArray<z.ZodObject<{
68
+ source: z.ZodString;
69
+ target: z.ZodString;
70
+ dangling: z.ZodDefault<z.ZodBoolean>;
71
+ }, z.core.$strip>>>;
72
+ truncated: z.ZodDefault<z.ZodBoolean>;
73
+ walked_at: z.ZodNumber;
74
+ }, z.core.$strip>;
75
+ export type OKFGraph = z.output<typeof OKFGraph>;
76
+ /** `(frontmatter, body, problem)`. `frontmatter` is null when there is none. */
77
+ export declare function splitFrontmatter(text: string): [string | null, string, string | null];
78
+ /**
79
+ * Concept ids `body` links to, in order, without repeats. Relative targets resolve against
80
+ * `conceptPath`; a target starting with `/` is relative to the bundle root. Links inside
81
+ * fenced code blocks, and links to the concept itself, are left out.
82
+ */
83
+ export declare function linkTargets(body: string, conceptPath: string): string[];
84
+ /**
85
+ * A concept's full text: `frontmatter` as YAML between `---` lines, then `body`.
86
+ * Records `generated` unless it is already present, and stamps a `verified` entry that has no `at`.
87
+ */
88
+ export declare function renderConcept(frontmatter: Record<string, unknown>, body: string, generated: {
89
+ by: string;
90
+ at: Date;
91
+ }): string;