@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,94 @@
|
|
|
1
|
+
import { type Agent, type AgentInputItem, type FunctionTool, type MCPServer, type RunState, type StreamedRunResult } from "@openai/agents-core";
|
|
2
|
+
import { type StatelessMcpServerOptions as TemporalStatelessMcpServerOptions, type TemporalRunOptions } from "@temporalio/openai-agents/workflow";
|
|
3
|
+
import type { ModelActivityOptions } from "@temporalio/openai-agents";
|
|
4
|
+
import { type TurnStreamContext } from "../harness/agent-protocol/index.ts";
|
|
5
|
+
import type { AgentWorkflowRunner } from "../harness/agent-workflow.ts";
|
|
6
|
+
import type { AnyHarnessTool } from "../harness/agent-workflow/types.ts";
|
|
7
|
+
/**
|
|
8
|
+
* What `runStreamed` hands the streamed model activity, serialized into the runner's
|
|
9
|
+
* `streamingTopic` (an opaque string `@temporalio/openai-agents` forwards into the activity
|
|
10
|
+
* input). The activity adds the requested model, which the activity input carries, to make
|
|
11
|
+
* Python's `_HarnessStreamToken`.
|
|
12
|
+
*/
|
|
13
|
+
export interface HarnessStreamRoute {
|
|
14
|
+
context: TurnStreamContext;
|
|
15
|
+
}
|
|
16
|
+
export interface OpenAIAgentToolOptions {
|
|
17
|
+
/** Workflow-supplied values for the tools' injected parameters. */
|
|
18
|
+
injections?: Record<string, unknown>;
|
|
19
|
+
/** Send the tool's schema in OpenAI's strict mode, as the SDK's FunctionTool does (default). */
|
|
20
|
+
strictJsonSchema?: boolean;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Adapt a harness tool (`tool`, `activityTool` or a `subagentToolset` entry) into an OpenAI Agents
|
|
24
|
+
* SDK `FunctionTool`. Every model call of it runs through `runner.runTool(...)` under the model's
|
|
25
|
+
* call id, so the harness still owns the approval policy, `tool_start` / `tool_end` /
|
|
26
|
+
* `tool_error`, injected parameters and activity-backed execution. A failed call goes back to the
|
|
27
|
+
* model as text; an input that is not a JSON object matching the tool's schema fails the run.
|
|
28
|
+
*/
|
|
29
|
+
export declare function asOpenAIAgentTool(runner: AgentWorkflowRunner, tool: AnyHarnessTool, { injections, strictJsonSchema }?: OpenAIAgentToolOptions): FunctionTool<AgentWorkflowRunner>;
|
|
30
|
+
/** Adapt several harness tools; see `asOpenAIAgentTool`. */
|
|
31
|
+
export declare function asOpenAIAgentTools(runner: AgentWorkflowRunner, tools: readonly AnyHarnessTool[] | Readonly<Record<string, AnyHarnessTool>>, options?: OpenAIAgentToolOptions): FunctionTool<AgentWorkflowRunner>[];
|
|
32
|
+
export type RunStreamedOptions = Omit<TemporalRunOptions<AgentWorkflowRunner>, "stream" | "context"> & {
|
|
33
|
+
/**
|
|
34
|
+
* The `TemporalOpenAIRunner`'s `defaultModelParams`: model activity options written in the
|
|
35
|
+
* workflow, layered over the package defaults and under the `modelParams` of a client carrying
|
|
36
|
+
* the OpenAIAgentsPlugin. `streamingTopic` is the harness's turn route and cannot be set.
|
|
37
|
+
*/
|
|
38
|
+
defaultModelParams?: Omit<ModelActivityOptions, "streamingTopic">;
|
|
39
|
+
};
|
|
40
|
+
type AnyAgent = Agent<any, any>;
|
|
41
|
+
/**
|
|
42
|
+
* Run `agent` with streaming on the `TemporalOpenAIRunner`, every model call's events published
|
|
43
|
+
* to the current turn by the streamed model activity: `model_interaction_started`,
|
|
44
|
+
* `reply_delta`, `thought_summary`, `text_annotation`, `tool_requested`, then
|
|
45
|
+
* `model_interaction_ended`. The run context is `runner`, as Python authors pass
|
|
46
|
+
* `Runner.run_streamed(..., context=self._runner)`. Iterate the result (or await
|
|
47
|
+
* `result.completed`) to drive the run to its end.
|
|
48
|
+
*
|
|
49
|
+
* Refuses an agent whose tools are raw functions, or whose MCP servers were not built by a
|
|
50
|
+
* harness MCP factory (`statelessMcpServer`) or wrapped with `asHarnessMcpServer` and marked
|
|
51
|
+
* durable.
|
|
52
|
+
*/
|
|
53
|
+
export declare function runStreamed<TAgent extends AnyAgent>(runner: AgentWorkflowRunner, agent: TAgent, input: string | AgentInputItem[] | RunState<AgentWorkflowRunner, TAgent>, { defaultModelParams, ...options }?: RunStreamedOptions): Promise<StreamedRunResult<AgentWorkflowRunner, TAgent>>;
|
|
54
|
+
/**
|
|
55
|
+
* Declare that `server` replays durably, and return it. `runStreamed` rejects an MCP server
|
|
56
|
+
* without this mark: a plain SDK server (stdio, HTTP) re-runs its tool calls on replay. Mark only a
|
|
57
|
+
* server whose calls go through an activity or a Nexus operation.
|
|
58
|
+
*/
|
|
59
|
+
export declare function markDurableMcpServer<S extends MCPServer>(server: S): S;
|
|
60
|
+
/** Whether `server` was marked by `markDurableMcpServer`. */
|
|
61
|
+
export declare const isDurableMcpServer: (server: MCPServer) => boolean;
|
|
62
|
+
/** Whether `server` is already under harness governance. */
|
|
63
|
+
export declare const isHarnessMcpServer: (server: MCPServer) => boolean;
|
|
64
|
+
export interface HarnessMcpOptions {
|
|
65
|
+
/** Every tool on this server is safe under any input; the policy decides what that skips. */
|
|
66
|
+
inherentlySafe?: boolean;
|
|
67
|
+
/**
|
|
68
|
+
* The criteria set AUTO MODE judges this server's tools against. `AutoApprovalCriteria.tools`
|
|
69
|
+
* still assigns per tool name.
|
|
70
|
+
*/
|
|
71
|
+
autoApprovalCriteria?: string;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Put an OpenAI Agents SDK MCP server under harness tool governance. Returns the same server with
|
|
75
|
+
* its `callTool` wrapped, so every MCP tool call is evaluated against the agent's
|
|
76
|
+
* ToolApprovalPolicy (waiting for a human when it does not auto-approve) and publishes
|
|
77
|
+
* `tool_start`, then `tool_end` or `tool_error`, under the model's call id. A denied call returns
|
|
78
|
+
* an `isError` result to the model instead of throwing. Wrapping twice is a no-op.
|
|
79
|
+
*/
|
|
80
|
+
export declare function asHarnessMcpServer<S extends MCPServer>(server: S, runner: AgentWorkflowRunner, { inherentlySafe, autoApprovalCriteria }?: HarnessMcpOptions): S;
|
|
81
|
+
/** Put several MCP servers under harness tool governance; see `asHarnessMcpServer`. */
|
|
82
|
+
export declare const asHarnessMcpServers: (servers: Iterable<MCPServer>, runner: AgentWorkflowRunner, options?: HarnessMcpOptions) => MCPServer[];
|
|
83
|
+
export type StatelessMcpServerOptions = TemporalStatelessMcpServerOptions & Pick<HarnessMcpOptions, "inherentlySafe"> & {
|
|
84
|
+
/** The agent's runner: governance needs it, and the SDK calls `callTool` with no context. */
|
|
85
|
+
runner: AgentWorkflowRunner;
|
|
86
|
+
};
|
|
87
|
+
/**
|
|
88
|
+
* A governed, durable stateless MCP server: each list and call runs as an activity against the
|
|
89
|
+
* `StatelessMCPServerProvider` of the same `name` registered on the worker's OpenAIAgentsPlugin,
|
|
90
|
+
* under the harness approval policy and tool lifecycle. Lists its tools anew per run unless
|
|
91
|
+
* `cacheToolsList` (default false).
|
|
92
|
+
*/
|
|
93
|
+
export declare function statelessMcpServer(name: string, { runner, inherentlySafe, cacheToolsList, ...options }: StatelessMcpServerOptions): MCPServer;
|
|
94
|
+
export {};
|
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
// Harness glue for the OpenAI Agents SDK, workflow side (Python's openai_agents_harness.py, the
|
|
2
|
+
// half that runs in the workflow): `asOpenAIAgentTool(s)` adapting harness tools onto the SDK,
|
|
3
|
+
// `runStreamed` running an Agents SDK agent on `TemporalOpenAIRunner` with its model calls routed
|
|
4
|
+
// to the in-flight turn, and the MCP governance wrappers putting an SDK MCP server under the
|
|
5
|
+
// harness approval policy and tool lifecycle. The worker half (the observer that turns each model
|
|
6
|
+
// call's stream into turn events, and the streamed model activity that runs it) is
|
|
7
|
+
// `@temporalio/agent-harness-worker/ai-sdks/openai-agents-harness`.
|
|
8
|
+
//
|
|
9
|
+
// Only `@openai/agents-core` types are imported here, except `RunContext`. Workers bundle workflow
|
|
10
|
+
// code with the OpenAIAgentsPlugin from `@temporalio/agent-harness-worker/ai-sdks/openai-agents`,
|
|
11
|
+
// which resolves `@openai/agents-core` to the copy `@temporalio/openai-agents` loads.
|
|
12
|
+
import { RunContext, } from "@openai/agents-core";
|
|
13
|
+
import { TemporalOpenAIRunner, statelessMcpServer as temporalStatelessMcpServer, } from "@temporalio/openai-agents/workflow";
|
|
14
|
+
import { ApplicationFailure, AsyncLocalStorage, isCancellation, uuid4 } from "@temporalio/workflow";
|
|
15
|
+
import { validate } from "../harness/agent-protocol/index.js";
|
|
16
|
+
import { ToolApprovalDenied, errorMessage } from "../harness/agent-workflow/errors.js";
|
|
17
|
+
// ---------------------------------------------------------------------------------------------
|
|
18
|
+
// Tool adapter: harness tools -> OpenAI Agents SDK FunctionTools
|
|
19
|
+
// ---------------------------------------------------------------------------------------------
|
|
20
|
+
const HARNESS_TOOL_KINDS = new Set(["inline", "activity", "callback"]);
|
|
21
|
+
/**
|
|
22
|
+
* Adapt a harness tool (`tool`, `activityTool` or a `subagentToolset` entry) into an OpenAI Agents
|
|
23
|
+
* SDK `FunctionTool`. Every model call of it runs through `runner.runTool(...)` under the model's
|
|
24
|
+
* call id, so the harness still owns the approval policy, `tool_start` / `tool_end` /
|
|
25
|
+
* `tool_error`, injected parameters and activity-backed execution. A failed call goes back to the
|
|
26
|
+
* model as text; an input that is not a JSON object matching the tool's schema fails the run.
|
|
27
|
+
*/
|
|
28
|
+
export function asOpenAIAgentTool(runner, tool, { injections, strictJsonSchema = true } = {}) {
|
|
29
|
+
requireHarnessTool(tool);
|
|
30
|
+
const name = tool.name;
|
|
31
|
+
return {
|
|
32
|
+
type: "function",
|
|
33
|
+
name,
|
|
34
|
+
description: tool.description,
|
|
35
|
+
parameters: (strictJsonSchema
|
|
36
|
+
? ensureStrictJsonSchema(tool.parameters)
|
|
37
|
+
: tool.parameters),
|
|
38
|
+
strict: strictJsonSchema,
|
|
39
|
+
// Rethrow: an invalid input fails the run, as Python's on_invoke_tool raises it.
|
|
40
|
+
errorFunction: null,
|
|
41
|
+
needsApproval: async () => false,
|
|
42
|
+
isEnabled: async () => true,
|
|
43
|
+
async invoke(_runContext, input, details) {
|
|
44
|
+
let json;
|
|
45
|
+
try {
|
|
46
|
+
json = JSON.parse(input || "{}");
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
throw invalidToolInput(`Invalid JSON input for tool ${name}: ${input}`);
|
|
50
|
+
}
|
|
51
|
+
if (json === null || typeof json !== "object" || Array.isArray(json)) {
|
|
52
|
+
const got = Array.isArray(json) ? "list" : typeof json;
|
|
53
|
+
throw invalidToolInput(`Tool ${name} expected a JSON object input, got ${got}.`);
|
|
54
|
+
}
|
|
55
|
+
const parsed = validate(tool.inputSchema, json);
|
|
56
|
+
if (!parsed.ok)
|
|
57
|
+
throw invalidToolInput(`Payload for tool '${name}' does not match its schema: ${parsed.message}`);
|
|
58
|
+
const callId = details?.toolCall?.callId || newToolCallId(name);
|
|
59
|
+
let result;
|
|
60
|
+
try {
|
|
61
|
+
result = await runner.runTool(callId, tool, json, injections ? { injections } : {});
|
|
62
|
+
}
|
|
63
|
+
catch (e) {
|
|
64
|
+
if (isCancellation(e))
|
|
65
|
+
throw e;
|
|
66
|
+
return `Tool '${name}' failed: ${errorMessage(e)}`;
|
|
67
|
+
}
|
|
68
|
+
return typeof result === "string" ? result : JSON.stringify(result ?? null);
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
/** Adapt several harness tools; see `asOpenAIAgentTool`. */
|
|
73
|
+
export function asOpenAIAgentTools(runner, tools, options) {
|
|
74
|
+
const list = Array.isArray(tools)
|
|
75
|
+
? tools
|
|
76
|
+
: Object.values(tools);
|
|
77
|
+
return list.map((t) => asOpenAIAgentTool(runner, t, options));
|
|
78
|
+
}
|
|
79
|
+
function requireHarnessTool(tool) {
|
|
80
|
+
const candidate = tool;
|
|
81
|
+
if (candidate !== null &&
|
|
82
|
+
typeof candidate === "object" &&
|
|
83
|
+
HARNESS_TOOL_KINDS.has(candidate.kind ?? "") &&
|
|
84
|
+
typeof candidate.invoke === "function")
|
|
85
|
+
return;
|
|
86
|
+
throw new TypeError(`${String(candidate?.name ?? candidate)} is not a harness tool; declare it with tool(...), ` +
|
|
87
|
+
"activityTool(...), or use subagentToolset(...).");
|
|
88
|
+
}
|
|
89
|
+
function invalidToolInput(message) {
|
|
90
|
+
return ApplicationFailure.create({ type: "InvalidToolInput", nonRetryable: true, message });
|
|
91
|
+
}
|
|
92
|
+
function newToolCallId(toolName) {
|
|
93
|
+
return `openai_agents:${toolName}:${uuid4()}`;
|
|
94
|
+
}
|
|
95
|
+
const isSchemaObject = (v) => v !== null && typeof v === "object" && !Array.isArray(v);
|
|
96
|
+
/**
|
|
97
|
+
* The Agents SDK's `ensure_strict_json_schema` (Python), which OpenAI's strict mode needs: every
|
|
98
|
+
* object closed and every property required, a lone `allOf` inlined, `default: null` dropped, and
|
|
99
|
+
* a `$ref` with siblings expanded. Returns a copy.
|
|
100
|
+
*/
|
|
101
|
+
function ensureStrictJsonSchema(schema) {
|
|
102
|
+
if (Object.keys(schema).length === 0)
|
|
103
|
+
return { additionalProperties: false, type: "object", properties: {}, required: [] };
|
|
104
|
+
const root = JSON.parse(JSON.stringify(schema));
|
|
105
|
+
return strictify(root, root);
|
|
106
|
+
}
|
|
107
|
+
function strictify(node, root) {
|
|
108
|
+
for (const key of ["$defs", "definitions"]) {
|
|
109
|
+
const defs = node[key];
|
|
110
|
+
if (isSchemaObject(defs))
|
|
111
|
+
for (const [name, def] of Object.entries(defs))
|
|
112
|
+
if (isSchemaObject(def))
|
|
113
|
+
defs[name] = strictify(def, root);
|
|
114
|
+
}
|
|
115
|
+
if (node.type === "object") {
|
|
116
|
+
if (!("additionalProperties" in node))
|
|
117
|
+
node.additionalProperties = false;
|
|
118
|
+
else if (node.additionalProperties)
|
|
119
|
+
throw new TypeError("additionalProperties should not be set for object types. This could be because you're " +
|
|
120
|
+
"using an older version of Pydantic, or because you configured additional properties " +
|
|
121
|
+
"to be allowed. If you really need this, set strictJsonSchema: false.");
|
|
122
|
+
}
|
|
123
|
+
const properties = node.properties;
|
|
124
|
+
if (isSchemaObject(properties)) {
|
|
125
|
+
node.required = Object.keys(properties);
|
|
126
|
+
for (const [name, prop] of Object.entries(properties))
|
|
127
|
+
if (isSchemaObject(prop))
|
|
128
|
+
properties[name] = strictify(prop, root);
|
|
129
|
+
}
|
|
130
|
+
if (isSchemaObject(node.items))
|
|
131
|
+
node.items = strictify(node.items, root);
|
|
132
|
+
for (const key of ["anyOf", "oneOf"]) {
|
|
133
|
+
const variants = node[key];
|
|
134
|
+
if (Array.isArray(variants))
|
|
135
|
+
node[key] = variants.map((v) => (isSchemaObject(v) ? strictify(v, root) : v));
|
|
136
|
+
}
|
|
137
|
+
const allOf = node.allOf;
|
|
138
|
+
if (Array.isArray(allOf)) {
|
|
139
|
+
if (allOf.length === 1 && isSchemaObject(allOf[0])) {
|
|
140
|
+
Object.assign(node, strictify(allOf[0], root));
|
|
141
|
+
delete node.allOf;
|
|
142
|
+
}
|
|
143
|
+
else {
|
|
144
|
+
node.allOf = allOf.map((v) => (isSchemaObject(v) ? strictify(v, root) : v));
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
if ("default" in node && node.default === null)
|
|
148
|
+
delete node.default;
|
|
149
|
+
const ref = node.$ref;
|
|
150
|
+
if (typeof ref === "string" && Object.keys(node).length > 1) {
|
|
151
|
+
const resolved = resolveRef(root, ref);
|
|
152
|
+
const expanded = { ...resolved, ...node };
|
|
153
|
+
delete expanded.$ref;
|
|
154
|
+
for (const key of Object.keys(node))
|
|
155
|
+
delete node[key];
|
|
156
|
+
Object.assign(node, expanded);
|
|
157
|
+
return strictify(node, root);
|
|
158
|
+
}
|
|
159
|
+
return node;
|
|
160
|
+
}
|
|
161
|
+
function resolveRef(root, ref) {
|
|
162
|
+
if (!ref.startsWith("#/"))
|
|
163
|
+
throw new TypeError(`Unexpected $ref format '${ref}'; does not start with #/`);
|
|
164
|
+
let node = root;
|
|
165
|
+
for (const part of ref.slice(2).split("/")) {
|
|
166
|
+
if (!isSchemaObject(node))
|
|
167
|
+
break;
|
|
168
|
+
node = node[part];
|
|
169
|
+
}
|
|
170
|
+
if (!isSchemaObject(node))
|
|
171
|
+
throw new TypeError(`Encountered non-object $ref target '${ref}'`);
|
|
172
|
+
return node;
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Run `agent` with streaming on the `TemporalOpenAIRunner`, every model call's events published
|
|
176
|
+
* to the current turn by the streamed model activity: `model_interaction_started`,
|
|
177
|
+
* `reply_delta`, `thought_summary`, `text_annotation`, `tool_requested`, then
|
|
178
|
+
* `model_interaction_ended`. The run context is `runner`, as Python authors pass
|
|
179
|
+
* `Runner.run_streamed(..., context=self._runner)`. Iterate the result (or await
|
|
180
|
+
* `result.completed`) to drive the run to its end.
|
|
181
|
+
*
|
|
182
|
+
* Refuses an agent whose tools are raw functions, or whose MCP servers were not built by a
|
|
183
|
+
* harness MCP factory (`statelessMcpServer`) or wrapped with `asHarnessMcpServer` and marked
|
|
184
|
+
* durable.
|
|
185
|
+
*/
|
|
186
|
+
export async function runStreamed(runner, agent, input, { defaultModelParams, ...options } = {}) {
|
|
187
|
+
const context = runner.currentStreamContext;
|
|
188
|
+
if (context === null)
|
|
189
|
+
throw new Error("runStreamed() must be called within an active turn, from a message handler");
|
|
190
|
+
for (const t of agent.tools) {
|
|
191
|
+
if (typeof t === "function")
|
|
192
|
+
throw new TypeError("Provided tool is not a tool type. If using an activity, make sure to wrap it with " +
|
|
193
|
+
"activityAsTool from @temporalio/openai-agents/workflow.");
|
|
194
|
+
}
|
|
195
|
+
const starting = await withMcpCallIds(agent, runner);
|
|
196
|
+
const route = { context };
|
|
197
|
+
const temporalRunner = new TemporalOpenAIRunner({
|
|
198
|
+
defaultModelParams: { ...defaultModelParams, streamingTopic: JSON.stringify(route) },
|
|
199
|
+
});
|
|
200
|
+
return temporalRunner.run(starting, input, { ...options, context: runner, stream: true });
|
|
201
|
+
}
|
|
202
|
+
// ---------------------------------------------------------------------------------------------
|
|
203
|
+
// MCP adapter: harness gating + tool lifecycle events for an SDK MCP server
|
|
204
|
+
// ---------------------------------------------------------------------------------------------
|
|
205
|
+
//
|
|
206
|
+
// `asHarnessMcpServer` wraps the server's `callTool` so the call funnels through `runTool` exactly
|
|
207
|
+
// like a harness tool: policy gate first, then tool_start -> tool_end / tool_error.
|
|
208
|
+
const MCP_GOVERNED = Symbol.for("temporal.harness/mcp-governed");
|
|
209
|
+
const MCP_DURABLE = Symbol.for("temporal.harness/mcp-durable");
|
|
210
|
+
// agents-core hands `callTool` no call id (its meta resolver sees only the run context), so
|
|
211
|
+
// `runStreamed` wraps each MCP function tool to put the model's call id in scope here.
|
|
212
|
+
const mcpCallId = new AsyncLocalStorage();
|
|
213
|
+
/**
|
|
214
|
+
* Declare that `server` replays durably, and return it. `runStreamed` rejects an MCP server
|
|
215
|
+
* without this mark: a plain SDK server (stdio, HTTP) re-runs its tool calls on replay. Mark only a
|
|
216
|
+
* server whose calls go through an activity or a Nexus operation.
|
|
217
|
+
*/
|
|
218
|
+
export function markDurableMcpServer(server) {
|
|
219
|
+
server[MCP_DURABLE] = true;
|
|
220
|
+
return server;
|
|
221
|
+
}
|
|
222
|
+
/** Whether `server` was marked by `markDurableMcpServer`. */
|
|
223
|
+
export const isDurableMcpServer = (server) => server[MCP_DURABLE] === true;
|
|
224
|
+
/** Whether `server` is already under harness governance. */
|
|
225
|
+
export const isHarnessMcpServer = (server) => server[MCP_GOVERNED] === true;
|
|
226
|
+
/**
|
|
227
|
+
* Put an OpenAI Agents SDK MCP server under harness tool governance. Returns the same server with
|
|
228
|
+
* its `callTool` wrapped, so every MCP tool call is evaluated against the agent's
|
|
229
|
+
* ToolApprovalPolicy (waiting for a human when it does not auto-approve) and publishes
|
|
230
|
+
* `tool_start`, then `tool_end` or `tool_error`, under the model's call id. A denied call returns
|
|
231
|
+
* an `isError` result to the model instead of throwing. Wrapping twice is a no-op.
|
|
232
|
+
*/
|
|
233
|
+
export function asHarnessMcpServer(server, runner, { inherentlySafe = false, autoApprovalCriteria } = {}) {
|
|
234
|
+
if (isHarnessMcpServer(server))
|
|
235
|
+
return server;
|
|
236
|
+
const inner = server.callTool.bind(server);
|
|
237
|
+
server.callTool = (toolName, args, meta, options) => {
|
|
238
|
+
const toolId = mcpCallId.getStore() ?? newToolCallId(toolName);
|
|
239
|
+
const toolInput = { ...args };
|
|
240
|
+
const invoke = async () => {
|
|
241
|
+
try {
|
|
242
|
+
await runner.applyApprovalPolicy(toolId, toolName, toolInput, {
|
|
243
|
+
inherentlySafe,
|
|
244
|
+
description: null,
|
|
245
|
+
autoApprovalCriteria: autoApprovalCriteria ?? null,
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
catch (e) {
|
|
249
|
+
// The gate already published tool_approval_resolved(approved=false); the call never
|
|
250
|
+
// starts, so it gets no tool_start/tool_error bracket.
|
|
251
|
+
if (!(e instanceof ToolApprovalDenied))
|
|
252
|
+
throw e;
|
|
253
|
+
return mcpErrorResult(e.message);
|
|
254
|
+
}
|
|
255
|
+
runner.publish({
|
|
256
|
+
type: "tool_start",
|
|
257
|
+
tool_id: toolId,
|
|
258
|
+
tool_name: toolName,
|
|
259
|
+
tool_input: toolInput,
|
|
260
|
+
});
|
|
261
|
+
let result;
|
|
262
|
+
try {
|
|
263
|
+
result = await inner(toolName, args, meta, options);
|
|
264
|
+
}
|
|
265
|
+
catch (e) {
|
|
266
|
+
// Close the bracket, then let the error travel on, as the tool wrappers do.
|
|
267
|
+
runner.publish({
|
|
268
|
+
type: "tool_error",
|
|
269
|
+
tool_id: toolId,
|
|
270
|
+
tool_name: toolName,
|
|
271
|
+
message: errorMessage(e),
|
|
272
|
+
});
|
|
273
|
+
throw e;
|
|
274
|
+
}
|
|
275
|
+
// An MCP server reports a failed call in the result, not by raising.
|
|
276
|
+
const text = mcpResultText(result);
|
|
277
|
+
runner.publish(result.isError
|
|
278
|
+
? { type: "tool_error", tool_id: toolId, tool_name: toolName, message: text }
|
|
279
|
+
: { type: "tool_end", tool_id: toolId, tool_name: toolName, tool_output: text });
|
|
280
|
+
return result;
|
|
281
|
+
};
|
|
282
|
+
return runner.runTool(toolId, { name: toolName, invoke }, undefined);
|
|
283
|
+
};
|
|
284
|
+
server[MCP_GOVERNED] = true;
|
|
285
|
+
return server;
|
|
286
|
+
}
|
|
287
|
+
/** Put several MCP servers under harness tool governance; see `asHarnessMcpServer`. */
|
|
288
|
+
export const asHarnessMcpServers = (servers, runner, options) => [...servers].map((s) => asHarnessMcpServer(s, runner, options));
|
|
289
|
+
/**
|
|
290
|
+
* A governed, durable stateless MCP server: each list and call runs as an activity against the
|
|
291
|
+
* `StatelessMCPServerProvider` of the same `name` registered on the worker's OpenAIAgentsPlugin,
|
|
292
|
+
* under the harness approval policy and tool lifecycle. Lists its tools anew per run unless
|
|
293
|
+
* `cacheToolsList` (default false).
|
|
294
|
+
*/
|
|
295
|
+
export function statelessMcpServer(name, { runner, inherentlySafe, cacheToolsList = false, ...options }) {
|
|
296
|
+
const server = temporalStatelessMcpServer(name, { ...options, cacheToolsList });
|
|
297
|
+
// The worker's call activity is the harness's, which returns `{content, isError}`: the content
|
|
298
|
+
// array alone would lose the server's isError flag to serialization.
|
|
299
|
+
const callActivity = server.callTool.bind(server);
|
|
300
|
+
server.callTool = async (toolName, args, meta, callOptions) => fromCallToolWire(await callActivity(toolName, args, meta, callOptions));
|
|
301
|
+
return asHarnessMcpServer(markDurableMcpServer(server), runner, { inherentlySafe });
|
|
302
|
+
}
|
|
303
|
+
/** The harness MCP call activity's `{content, isError}` as agents-core's content-with-flag. */
|
|
304
|
+
function fromCallToolWire(wire) {
|
|
305
|
+
if (Array.isArray(wire))
|
|
306
|
+
return wire;
|
|
307
|
+
const { content, isError } = wire;
|
|
308
|
+
return isError ? Object.assign(content, { isError: true }) : content;
|
|
309
|
+
}
|
|
310
|
+
function mcpResultText(content) {
|
|
311
|
+
const texts = content.flatMap((block) => block.type === "text" && typeof block.text === "string" ? [block.text] : []);
|
|
312
|
+
return texts.length ? texts.join("\n") : JSON.stringify(content);
|
|
313
|
+
}
|
|
314
|
+
function mcpErrorResult(message) {
|
|
315
|
+
return Object.assign([{ type: "text", text: message }], { isError: true });
|
|
316
|
+
}
|
|
317
|
+
/**
|
|
318
|
+
* Validate `agent`'s MCP servers and turn them into plain function tools whose calls run with the
|
|
319
|
+
* model's call id in scope, so a governed server's events share the `tool_id` of the model's
|
|
320
|
+
* `tool_requested`. The servers' tools are listed here, once per run.
|
|
321
|
+
*/
|
|
322
|
+
async function withMcpCallIds(agent, runner) {
|
|
323
|
+
if (agent.mcpServers.length === 0)
|
|
324
|
+
return agent;
|
|
325
|
+
for (const s of agent.mcpServers) {
|
|
326
|
+
if (!isDurableMcpServer(s))
|
|
327
|
+
throw new TypeError(`Unknown mcp_server '${s.name}' may not work durably. Build it with a harness ` +
|
|
328
|
+
"mcp_server factory.");
|
|
329
|
+
if (!isHarnessMcpServer(s))
|
|
330
|
+
throw new TypeError(`MCP server '${s.name}' was not built by a harness mcp_server factory. Build it ` +
|
|
331
|
+
"with one, or wrap it with asHarnessMcpServer().");
|
|
332
|
+
}
|
|
333
|
+
// Fresh tool objects on every listing, so wrapping them in place touches no shared state.
|
|
334
|
+
const tools = await agent.getMcpTools(new RunContext(runner));
|
|
335
|
+
for (const t of tools) {
|
|
336
|
+
if (t.type !== "function")
|
|
337
|
+
continue;
|
|
338
|
+
const invoke = t.invoke;
|
|
339
|
+
t.invoke = (ctx, input, details) => {
|
|
340
|
+
const callId = details?.toolCall?.callId;
|
|
341
|
+
return callId
|
|
342
|
+
? mcpCallId.run(callId, () => invoke(ctx, input, details))
|
|
343
|
+
: invoke(ctx, input, details);
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
return agent.clone({ mcpServers: [], tools: [...agent.tools, ...tools] });
|
|
347
|
+
}
|