@temporalio/agent-harness 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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,137 @@
|
|
|
1
|
+
import { type ActivityOptions } from "@temporalio/workflow";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { MidTurn, ToolApprovalPolicy, type AgentConfig, type InferInput, type InferOutput, type JsonValue, type Schema, type AutoApprovalCriteria, type AutoModeEvaluator, type TurnStreamContext } from "./agent-protocol/index.ts";
|
|
4
|
+
import { AgentWorkflowRunner, type HandlerDecl, type HandlerImpl } from "./agent-workflow.ts";
|
|
5
|
+
import type { StateDecl, StateRef } from "./state/index.ts";
|
|
6
|
+
import type { AnyHarnessTool, HarnessTool, ObjectSchema } from "./agent-workflow/types.ts";
|
|
7
|
+
export type { AnyHarnessTool, HarnessTool };
|
|
8
|
+
export declare function handler<I extends z.ZodType, O extends z.ZodType>(decl: {
|
|
9
|
+
description: string;
|
|
10
|
+
input: I;
|
|
11
|
+
output: O;
|
|
12
|
+
midTurn?: MidTurn;
|
|
13
|
+
modelCallable?: boolean;
|
|
14
|
+
/**
|
|
15
|
+
* The input's parameter name (a Python handler's own parameter name): a parent's subagent send
|
|
16
|
+
* tool takes the input under it. Defaults to "message".
|
|
17
|
+
*/
|
|
18
|
+
inputName?: string;
|
|
19
|
+
}): HandlerDecl<I, O>;
|
|
20
|
+
/** Phantom marker for a workflow-supplied tool parameter, hidden from the model. */
|
|
21
|
+
export interface Injected<T> {
|
|
22
|
+
readonly __injected?: T;
|
|
23
|
+
}
|
|
24
|
+
export declare const injected: <T>() => Injected<T>;
|
|
25
|
+
type InjectedValues<J> = {
|
|
26
|
+
[K in keyof J]: J[K] extends Injected<infer T> ? T : never;
|
|
27
|
+
};
|
|
28
|
+
interface ToolOptions<I extends ObjectSchema, O extends Schema | undefined, J> {
|
|
29
|
+
name: string;
|
|
30
|
+
description: string;
|
|
31
|
+
inputSchema: I;
|
|
32
|
+
outputSchema?: O;
|
|
33
|
+
inherentlySafe?: boolean;
|
|
34
|
+
autoApprovalCriteria?: string;
|
|
35
|
+
injected?: J;
|
|
36
|
+
}
|
|
37
|
+
type Output<O> = O extends Schema ? InferOutput<O> : unknown;
|
|
38
|
+
/** Which call an inline tool's `execute` is running. */
|
|
39
|
+
export interface InlineToolCall {
|
|
40
|
+
tool_id: string;
|
|
41
|
+
}
|
|
42
|
+
/** The context an activity tool's body runs with: which call it is, and where to publish. */
|
|
43
|
+
export interface AgentToolContext {
|
|
44
|
+
stream_context: TurnStreamContext;
|
|
45
|
+
tool_id: string;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* `tool_end`'s `tool_output`: the result as JSON. A tool that returns nothing gives `null`, an
|
|
49
|
+
* object's `toJSON` is honored (a `Date` is its ISO string), and an object property that is
|
|
50
|
+
* `undefined` is absent, as the payload converter encodes them.
|
|
51
|
+
*
|
|
52
|
+
* Where `JSON.stringify` would lose data silently, the call fails instead: a `Map`, `Set` or other
|
|
53
|
+
* built-in object with no JSON form, a function, symbol or bigint, a number that is not finite, an
|
|
54
|
+
* `undefined` array item, or a cycle. The protocol carries tool results as JSON, and a value that
|
|
55
|
+
* reaches readers changed would mean something other than what the tool returned.
|
|
56
|
+
*/
|
|
57
|
+
export declare function toolOutputJson(toolName: string, result: unknown): JsonValue;
|
|
58
|
+
/**
|
|
59
|
+
* An inline tool when given `execute` (it runs in the workflow: deterministic code only), or a
|
|
60
|
+
* callback tool when not: the call parks in the workflow until an attached client fulfills it
|
|
61
|
+
* via the `provide_callback_result` update, validated against the required `outputSchema`.
|
|
62
|
+
* `execute` gets the call's `tool_id` third, to key the side effects of activities it starts on.
|
|
63
|
+
*/
|
|
64
|
+
export declare function tool<I extends ObjectSchema, O extends Schema | undefined = undefined, J = {}>(options: ToolOptions<I, O, J> & {
|
|
65
|
+
execute: (input: InferOutput<I>, injected: InjectedValues<J>, call: InlineToolCall) => Promise<Output<O>>;
|
|
66
|
+
}): HarnessTool<InferInput<I>, Output<O>>;
|
|
67
|
+
export declare function tool<I extends ObjectSchema, O extends Schema, J = {}>(options: ToolOptions<I, O, J> & {
|
|
68
|
+
outputSchema: O;
|
|
69
|
+
execute?: undefined;
|
|
70
|
+
timeout?: string | number;
|
|
71
|
+
}): HarnessTool<InferInput<I>, InferOutput<O>>;
|
|
72
|
+
/**
|
|
73
|
+
* A durable, activity-backed tool. This is the workflow-side declaration; register its body on a
|
|
74
|
+
* worker with `toolActivity(tool, impl)` from `@temporalio/agent-harness-worker`, which also
|
|
75
|
+
* publishes the call's tool_start and its tool_end or tool_error from the activity, once per
|
|
76
|
+
* attempt, and heartbeats while it runs.
|
|
77
|
+
*
|
|
78
|
+
* Runs with a 30 s start-to-close timeout and the default retry policy unless `activityOptions`
|
|
79
|
+
* replaces them (its `summary` defaults to "tool_call"). The body runs at least once and may run
|
|
80
|
+
* again after a crash or timeout, so make side effects idempotent (key them on `ctx.tool_id`).
|
|
81
|
+
*/
|
|
82
|
+
export declare function activityTool<I extends ObjectSchema, O extends Schema | undefined = undefined, J = {}>(options: ToolOptions<I, O, J> & {
|
|
83
|
+
activityOptions?: ActivityOptions;
|
|
84
|
+
}): HarnessTool<InferInput<I>, Output<O>> & {
|
|
85
|
+
readonly __activity?: {
|
|
86
|
+
input: InferOutput<I>;
|
|
87
|
+
output: Output<O>;
|
|
88
|
+
injected: InjectedValues<J>;
|
|
89
|
+
};
|
|
90
|
+
};
|
|
91
|
+
type States = Record<string, StateDecl<z.ZodType<Record<string, unknown>>>>;
|
|
92
|
+
type StateRefs<S extends States> = {
|
|
93
|
+
[K in keyof S]: StateRef<z.output<S[K]["schema"]>>;
|
|
94
|
+
};
|
|
95
|
+
type InitData<D> = D extends {
|
|
96
|
+
schema: infer Z extends z.ZodType;
|
|
97
|
+
required: infer R;
|
|
98
|
+
} ? R extends true ? z.output<Z> : z.output<Z> | null : undefined;
|
|
99
|
+
/** An agent's init data: what its workflow takes after its config, and whether it must be sent. */
|
|
100
|
+
export type InitDataDecl = {
|
|
101
|
+
schema: z.ZodType;
|
|
102
|
+
required: boolean;
|
|
103
|
+
};
|
|
104
|
+
export interface AgentContext<S extends States, D> {
|
|
105
|
+
runner: AgentWorkflowRunner;
|
|
106
|
+
config: AgentConfig;
|
|
107
|
+
data: InitData<D>;
|
|
108
|
+
state: StateRefs<S>;
|
|
109
|
+
}
|
|
110
|
+
export type AgentImpl<H extends Record<string, HandlerDecl>> = {
|
|
111
|
+
[K in keyof H]: HandlerImpl<H[K]>;
|
|
112
|
+
} & {
|
|
113
|
+
/** Awaited before the first message, and again at the start of each run after a continue-as-new. */
|
|
114
|
+
setup?: () => Promise<void>;
|
|
115
|
+
/** Awaited once, after close drains (or on cancellation). */
|
|
116
|
+
teardown?: () => Promise<void>;
|
|
117
|
+
};
|
|
118
|
+
export interface AgentDefinition<H extends Record<string, HandlerDecl> = Record<string, HandlerDecl>, S extends States = States, D extends InitDataDecl | undefined = InitDataDecl | undefined> {
|
|
119
|
+
/** The workflow type. Export the result of `defineAgent` under exactly this name. */
|
|
120
|
+
name: string;
|
|
121
|
+
handlers: H;
|
|
122
|
+
states?: S;
|
|
123
|
+
initData?: D;
|
|
124
|
+
approvalPolicyDefault: ToolApprovalPolicy;
|
|
125
|
+
autoApprovalCriteriaDefault?: AutoApprovalCriteria;
|
|
126
|
+
autoModeEvaluator?: AutoModeEvaluator;
|
|
127
|
+
create(ctx: AgentContext<S, D>): AgentImpl<H>;
|
|
128
|
+
}
|
|
129
|
+
export type AgentWorkflow<Def extends AgentDefinition = AgentDefinition> = ((config?: unknown, data?: unknown) => Promise<void>) & {
|
|
130
|
+
readonly definition: Def;
|
|
131
|
+
};
|
|
132
|
+
export declare function defineAgent<H extends Record<string, HandlerDecl>, S extends States = {}, D extends InitDataDecl | undefined = undefined>(definition: AgentDefinition<H, S, D>): AgentWorkflow<AgentDefinition<H, S, D>>;
|
|
133
|
+
/** `{ required, schema }` for an agent's init data, as `GET /api/agents` reports it; null if none. */
|
|
134
|
+
export declare function agentInitDataSchema(agent: AgentWorkflow): {
|
|
135
|
+
required: boolean;
|
|
136
|
+
schema: Record<string, unknown>;
|
|
137
|
+
} | null;
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
// The authoring API. `defineAgent` turns a declaration into a Temporal workflow function (export
|
|
2
|
+
// it under the agent's workflow type name); `handler` declares an accepted message; `tool` (inline,
|
|
3
|
+
// or callback when it has no `execute`) and `activityTool` declare the three tool kinds. Every tool
|
|
4
|
+
// publishes its own tool_start/tool_end lifecycle and passes the approval gate before it runs.
|
|
5
|
+
//
|
|
6
|
+
// Declarations are static (plain data plus zod schemas), so the web server and a parent agent can
|
|
7
|
+
// read an agent's handlers and init data without running it, exactly as Python reads its class.
|
|
8
|
+
import { ApplicationFailure, CancellationScope, isCancellation, proxyActivities, workflowInfo, } from "@temporalio/workflow";
|
|
9
|
+
import { z } from "zod";
|
|
10
|
+
import { AgentConfigSchema, MidTurn, ToolApprovalPolicy, toJsonSchema, validate, } from "./agent-protocol/index.js";
|
|
11
|
+
import { AgentWorkflowRunner, currentToolCall, } from "./agent-workflow.js";
|
|
12
|
+
import { errorMessage, isOwnCancellation } from "./agent-workflow/errors.js";
|
|
13
|
+
// ---------------------------------------------------------------------------------------------
|
|
14
|
+
// Handlers
|
|
15
|
+
// ---------------------------------------------------------------------------------------------
|
|
16
|
+
export function handler(decl) {
|
|
17
|
+
if (!decl.description.trim())
|
|
18
|
+
throw new TypeError("handler(): a description is required (it is the tool description)");
|
|
19
|
+
return { midTurn: MidTurn.REJECT, modelCallable: true, inputName: "message", ...decl };
|
|
20
|
+
}
|
|
21
|
+
export const injected = () => ({});
|
|
22
|
+
/**
|
|
23
|
+
* `tool_end`'s `tool_output`: the result as JSON. A tool that returns nothing gives `null`, an
|
|
24
|
+
* object's `toJSON` is honored (a `Date` is its ISO string), and an object property that is
|
|
25
|
+
* `undefined` is absent, as the payload converter encodes them.
|
|
26
|
+
*
|
|
27
|
+
* Where `JSON.stringify` would lose data silently, the call fails instead: a `Map`, `Set` or other
|
|
28
|
+
* built-in object with no JSON form, a function, symbol or bigint, a number that is not finite, an
|
|
29
|
+
* `undefined` array item, or a cycle. The protocol carries tool results as JSON, and a value that
|
|
30
|
+
* reaches readers changed would mean something other than what the tool returned.
|
|
31
|
+
*/
|
|
32
|
+
export function toolOutputJson(toolName, result) {
|
|
33
|
+
if (result === undefined)
|
|
34
|
+
return null;
|
|
35
|
+
const notJson = (path, what) => ApplicationFailure.create({
|
|
36
|
+
type: "ToolOutputNotJson",
|
|
37
|
+
nonRetryable: true,
|
|
38
|
+
message: `tool '${toolName}' returned a value with no JSON form at ${path}: ${what}`,
|
|
39
|
+
});
|
|
40
|
+
const ancestors = new Set();
|
|
41
|
+
const encode = (value, path, key) => {
|
|
42
|
+
if (value === null || typeof value === "string" || typeof value === "boolean")
|
|
43
|
+
return value;
|
|
44
|
+
if (typeof value === "number") {
|
|
45
|
+
if (!Number.isFinite(value))
|
|
46
|
+
throw notJson(path, String(value));
|
|
47
|
+
return value;
|
|
48
|
+
}
|
|
49
|
+
if (typeof value !== "object")
|
|
50
|
+
throw notJson(path, `a ${typeof value}`);
|
|
51
|
+
const toJSON = value.toJSON;
|
|
52
|
+
if (typeof toJSON === "function")
|
|
53
|
+
return encode(toJSON.call(value, key), path, key);
|
|
54
|
+
if (ancestors.has(value))
|
|
55
|
+
throw notJson(path, "a cycle");
|
|
56
|
+
ancestors.add(value);
|
|
57
|
+
try {
|
|
58
|
+
if (Array.isArray(value))
|
|
59
|
+
return value.map((item, i) => {
|
|
60
|
+
if (item === undefined)
|
|
61
|
+
throw notJson(`${path}[${i}]`, "undefined");
|
|
62
|
+
return encode(item, `${path}[${i}]`, String(i));
|
|
63
|
+
});
|
|
64
|
+
// Plain objects and class instances tag as `Object`; Map, Set, RegExp, Error, typed
|
|
65
|
+
// arrays and the like have no own fields for JSON to carry.
|
|
66
|
+
const tag = Object.prototype.toString.call(value).slice(8, -1);
|
|
67
|
+
if (tag !== "Object")
|
|
68
|
+
throw notJson(path, `a ${tag}`);
|
|
69
|
+
const out = {};
|
|
70
|
+
for (const [k, v] of Object.entries(value)) {
|
|
71
|
+
if (v !== undefined)
|
|
72
|
+
out[k] = encode(v, `${path}.${k}`, k);
|
|
73
|
+
}
|
|
74
|
+
return out;
|
|
75
|
+
}
|
|
76
|
+
finally {
|
|
77
|
+
ancestors.delete(value);
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
return encode(result, "$", "");
|
|
81
|
+
}
|
|
82
|
+
function injectedValues(name, names, available) {
|
|
83
|
+
const values = {};
|
|
84
|
+
for (const n of names) {
|
|
85
|
+
if (!(n in available)) {
|
|
86
|
+
throw ApplicationFailure.create({
|
|
87
|
+
type: "MissingInjection",
|
|
88
|
+
nonRetryable: true,
|
|
89
|
+
message: `tool '${name}' requires injected argument '${n}', but runTool was called without it`,
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
values[n] = available[n];
|
|
93
|
+
}
|
|
94
|
+
return values;
|
|
95
|
+
}
|
|
96
|
+
function baseTool(kind, options, body, publishLifecycle) {
|
|
97
|
+
const injectedNames = Object.keys(options.injected ?? {});
|
|
98
|
+
const description = options.description;
|
|
99
|
+
return {
|
|
100
|
+
kind,
|
|
101
|
+
name: options.name,
|
|
102
|
+
description,
|
|
103
|
+
inputSchema: options.inputSchema,
|
|
104
|
+
outputSchema: options.outputSchema,
|
|
105
|
+
inherentlySafe: options.inherentlySafe ?? false,
|
|
106
|
+
autoApprovalCriteria: options.autoApprovalCriteria ?? null,
|
|
107
|
+
injected: injectedNames,
|
|
108
|
+
parameters: toJsonSchema(options.inputSchema),
|
|
109
|
+
async invoke(raw) {
|
|
110
|
+
const { runner, toolId, injections } = currentToolCall(`tool '${options.name}'`);
|
|
111
|
+
const ctx = runner.currentStreamContext;
|
|
112
|
+
if (ctx === null)
|
|
113
|
+
throw new Error(`tool '${options.name}' must be invoked within an active turn`);
|
|
114
|
+
const values = injectedValues(options.name, injectedNames, injections);
|
|
115
|
+
const parsed = validate(options.inputSchema, raw);
|
|
116
|
+
if (!parsed.ok) {
|
|
117
|
+
throw ApplicationFailure.create({
|
|
118
|
+
type: "InvalidToolInput",
|
|
119
|
+
nonRetryable: true,
|
|
120
|
+
message: `Payload for tool '${options.name}' does not match its schema: ${parsed.message}`,
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
const input = parsed.value;
|
|
124
|
+
const toolInput = input;
|
|
125
|
+
await runner.applyApprovalPolicy(toolId, options.name, toolInput, {
|
|
126
|
+
inherentlySafe: options.inherentlySafe ?? false,
|
|
127
|
+
description,
|
|
128
|
+
autoApprovalCriteria: options.autoApprovalCriteria ?? null,
|
|
129
|
+
});
|
|
130
|
+
if (!publishLifecycle)
|
|
131
|
+
return (await body(input, values, toolId, runner));
|
|
132
|
+
runner.pub(ctx.turn_id, ctx.turn_number, {
|
|
133
|
+
type: "tool_start",
|
|
134
|
+
tool_id: toolId,
|
|
135
|
+
tool_name: options.name,
|
|
136
|
+
tool_input: toolInput,
|
|
137
|
+
});
|
|
138
|
+
let result;
|
|
139
|
+
let toolOutput;
|
|
140
|
+
try {
|
|
141
|
+
result = await body(input, values, toolId, runner);
|
|
142
|
+
toolOutput = toolOutputJson(options.name, result);
|
|
143
|
+
}
|
|
144
|
+
catch (e) {
|
|
145
|
+
if (!isOwnCancellation(e))
|
|
146
|
+
runner.pub(ctx.turn_id, ctx.turn_number, {
|
|
147
|
+
type: "tool_error",
|
|
148
|
+
tool_id: toolId,
|
|
149
|
+
tool_name: options.name,
|
|
150
|
+
message: errorMessage(e),
|
|
151
|
+
});
|
|
152
|
+
throw e;
|
|
153
|
+
}
|
|
154
|
+
runner.pub(ctx.turn_id, ctx.turn_number, {
|
|
155
|
+
type: "tool_end",
|
|
156
|
+
tool_id: toolId,
|
|
157
|
+
tool_name: options.name,
|
|
158
|
+
tool_output: toolOutput,
|
|
159
|
+
});
|
|
160
|
+
return result;
|
|
161
|
+
},
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
export function tool(options) {
|
|
165
|
+
const { execute } = options;
|
|
166
|
+
if (execute !== undefined)
|
|
167
|
+
return baseTool("inline", options, (input, values, toolId) => execute(input, values, { tool_id: toolId }), true);
|
|
168
|
+
const output = options.outputSchema;
|
|
169
|
+
if (output === undefined)
|
|
170
|
+
throw new TypeError(`tool '${options.name}' has no execute, so it is a callback tool and needs an outputSchema`);
|
|
171
|
+
const outputSchema = toJsonSchema(output);
|
|
172
|
+
return baseTool("callback", options, (input, _values, toolId, runner) => runner.awaitCallbackResult({
|
|
173
|
+
toolId,
|
|
174
|
+
toolName: options.name,
|
|
175
|
+
toolInput: input,
|
|
176
|
+
output,
|
|
177
|
+
outputSchema,
|
|
178
|
+
timeout: options.timeout,
|
|
179
|
+
}), true);
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* A durable, activity-backed tool. This is the workflow-side declaration; register its body on a
|
|
183
|
+
* worker with `toolActivity(tool, impl)` from `@temporalio/agent-harness-worker`, which also
|
|
184
|
+
* publishes the call's tool_start and its tool_end or tool_error from the activity, once per
|
|
185
|
+
* attempt, and heartbeats while it runs.
|
|
186
|
+
*
|
|
187
|
+
* Runs with a 30 s start-to-close timeout and the default retry policy unless `activityOptions`
|
|
188
|
+
* replaces them (its `summary` defaults to "tool_call"). The body runs at least once and may run
|
|
189
|
+
* again after a crash or timeout, so make side effects idempotent (key them on `ctx.tool_id`).
|
|
190
|
+
*/
|
|
191
|
+
export function activityTool(options) {
|
|
192
|
+
const activityOptions = {
|
|
193
|
+
summary: "tool_call",
|
|
194
|
+
...(options.activityOptions ?? { startToCloseTimeout: "30 seconds" }),
|
|
195
|
+
};
|
|
196
|
+
return baseTool("activity", options, async (input, values, toolId, runner) => {
|
|
197
|
+
const ctx = {
|
|
198
|
+
stream_context: runner.currentStreamContext,
|
|
199
|
+
tool_id: toolId,
|
|
200
|
+
};
|
|
201
|
+
const activities = proxyActivities(activityOptions);
|
|
202
|
+
return activities[options.name](input, values, ctx);
|
|
203
|
+
}, false);
|
|
204
|
+
}
|
|
205
|
+
function invalidInput(message, type = "InvalidAgentInput") {
|
|
206
|
+
return ApplicationFailure.create({ type, nonRetryable: true, message });
|
|
207
|
+
}
|
|
208
|
+
export function defineAgent(definition) {
|
|
209
|
+
const workflow = async (rawConfig, rawData) => {
|
|
210
|
+
const workflowType = workflowInfo().workflowType;
|
|
211
|
+
if (workflowType !== definition.name) {
|
|
212
|
+
throw invalidInput(`agent '${definition.name}' is registered as workflow type '${workflowType}'; export it under its own name`);
|
|
213
|
+
}
|
|
214
|
+
const config = AgentConfigSchema.safeParse(rawConfig ?? {});
|
|
215
|
+
if (!config.success)
|
|
216
|
+
throw invalidInput(`${definition.name} was started with an invalid AgentConfig: ${config.error.message}`);
|
|
217
|
+
let data;
|
|
218
|
+
if (definition.initData === undefined) {
|
|
219
|
+
data = undefined;
|
|
220
|
+
}
|
|
221
|
+
else if (rawData === undefined || rawData === null) {
|
|
222
|
+
if (definition.initData.required) {
|
|
223
|
+
throw invalidInput(`${definition.name} requires init data, and none was given.`, "InitDataRequired");
|
|
224
|
+
}
|
|
225
|
+
data = null;
|
|
226
|
+
}
|
|
227
|
+
else {
|
|
228
|
+
const parsed = definition.initData.schema.safeParse(rawData);
|
|
229
|
+
if (!parsed.success)
|
|
230
|
+
throw invalidInput(`${definition.name} was started with invalid init data: ${parsed.error.message}`);
|
|
231
|
+
data = parsed.data;
|
|
232
|
+
}
|
|
233
|
+
const runner = new AgentWorkflowRunner(config.data, {
|
|
234
|
+
handlers: definition.handlers,
|
|
235
|
+
approvalPolicyDefault: definition.approvalPolicyDefault,
|
|
236
|
+
autoApprovalCriteriaDefault: definition.autoApprovalCriteriaDefault,
|
|
237
|
+
autoModeEvaluator: definition.autoModeEvaluator,
|
|
238
|
+
states: definition.states,
|
|
239
|
+
data,
|
|
240
|
+
});
|
|
241
|
+
const state = runner.state;
|
|
242
|
+
const impl = definition.create({
|
|
243
|
+
runner,
|
|
244
|
+
config: config.data,
|
|
245
|
+
data: data,
|
|
246
|
+
state,
|
|
247
|
+
});
|
|
248
|
+
for (const name of Object.keys(definition.handlers)) {
|
|
249
|
+
if (typeof impl[name] !== "function")
|
|
250
|
+
throw new TypeError(`${definition.name}.create() returned no implementation for handler '${name}'`);
|
|
251
|
+
}
|
|
252
|
+
await impl.setup?.();
|
|
253
|
+
try {
|
|
254
|
+
await runner.run(impl);
|
|
255
|
+
}
|
|
256
|
+
catch (e) {
|
|
257
|
+
if (isCancellation(e) && impl.teardown)
|
|
258
|
+
await CancellationScope.nonCancellable(impl.teardown);
|
|
259
|
+
throw e;
|
|
260
|
+
}
|
|
261
|
+
await impl.teardown?.();
|
|
262
|
+
};
|
|
263
|
+
Object.defineProperty(workflow, "name", { value: definition.name });
|
|
264
|
+
return Object.assign(workflow, { definition });
|
|
265
|
+
}
|
|
266
|
+
/** `{ required, schema }` for an agent's init data, as `GET /api/agents` reports it; null if none. */
|
|
267
|
+
export function agentInitDataSchema(agent) {
|
|
268
|
+
const init = agent.definition.initData;
|
|
269
|
+
if (init === undefined)
|
|
270
|
+
return null;
|
|
271
|
+
return { required: init.required, schema: toJsonSchema(init.schema) };
|
|
272
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { z } from "zod";
|
|
2
|
+
import type { FileStat, FileSystem } from "./vfs.ts";
|
|
3
|
+
import type { OKFGraph } from "./okf.ts";
|
|
4
|
+
/** The most one file-view call returns. The console pages through anything larger. */
|
|
5
|
+
export declare const MAX_VIEW_BYTES = 256000;
|
|
6
|
+
export declare function activityName(filesystem: string, operation: string): string;
|
|
7
|
+
export interface ActivityFilesystem<C> {
|
|
8
|
+
readonly kind: "activity-filesystem";
|
|
9
|
+
readonly name: string;
|
|
10
|
+
readonly writable: boolean;
|
|
11
|
+
readonly configSchema: z.ZodType<C>;
|
|
12
|
+
backend(config: C, options?: {
|
|
13
|
+
timeout?: string | number;
|
|
14
|
+
okfVersion?: string | null;
|
|
15
|
+
}): ActivityBackend;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* A filesystem whose operations run as `vfs.<name>.*` activities. `configSchema` is validated
|
|
19
|
+
* on every activity, not here: a bad config fails the activity that uses it.
|
|
20
|
+
*/
|
|
21
|
+
export declare function activityFilesystem<C>(options: {
|
|
22
|
+
name: string;
|
|
23
|
+
configSchema: z.ZodType<C>;
|
|
24
|
+
writable: boolean;
|
|
25
|
+
}): ActivityFilesystem<C>;
|
|
26
|
+
/**
|
|
27
|
+
* A read-only mount backend. Each operation is one activity on the workflow's task queue.
|
|
28
|
+
* `okfGraph` walks the bundle in a single activity.
|
|
29
|
+
*/
|
|
30
|
+
export declare class ActivityBackend implements FileSystem {
|
|
31
|
+
#private;
|
|
32
|
+
readonly source: {
|
|
33
|
+
filesystem: string;
|
|
34
|
+
config: Record<string, unknown>;
|
|
35
|
+
};
|
|
36
|
+
readonly okfVersion: string | null;
|
|
37
|
+
constructor(source: {
|
|
38
|
+
filesystem: string;
|
|
39
|
+
config: Record<string, unknown>;
|
|
40
|
+
}, timeout: string | number, okfVersion: string | null);
|
|
41
|
+
stat(path: string): Promise<FileStat | null>;
|
|
42
|
+
read(path: string): Promise<Uint8Array>;
|
|
43
|
+
list(path: string): Promise<string[]>;
|
|
44
|
+
okfGraph(prefix: string, links: boolean): Promise<OKFGraph>;
|
|
45
|
+
protected do(operation: string, extra: Record<string, unknown>): Promise<void>;
|
|
46
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
// The workflow half of an activity-backed filesystem. The worker builds the activities
|
|
2
|
+
// (`filesystemActivities`); here a mount only calls them by name, so this module stays sandbox-safe.
|
|
3
|
+
import { proxyActivities } from "@temporalio/workflow";
|
|
4
|
+
import { decodeBase64, encodeBase64 } from "./base64.js";
|
|
5
|
+
const NAME = /^[a-z0-9][a-z0-9_-]*$/;
|
|
6
|
+
/** The most one file-view call returns. The console pages through anything larger. */
|
|
7
|
+
export const MAX_VIEW_BYTES = 256_000;
|
|
8
|
+
const RETRY = { maximumAttempts: 3 };
|
|
9
|
+
export function activityName(filesystem, operation) {
|
|
10
|
+
return `vfs.${filesystem}.${operation}`;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* A filesystem whose operations run as `vfs.<name>.*` activities. `configSchema` is validated
|
|
14
|
+
* on every activity, not here: a bad config fails the activity that uses it.
|
|
15
|
+
*/
|
|
16
|
+
export function activityFilesystem(options) {
|
|
17
|
+
if (!NAME.test(options.name))
|
|
18
|
+
throw new TypeError(`filesystem name '${options.name}' must be lowercase letters, digits, '-' and '_', like 'local-disk'`);
|
|
19
|
+
return {
|
|
20
|
+
kind: "activity-filesystem",
|
|
21
|
+
name: options.name,
|
|
22
|
+
writable: options.writable,
|
|
23
|
+
configSchema: options.configSchema,
|
|
24
|
+
backend(config, { timeout = "30 seconds", okfVersion = null } = {}) {
|
|
25
|
+
if (config === null || typeof config !== "object" || Array.isArray(config))
|
|
26
|
+
throw new TypeError(`${options.name}.backend() takes a config object`);
|
|
27
|
+
const stored = JSON.parse(JSON.stringify(config));
|
|
28
|
+
const source = { filesystem: options.name, config: stored };
|
|
29
|
+
return options.writable
|
|
30
|
+
? new WritableActivityBackend(source, timeout, okfVersion)
|
|
31
|
+
: new ActivityBackend(source, timeout, okfVersion);
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
const utf8 = new TextEncoder();
|
|
36
|
+
function unwire(content, encoding) {
|
|
37
|
+
return encoding === "base64" ? decodeBase64(content) : utf8.encode(content);
|
|
38
|
+
}
|
|
39
|
+
function wire(data) {
|
|
40
|
+
try {
|
|
41
|
+
return { content: new TextDecoder("utf-8", { fatal: true }).decode(data), encoding: "utf-8" };
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
return { content: encodeBase64(data), encoding: "base64" };
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* A read-only mount backend. Each operation is one activity on the workflow's task queue.
|
|
49
|
+
* `okfGraph` walks the bundle in a single activity.
|
|
50
|
+
*/
|
|
51
|
+
export class ActivityBackend {
|
|
52
|
+
source;
|
|
53
|
+
okfVersion;
|
|
54
|
+
#timeout;
|
|
55
|
+
constructor(source, timeout, okfVersion) {
|
|
56
|
+
this.source = source;
|
|
57
|
+
this.#timeout = timeout;
|
|
58
|
+
this.okfVersion = okfVersion;
|
|
59
|
+
}
|
|
60
|
+
async stat(path) {
|
|
61
|
+
const result = await this.#call("stat", { path });
|
|
62
|
+
return result.found;
|
|
63
|
+
}
|
|
64
|
+
async read(path) {
|
|
65
|
+
const found = await this.#call("read", { path });
|
|
66
|
+
return unwire(found.content, found.encoding);
|
|
67
|
+
}
|
|
68
|
+
async list(path) {
|
|
69
|
+
return this.#call("list", { path });
|
|
70
|
+
}
|
|
71
|
+
okfGraph(prefix, links) {
|
|
72
|
+
return this.#call("okf_graph", { path: prefix, links });
|
|
73
|
+
}
|
|
74
|
+
async #call(operation, extra) {
|
|
75
|
+
const arg = { config: this.source.config, ...extra };
|
|
76
|
+
const activities = proxyActivities({
|
|
77
|
+
startToCloseTimeout: this.#timeout,
|
|
78
|
+
retry: RETRY,
|
|
79
|
+
});
|
|
80
|
+
// A failure stays an ActivityFailure, as in Python: the driver gives the script an OS error
|
|
81
|
+
// as its built-in, and any other as the activity's failure.
|
|
82
|
+
return activities[activityName(this.source.filesystem, operation)](arg);
|
|
83
|
+
}
|
|
84
|
+
do(operation, extra) {
|
|
85
|
+
return this.#call(operation, extra);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
class WritableActivityBackend extends ActivityBackend {
|
|
89
|
+
async write(path, data) {
|
|
90
|
+
const { content, encoding } = wire(data);
|
|
91
|
+
await this.do("write", { path, content, encoding });
|
|
92
|
+
}
|
|
93
|
+
async mkdir(path) {
|
|
94
|
+
await this.do("mkdir", { path });
|
|
95
|
+
}
|
|
96
|
+
async delete(path) {
|
|
97
|
+
await this.do("delete", { path });
|
|
98
|
+
}
|
|
99
|
+
async rename(path, target) {
|
|
100
|
+
await this.do("rename", { path, target });
|
|
101
|
+
}
|
|
102
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// Base64 that runs in the workflow sandbox, which has no atob/btoa or Buffer.
|
|
2
|
+
const ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
|
3
|
+
export function encodeBase64(bytes) {
|
|
4
|
+
let out = "";
|
|
5
|
+
for (let i = 0; i < bytes.length; i += 3) {
|
|
6
|
+
const a = bytes[i] ?? 0;
|
|
7
|
+
const b = i + 1 < bytes.length ? (bytes[i + 1] ?? 0) : 0;
|
|
8
|
+
const c = i + 2 < bytes.length ? (bytes[i + 2] ?? 0) : 0;
|
|
9
|
+
const n = (a << 16) | (b << 8) | c;
|
|
10
|
+
out += ALPHABET[(n >> 18) & 63] + ALPHABET[(n >> 12) & 63];
|
|
11
|
+
out += i + 1 < bytes.length ? ALPHABET[(n >> 6) & 63] : "=";
|
|
12
|
+
out += i + 2 < bytes.length ? ALPHABET[n & 63] : "=";
|
|
13
|
+
}
|
|
14
|
+
return out;
|
|
15
|
+
}
|
|
16
|
+
export function decodeBase64(value) {
|
|
17
|
+
let length = value.length;
|
|
18
|
+
while (length > 0 && value[length - 1] === "=")
|
|
19
|
+
length--;
|
|
20
|
+
const clean = value.slice(0, length);
|
|
21
|
+
if (clean.length % 4 === 1)
|
|
22
|
+
throw new Error("invalid base64");
|
|
23
|
+
const out = new Uint8Array((clean.length * 3) >> 2);
|
|
24
|
+
let n = 0;
|
|
25
|
+
for (let i = 0; i < clean.length; i += 4) {
|
|
26
|
+
const a = ALPHABET.indexOf(clean[i] ?? "");
|
|
27
|
+
const b = ALPHABET.indexOf(clean[i + 1] ?? "");
|
|
28
|
+
const c = i + 2 < clean.length ? ALPHABET.indexOf(clean[i + 2]) : -1;
|
|
29
|
+
const d = i + 3 < clean.length ? ALPHABET.indexOf(clean[i + 3]) : -1;
|
|
30
|
+
if (a < 0 || b < 0 || (c < 0 && i + 2 < clean.length) || (d < 0 && i + 3 < clean.length)) {
|
|
31
|
+
throw new Error("invalid base64");
|
|
32
|
+
}
|
|
33
|
+
const chunk = (a << 18) | (b << 12) | ((c < 0 ? 0 : c) << 6) | (d < 0 ? 0 : d);
|
|
34
|
+
out[n++] = (chunk >> 16) & 255;
|
|
35
|
+
if (c >= 0)
|
|
36
|
+
out[n++] = (chunk >> 8) & 255;
|
|
37
|
+
if (d >= 0)
|
|
38
|
+
out[n++] = chunk & 255;
|
|
39
|
+
}
|
|
40
|
+
return out.subarray(0, n);
|
|
41
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import type { AnyHarnessTool } from "../agent.ts";
|
|
2
|
+
import type { AgentWorkflowRunner } from "../agent-workflow.ts";
|
|
3
|
+
import { type TypeCheckStubs } from "./stubs.ts";
|
|
4
|
+
import { type FileOpCall, type Mount } from "./vfs.ts";
|
|
5
|
+
export declare const CODE_START_BATCH_ACTIVITY = "code_start_batch";
|
|
6
|
+
export declare const CODE_RESUME_BATCH_ACTIVITY = "code_resume_batch";
|
|
7
|
+
export declare const CODE_TYPE_CHECK_ACTIVITY = "code_type_check";
|
|
8
|
+
export declare const DEFAULT_STEP_TIMEOUT = "30 seconds";
|
|
9
|
+
export declare const STEP_RETRY: {
|
|
10
|
+
maximumAttempts: number;
|
|
11
|
+
};
|
|
12
|
+
export interface PendingCall {
|
|
13
|
+
/** Monty's call id. */
|
|
14
|
+
call_id: number;
|
|
15
|
+
function_name: string;
|
|
16
|
+
args: unknown[];
|
|
17
|
+
kwargs: Record<string, unknown>;
|
|
18
|
+
}
|
|
19
|
+
/** A pending call that is the script's `await asyncio.sleep(seconds)`, answered with a durable timer. */
|
|
20
|
+
export declare const SLEEP_CALL = "asyncio.sleep";
|
|
21
|
+
/** The workflow's clock (epoch ms) and the script's random seed: a script observes nothing of the worker. */
|
|
22
|
+
export interface HostClock {
|
|
23
|
+
now: number;
|
|
24
|
+
seed: number;
|
|
25
|
+
}
|
|
26
|
+
export interface CodeBatchStep {
|
|
27
|
+
done: boolean;
|
|
28
|
+
stdout: string;
|
|
29
|
+
/** Opaque resume state while awaiting a batch: Monty's snapshot dump. */
|
|
30
|
+
snapshot: string | null;
|
|
31
|
+
/** Every call the script made in this step, in call order. Each starts at once. */
|
|
32
|
+
calls: PendingCall[];
|
|
33
|
+
/** The calls the script is blocked on, in call order (made in this step or earlier). */
|
|
34
|
+
awaiting: number[];
|
|
35
|
+
/** Set when the step stopped at a file operation on a mount: the next resume answers it. */
|
|
36
|
+
file_op?: FileOpCall;
|
|
37
|
+
output_json: string | null;
|
|
38
|
+
error: string | null;
|
|
39
|
+
}
|
|
40
|
+
export interface CallResult {
|
|
41
|
+
call_id: number;
|
|
42
|
+
return_value: unknown;
|
|
43
|
+
/** Set when the host call failed or was denied: the script sees it as an exception it can catch. */
|
|
44
|
+
error?: string;
|
|
45
|
+
/** The Python exception the script raises for `error`. */
|
|
46
|
+
error_type?: string;
|
|
47
|
+
}
|
|
48
|
+
export interface ResumeBatchInput {
|
|
49
|
+
snapshot: string;
|
|
50
|
+
results: CallResult[];
|
|
51
|
+
type_names: string[];
|
|
52
|
+
clock: HostClock;
|
|
53
|
+
/** The script has mounts, so its file operations end steps. */
|
|
54
|
+
files?: boolean;
|
|
55
|
+
}
|
|
56
|
+
export interface CodeModeActivities {
|
|
57
|
+
code_start_batch(script: string, stubs: TypeCheckStubs | null, clock: HostClock, files?: boolean): Promise<CodeBatchStep>;
|
|
58
|
+
code_resume_batch(input: ResumeBatchInput): Promise<CodeBatchStep>;
|
|
59
|
+
code_type_check(script: string, stubs: TypeCheckStubs): Promise<string>;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Runs a Code Mode script to completion, dispatching its host calls to real harness tools.
|
|
63
|
+
*
|
|
64
|
+
* Constructed per invocation by the `codeModeTool` closure, with the live runner. `toolsByName` is
|
|
65
|
+
* validated once by the factory; `stubs` are the generated stubs the sandbox type-checks the
|
|
66
|
+
* script against before running it.
|
|
67
|
+
*/
|
|
68
|
+
export declare class CodeModeDriver {
|
|
69
|
+
#private;
|
|
70
|
+
constructor(runner: AgentWorkflowRunner, toolsByName: ReadonlyMap<string, AnyHarnessTool>, options: {
|
|
71
|
+
injections: Readonly<Record<string, unknown>>;
|
|
72
|
+
stubs: TypeCheckStubs;
|
|
73
|
+
mounts: readonly Mount[];
|
|
74
|
+
stepTimeout: string | number;
|
|
75
|
+
});
|
|
76
|
+
/** Drive `script` to completion; return its output and final value (or the error) as text. */
|
|
77
|
+
runScript(script: string): Promise<string>;
|
|
78
|
+
}
|