@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.
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
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Temporal Technologies Inc. All rights reserved.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,6 @@
1
- # Temporary Holding Version
1
+ # @temporalio/agent-harness
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ The workflow-safe authoring API of the Temporal agent harness: `defineAgent`, handlers, tools,
4
+ approvals, state and the protocol, with no Node dependencies. Agents written with it run on a
5
+ worker from [`@temporalio/agent-harness-worker`](../worker/README.md), whose README also shows how
6
+ to serve them to the harness's server and console.
@@ -0,0 +1,6 @@
1
+ export { InteractionConversation, type FunctionCallStep, type FunctionResultStepParam, type InteractionReply, } from "./interactions-conversation.ts";
2
+ export type { InteractionResult } from "./interactions-models.ts";
3
+ export { functionParam } from "./interactions-workflow.ts";
4
+ export { TemporalAsyncClient } from "./temporal-async-client.ts";
5
+ export { GEMINI_INTERACTIONS_CREATE_STREAMED_ACTIVITY, TemporalAsyncInteractionStream, TemporalAsyncInteractions, type GeminiInteractionsActivity, type InteractionCreateParamsStreaming, } from "./temporal-interactions.ts";
6
+ export { googleGenaiClient } from "./workflow.ts";
@@ -0,0 +1,24 @@
1
+ // The Google Gemini integration's workflow side (Python's google_genai_plugin). Experimental.
2
+ //
3
+ // `googleGenaiClient` gives workflow code a Gemini client whose network calls run as activities on
4
+ // the worker's real client, registered by `GoogleGenAIPlugin` from
5
+ // `@temporalio/agent-harness-worker/ai-sdks/google-genai-plugin`; no credentials enter the
6
+ // workflow or its history. The Interactions API has no automatic function calling, so the workflow
7
+ // drives the tool loop: declare tools with `functionParam`, keep the conversation in an
8
+ // `InteractionConversation`, and run each call with `runner.runTool`.
9
+ //
10
+ // const gemini = googleGenaiClient({ runner });
11
+ // conversation.addUserText(text);
12
+ // while (true) {
13
+ // const stream = await gemini.interactions.create({
14
+ // model, input: conversation.steps, tools: tools.map(functionParam), stream: true,
15
+ // });
16
+ // const reply = await conversation.readReply(stream);
17
+ // if (reply.functionCalls.length === 0) return reply.text;
18
+ // conversation.addFunctionResults(await Promise.all(reply.functionCalls.map(runCall)));
19
+ // }
20
+ export { InteractionConversation, } from "./interactions-conversation.js";
21
+ export { functionParam } from "./interactions-workflow.js";
22
+ export { TemporalAsyncClient } from "./temporal-async-client.js";
23
+ export { GEMINI_INTERACTIONS_CREATE_STREAMED_ACTIVITY, TemporalAsyncInteractionStream, TemporalAsyncInteractions, } from "./temporal-interactions.js";
24
+ export { googleGenaiClient } from "./workflow.js";
@@ -0,0 +1,35 @@
1
+ import type { Interactions } from "@google/genai";
2
+ /** A function call the model made. The API sends a `signature` the SDK's type does not declare. */
3
+ export type FunctionCallStep = Interactions.FunctionCallStep & {
4
+ signature?: string;
5
+ };
6
+ /** The result of one function call, to send back with `addFunctionResults`. */
7
+ export type FunctionResultStepParam = Interactions.FunctionResultStep & {
8
+ signature?: string;
9
+ };
10
+ /** One model reply: its text, the function calls it made, and the interaction's id. */
11
+ export interface InteractionReply {
12
+ text: string;
13
+ functionCalls: FunctionCallStep[];
14
+ interactionId: string;
15
+ }
16
+ /**
17
+ * The steps of one conversation with a Gemini model, oldest first. Pass `steps` as the `input` of
18
+ * every `interactions.create`, without `previous_interaction_id`.
19
+ */
20
+ export declare class InteractionConversation {
21
+ #private;
22
+ /** The whole conversation, to send as `input`. */
23
+ get steps(): Interactions.Step[];
24
+ addUserText(text: string): void;
25
+ /** The results of the function calls in the last reply, in any order. */
26
+ addFunctionResults(results: Iterable<FunctionResultStepParam>): void;
27
+ /**
28
+ * Read one streamed `interactions.create` to its end and add the model's steps.
29
+ *
30
+ * The steps are added only once the stream completes, so a failed call leaves the conversation
31
+ * as it was. Throws an ApplicationFailure on an error event, on a stream that ends without
32
+ * completing, and (non-retryably) on a step this conversation cannot carry back to the model.
33
+ */
34
+ readReply(stream: AsyncIterable<Interactions.InteractionSSEEvent>): Promise<InteractionReply>;
35
+ }
@@ -0,0 +1,178 @@
1
+ // A Gemini Interactions conversation that the workflow holds and sends whole on every call
2
+ // (Python's _interactions_conversation.py).
3
+ //
4
+ // Why not chain with `previous_interaction_id`: the API rebuilds a chained conversation from the
5
+ // stored interactions, and a STREAMED interaction stores only its own steps. The rebuild reads the
6
+ // previous interaction and the one before it, no further, so once two streamed interactions follow
7
+ // each other the rebuilt conversation starts partway through, at a function result whose call is
8
+ // missing, and the API rejects the next function result with 400 "Please ensure that function
9
+ // response turn comes immediately after a function call turn". A tool-calling loop that streams
10
+ // every model call hits that on its third tool result.
11
+ //
12
+ // So the conversation lives here, in workflow state, and goes out as the `input` of every
13
+ // `interactions.create`. That also makes the workflow the record of what the model was told: a
14
+ // replay, a retry or a restarted worker sends exactly the same conversation. The cost is that
15
+ // every model call carries the whole conversation in its activity input.
16
+ //
17
+ // A typical tool-calling turn:
18
+ //
19
+ // conversation.addUserText(text);
20
+ // while (true) {
21
+ // const stream = await gemini.interactions.create({
22
+ // model, input: conversation.steps, tools, stream: true,
23
+ // });
24
+ // const reply = await conversation.readReply(stream);
25
+ // if (reply.functionCalls.length === 0) return reply.text;
26
+ // conversation.addFunctionResults(await runCalls(reply.functionCalls));
27
+ // }
28
+ import { ApplicationFailure } from "@temporalio/workflow";
29
+ /**
30
+ * The steps of one conversation with a Gemini model, oldest first. Pass `steps` as the `input` of
31
+ * every `interactions.create`, without `previous_interaction_id`.
32
+ */
33
+ export class InteractionConversation {
34
+ #steps = [];
35
+ /** The whole conversation, to send as `input`. */
36
+ get steps() {
37
+ return [...this.#steps];
38
+ }
39
+ addUserText(text) {
40
+ this.#steps.push({ type: "user_input", content: [{ type: "text", text }] });
41
+ }
42
+ /** The results of the function calls in the last reply, in any order. */
43
+ addFunctionResults(results) {
44
+ for (const result of results)
45
+ this.#steps.push({ ...result });
46
+ }
47
+ /**
48
+ * Read one streamed `interactions.create` to its end and add the model's steps.
49
+ *
50
+ * The steps are added only once the stream completes, so a failed call leaves the conversation
51
+ * as it was. Throws an ApplicationFailure on an error event, on a stream that ends without
52
+ * completing, and (non-retryably) on a step this conversation cannot carry back to the model.
53
+ */
54
+ async readReply(stream) {
55
+ const thoughts = new Map();
56
+ const calls = new Map();
57
+ const args = new Map();
58
+ const outputs = new Map();
59
+ let interactionId = null;
60
+ for await (const event of stream) {
61
+ switch (event.event_type) {
62
+ case "error":
63
+ throw ApplicationFailure.create({
64
+ type: event.error?.code || "stream_error",
65
+ message: event.error?.message || "stream error",
66
+ });
67
+ case "step.start": {
68
+ const { index, step } = event;
69
+ if (step.type === "thought") {
70
+ const thought = { type: "thought" };
71
+ if (step.signature)
72
+ thought.signature = step.signature;
73
+ if (step.summary?.length)
74
+ thought.summary = step.summary.map(textParam);
75
+ thoughts.set(index, thought);
76
+ }
77
+ else if (step.type === "function_call") {
78
+ calls.set(index, step);
79
+ }
80
+ else if (step.type === "model_output") {
81
+ outputs.set(index, (step.content ?? []).map((c) => textParam(c).text).join(""));
82
+ }
83
+ else {
84
+ throw unsupported(`produced a '${step.type}' step`);
85
+ }
86
+ break;
87
+ }
88
+ case "step.delta": {
89
+ const { index, delta } = event;
90
+ const thought = thoughts.get(index);
91
+ if (delta.type === "text" && outputs.has(index)) {
92
+ outputs.set(index, outputs.get(index) + (delta.text ?? ""));
93
+ }
94
+ else if (delta.type === "arguments_delta" && calls.has(index)) {
95
+ args.set(index, (args.get(index) ?? "") + (delta.arguments ?? ""));
96
+ }
97
+ else if (delta.type === "thought_signature" && thought) {
98
+ if (delta.signature)
99
+ thought.signature = delta.signature;
100
+ }
101
+ else if (delta.type === "thought_summary" && thought) {
102
+ if (delta.content !== undefined)
103
+ thought.summary = [...(thought.summary ?? []), textParam(delta.content)];
104
+ }
105
+ else {
106
+ throw unsupported(`streamed a '${delta.type}' delta`);
107
+ }
108
+ break;
109
+ }
110
+ case "interaction.completed":
111
+ interactionId = event.interaction.id;
112
+ break;
113
+ }
114
+ }
115
+ if (interactionId === null)
116
+ throw ApplicationFailure.create({
117
+ type: "stream_error",
118
+ message: "stream ended without interaction.completed event",
119
+ });
120
+ const modelSteps = [];
121
+ const functionCalls = [];
122
+ const indexes = [...new Set([...thoughts.keys(), ...calls.keys(), ...outputs.keys()])];
123
+ for (const index of indexes.sort((a, b) => a - b)) {
124
+ const thought = thoughts.get(index);
125
+ let call = calls.get(index);
126
+ if (thought) {
127
+ modelSteps.push(thought);
128
+ }
129
+ else if (call) {
130
+ const raw = args.get(index);
131
+ if (raw !== undefined)
132
+ call = { ...call, arguments: parseArguments(raw) };
133
+ functionCalls.push(call);
134
+ const callStep = {
135
+ type: "function_call",
136
+ id: call.id,
137
+ name: call.name,
138
+ arguments: call.arguments,
139
+ };
140
+ if (call.signature)
141
+ callStep.signature = call.signature;
142
+ modelSteps.push(callStep);
143
+ }
144
+ else {
145
+ const text = outputs.get(index);
146
+ modelSteps.push(text
147
+ ? { type: "model_output", content: [{ type: "text", text }] }
148
+ : { type: "model_output" });
149
+ }
150
+ }
151
+ this.#steps.push(...modelSteps);
152
+ const text = [...outputs.entries()]
153
+ .sort(([a], [b]) => a - b)
154
+ .map(([, t]) => t)
155
+ .join("");
156
+ return { text, functionCalls, interactionId };
157
+ }
158
+ }
159
+ /** A text block the conversation can send back; anything else is refused. */
160
+ function textParam(content) {
161
+ if (content.type !== "text" || content.annotations?.length)
162
+ throw unsupported(`produced ${content.type} content`);
163
+ return { type: "text", text: content.text };
164
+ }
165
+ /** A function call's arguments, from the JSON fragments streamed for it. */
166
+ function parseArguments(raw) {
167
+ const parsed = JSON.parse(raw);
168
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed))
169
+ throw new TypeError(`function call arguments are not a JSON object: ${raw}`);
170
+ return parsed;
171
+ }
172
+ function unsupported(what) {
173
+ return ApplicationFailure.create({
174
+ type: "UnsupportedInteractionStep",
175
+ nonRetryable: true,
176
+ message: `the model ${what}, which an InteractionConversation cannot carry back to it`,
177
+ });
178
+ }
@@ -0,0 +1,8 @@
1
+ import type { Interactions } from "@google/genai";
2
+ /**
3
+ * The `gemini_interactions_create_streamed` activity's result: every SSE event of the streamed
4
+ * call, verbatim and in order, so the workflow iterates them as it would the SDK's stream.
5
+ */
6
+ export interface InteractionResult {
7
+ events: Interactions.InteractionSSEEvent[];
8
+ }
@@ -0,0 +1,4 @@
1
+ // What crosses the activity boundary for the Interactions API (Python's _interactions_models.py).
2
+ // The request side has no type of its own: the workflow sends the `interactions.create` params as
3
+ // they are, plus the turn's stream context.
4
+ export {};
@@ -0,0 +1,8 @@
1
+ import type { Interactions } from "@google/genai";
2
+ import type { AnyHarnessTool } from "../../harness/agent-workflow/types.ts";
3
+ /**
4
+ * A harness tool as an Interactions-API function declaration, for the `tools` of
5
+ * `interactions.create`. The tool's name is the name the model calls it by, and its model-facing
6
+ * parameters (injected ones excluded) are the declared JSON schema.
7
+ */
8
+ export declare function functionParam(tool: AnyHarnessTool): Interactions.Function;
@@ -0,0 +1,15 @@
1
+ // Workflow-side helpers for the Interactions API (Python's _interactions_workflow.py). The workflow
2
+ // drives the tool-calling loop itself, running each call with `runner.runTool`.
3
+ /**
4
+ * A harness tool as an Interactions-API function declaration, for the `tools` of
5
+ * `interactions.create`. The tool's name is the name the model calls it by, and its model-facing
6
+ * parameters (injected ones excluded) are the declared JSON schema.
7
+ */
8
+ export function functionParam(tool) {
9
+ return {
10
+ type: "function",
11
+ name: tool.name,
12
+ description: tool.description,
13
+ parameters: tool.parameters,
14
+ };
15
+ }
@@ -0,0 +1,10 @@
1
+ import type { ActivityOptions } from "@temporalio/workflow";
2
+ import type { AgentWorkflowRunner } from "../../harness/agent-workflow.ts";
3
+ import { TemporalAsyncInteractions } from "./temporal-interactions.ts";
4
+ export declare class TemporalAsyncClient {
5
+ readonly interactions: TemporalAsyncInteractions;
6
+ constructor({ activityConfig, runner, }?: {
7
+ activityConfig?: ActivityOptions;
8
+ runner?: AgentWorkflowRunner | null;
9
+ });
10
+ }
@@ -0,0 +1,9 @@
1
+ // The Gemini client a workflow holds (Python's _temporal_async_client.py): each module it exposes
2
+ // runs its network calls in activities on the worker's real client.
3
+ import { TemporalAsyncInteractions } from "./temporal-interactions.js";
4
+ export class TemporalAsyncClient {
5
+ interactions;
6
+ constructor({ activityConfig, runner = null, } = {}) {
7
+ this.interactions = new TemporalAsyncInteractions(runner, activityConfig);
8
+ }
9
+ }
@@ -0,0 +1,31 @@
1
+ import type { Interactions } from "@google/genai";
2
+ import { type ActivityOptions } from "@temporalio/workflow";
3
+ import type { TurnStreamContext } from "../../harness/agent-protocol/index.ts";
4
+ import type { AgentWorkflowRunner } from "../../harness/agent-workflow.ts";
5
+ import type { InteractionResult } from "./interactions-models.ts";
6
+ export declare const GEMINI_INTERACTIONS_CREATE_STREAMED_ACTIVITY = "gemini_interactions_create_streamed";
7
+ /** The params of a streamed `interactions.create`, passed to the worker's client as they are. */
8
+ export type InteractionCreateParamsStreaming = Interactions.InteractionCreateParams.InteractionCreateParamsStreaming;
9
+ export type GeminiInteractionsActivity = (params: InteractionCreateParamsStreaming, streamContext: TurnStreamContext | null) => Promise<InteractionResult>;
10
+ /**
11
+ * The events of one streamed call, already collected by the activity. Iterate it with
12
+ * `for await`, as the SDK's stream; each iteration walks the same events again.
13
+ */
14
+ export declare class TemporalAsyncInteractionStream implements AsyncIterable<Interactions.InteractionSSEEvent> {
15
+ #private;
16
+ constructor(events: readonly Interactions.InteractionSSEEvent[]);
17
+ [Symbol.asyncIterator](): AsyncIterator<Interactions.InteractionSSEEvent>;
18
+ }
19
+ /**
20
+ * The client's `interactions`. Only the streaming `create` is supported; it runs in an activity
21
+ * whose options default to a 3 minute start-to-close timeout.
22
+ */
23
+ export declare class TemporalAsyncInteractions {
24
+ #private;
25
+ constructor(runner: AgentWorkflowRunner | null, activityConfig?: ActivityOptions);
26
+ /**
27
+ * One streamed `interactions.create`. With a runner, the activity publishes the call's events
28
+ * (model_interaction_started, reply_delta, tool_requested, ...) to the runner's current turn.
29
+ */
30
+ create(params: InteractionCreateParamsStreaming): Promise<TemporalAsyncInteractionStream>;
31
+ }
@@ -0,0 +1,43 @@
1
+ // The workflow-side `interactions` module (Python's _temporal_interactions.py): `create` has the
2
+ // SDK's shape but runs the call in the `gemini_interactions_create_streamed` activity, which holds
3
+ // the worker's real client and publishes the call's turn events as it streams.
4
+ import { proxyActivities } from "@temporalio/workflow";
5
+ export const GEMINI_INTERACTIONS_CREATE_STREAMED_ACTIVITY = "gemini_interactions_create_streamed";
6
+ /**
7
+ * The events of one streamed call, already collected by the activity. Iterate it with
8
+ * `for await`, as the SDK's stream; each iteration walks the same events again.
9
+ */
10
+ export class TemporalAsyncInteractionStream {
11
+ #events;
12
+ constructor(events) {
13
+ this.#events = events;
14
+ }
15
+ async *[Symbol.asyncIterator]() {
16
+ yield* this.#events;
17
+ }
18
+ }
19
+ /**
20
+ * The client's `interactions`. Only the streaming `create` is supported; it runs in an activity
21
+ * whose options default to a 3 minute start-to-close timeout.
22
+ */
23
+ export class TemporalAsyncInteractions {
24
+ #runner;
25
+ #activityConfig;
26
+ constructor(runner, activityConfig) {
27
+ this.#runner = runner;
28
+ this.#activityConfig = activityConfig ?? { startToCloseTimeout: "3 minutes" };
29
+ }
30
+ /**
31
+ * One streamed `interactions.create`. With a runner, the activity publishes the call's events
32
+ * (model_interaction_started, reply_delta, tool_requested, ...) to the runner's current turn.
33
+ */
34
+ async create(params) {
35
+ if (params.stream !== true)
36
+ throw new TypeError("TemporalAsyncInteractions only supports stream: true; pass stream: true or extend the " +
37
+ "shim for the non-streaming path.");
38
+ const streamContext = this.#runner === null ? null : this.#runner.currentStreamContext;
39
+ const activities = proxyActivities({ summary: "interactions.create", ...this.#activityConfig });
40
+ const result = await activities[GEMINI_INTERACTIONS_CREATE_STREAMED_ACTIVITY](params, streamContext);
41
+ return new TemporalAsyncInteractionStream(result.events);
42
+ }
43
+ }
@@ -0,0 +1,16 @@
1
+ import type { ActivityOptions } from "@temporalio/workflow";
2
+ import type { AgentWorkflowRunner } from "../../harness/agent-workflow.ts";
3
+ import { TemporalAsyncClient } from "./temporal-async-client.ts";
4
+ /**
5
+ * A Gemini client for workflow code whose API calls run as activities on the worker's
6
+ * `GoogleGenAIPlugin` client, so credentials never enter the workflow or its history.
7
+ *
8
+ * `activityConfig` replaces the activity options (default: a 3 minute start-to-close timeout).
9
+ * With `runner`, a streamed call publishes its events (model_interaction_started, reply_delta,
10
+ * tool_requested, model_interaction_ended, ...) to the runner's current turn from the activity;
11
+ * without it, streaming still works but nothing reaches the turn's stream.
12
+ */
13
+ export declare function googleGenaiClient({ activityConfig, runner, }?: {
14
+ activityConfig?: ActivityOptions;
15
+ runner?: AgentWorkflowRunner | null;
16
+ }): TemporalAsyncClient;
@@ -0,0 +1,14 @@
1
+ // Workflow utilities for the Gemini integration (Python's google_genai_plugin/workflow.py).
2
+ import { TemporalAsyncClient } from "./temporal-async-client.js";
3
+ /**
4
+ * A Gemini client for workflow code whose API calls run as activities on the worker's
5
+ * `GoogleGenAIPlugin` client, so credentials never enter the workflow or its history.
6
+ *
7
+ * `activityConfig` replaces the activity options (default: a 3 minute start-to-close timeout).
8
+ * With `runner`, a streamed call publishes its events (model_interaction_started, reply_delta,
9
+ * tool_requested, model_interaction_ended, ...) to the runner's current turn from the activity;
10
+ * without it, streaming still works but nothing reaches the turn's stream.
11
+ */
12
+ export function googleGenaiClient({ activityConfig, runner = null, } = {}) {
13
+ return new TemporalAsyncClient({ ...(activityConfig ? { activityConfig } : {}), runner });
14
+ }
@@ -0,0 +1,38 @@
1
+ import { type ActivityOptions } from "@temporalio/workflow";
2
+ import { type AutoApprovalContext, type AutoApprovalDecision, type AutoModeEvaluator } from "../../harness/agent-protocol/index.ts";
3
+ export declare const JEV_TOOL_APPROVAL_ACTIVITY = "jev_tool_approval";
4
+ export declare const DEFAULT_JEV_MODEL = "jev-latest";
5
+ export interface JevApprovalRequest {
6
+ state: Record<string, unknown>;
7
+ questions: Record<string, Record<string, unknown>>;
8
+ model: string | null;
9
+ }
10
+ export interface JevApprovalAnswer {
11
+ model: string;
12
+ request_id: string;
13
+ verdict: string;
14
+ verdict_confidence: number;
15
+ verdict_probabilities: Record<string, number>;
16
+ irreversible: number;
17
+ input_tokens: number | null;
18
+ output_tokens: number | null;
19
+ }
20
+ export type ExtraState = Record<string, unknown> | ((ctx: AutoApprovalContext) => Record<string, unknown>);
21
+ /** The jev_tool_approval activity's options; `jevEvaluator({activityOptions})` merges over them. */
22
+ export declare const DEFAULT_ACTIVITY_OPTIONS: ActivityOptions;
23
+ export declare function jevEvaluator({ model, extraState, activityOptions, }?: {
24
+ model?: string | null;
25
+ extraState?: ExtraState;
26
+ activityOptions?: ActivityOptions;
27
+ }): AutoModeEvaluator;
28
+ export declare function buildRequest(ctx: AutoApprovalContext, { model, extraState, }?: {
29
+ model?: string | null;
30
+ extraState?: Record<string, unknown> | undefined;
31
+ }): JevApprovalRequest;
32
+ /** Turn Jev's answer into a decision under the operator's thresholds. Escalates when unsure. */
33
+ export declare function decide(answer: JevApprovalAnswer, opts: {
34
+ minConfidence: number;
35
+ escalateIfIrreversibleAbove: number | null;
36
+ criteriaSetName?: string;
37
+ criteriaVersion?: number;
38
+ }): AutoApprovalDecision;
@@ -0,0 +1,155 @@
1
+ // The builtin auto mode evaluator, backed by TypeSafe's Jev. Workflow-safe: it builds plain JSON
2
+ // and dispatches the model call as the `jev_tool_approval` activity by name (see worker.ts).
3
+ import { proxyActivities } from "@temporalio/workflow";
4
+ import { ApprovalVerdict, } from "../../harness/agent-protocol/index.js";
5
+ export const JEV_TOOL_APPROVAL_ACTIVITY = "jev_tool_approval";
6
+ export const DEFAULT_JEV_MODEL = "jev-latest";
7
+ /** The jev_tool_approval activity's options; `jevEvaluator({activityOptions})` merges over them. */
8
+ export const DEFAULT_ACTIVITY_OPTIONS = {
9
+ startToCloseTimeout: "30 seconds",
10
+ retry: { maximumAttempts: 3 },
11
+ summary: "jev_approval",
12
+ };
13
+ export function jevEvaluator({ model = DEFAULT_JEV_MODEL, extraState, activityOptions, } = {}) {
14
+ const options = { ...DEFAULT_ACTIVITY_OPTIONS, ...activityOptions };
15
+ const approve = async (ctx) => {
16
+ const extra = typeof extraState === "function" ? extraState(ctx) : extraState;
17
+ const { jev_tool_approval } = proxyActivities(options);
18
+ const answer = await jev_tool_approval(buildRequest(ctx, { model, extraState: extra }));
19
+ const [minConfidence, irreversibleCeiling] = ctx.thresholds;
20
+ return decide(answer, {
21
+ minConfidence,
22
+ escalateIfIrreversibleAbove: irreversibleCeiling,
23
+ criteriaSetName: ctx.criteria_set_name,
24
+ criteriaVersion: ctx.criteria_version,
25
+ });
26
+ };
27
+ approve.evaluatorLabel = "jev_evaluator";
28
+ return approve;
29
+ }
30
+ function rules(set, ctx) {
31
+ const out = {};
32
+ if (set.effect)
33
+ out.what_calls_to_this_tool_do = set.effect;
34
+ if (set.approve_when.length)
35
+ out.approve_when = [...set.approve_when];
36
+ if (set.deny_when.length)
37
+ out.deny_when = [...set.deny_when];
38
+ if (set.escalate_when.length)
39
+ out.escalate_when = [...set.escalate_when];
40
+ if (Object.keys(ctx.criteria.context).length)
41
+ out.operating_context = { ...ctx.criteria.context };
42
+ return out;
43
+ }
44
+ export function buildRequest(ctx, { model = DEFAULT_JEV_MODEL, extraState, } = {}) {
45
+ const state = {
46
+ situation: "An AI agent wants to run a tool. You are the approval gate that decides whether the call may run unattended, must be " +
47
+ "refused, or has to be shown to a person first. The agent chose this tool and these arguments itself.",
48
+ approval_criteria: rules(ctx.criteria_set, ctx),
49
+ tool: {
50
+ name: ctx.tool_name,
51
+ description: ctx.tool_description,
52
+ declared_inherently_safe: ctx.inherently_safe,
53
+ },
54
+ call_arguments: ctx.tool_input,
55
+ };
56
+ if (extraState && Object.keys(extraState).length)
57
+ state.agent_state = { ...extraState };
58
+ return {
59
+ state,
60
+ model,
61
+ questions: {
62
+ verdict: {
63
+ type: "choice",
64
+ instructions: {
65
+ question: "Under the operator's rules in `approval_criteria`, and those rules only, what should happen to the call described " +
66
+ "by `tool` and `call_arguments`?",
67
+ priorities: [
68
+ "Judge the call as it would actually run: `tool.description` says what the tool does, and `call_arguments` are the exact arguments it would run with.",
69
+ "`approval_criteria` is the operator's decision and the only authority here. Do not substitute your own sense of what is risky for what it says — neither to approve something it forbids nor to refuse something it allows.",
70
+ "`approval_criteria.deny_when` and `.escalate_when` are binding. `.approve_when` describes what a routine call looks like; it permits nothing that a deny or escalate rule also matches, so when rules conflict the stricter one wins.",
71
+ "An absent key in `approval_criteria` means the operator said nothing about it. Silence is never permission.",
72
+ "`tool.declared_inherently_safe` is the tool author's own claim that the tool is never unsafe under any input. Treat it as a hint; it never overrides `approval_criteria`.",
73
+ "If the rules do not settle this call, choose `escalate`. A person is available and asking costs almost nothing; guessing does not.",
74
+ ],
75
+ },
76
+ criteria: {
77
+ approve: {
78
+ what: "Run the call now, unattended, with no human review.",
79
+ when: "`approval_criteria` permits calls of this kind, and nothing in `call_arguments` crosses a line the criteria draw.",
80
+ not_for: "A call the criteria simply do not address — silence is not permission; that is `escalate`.",
81
+ },
82
+ deny: {
83
+ what: "Refuse the call. It never runs, and the agent is told it was refused so it can try a different approach.",
84
+ when: "`approval_criteria` rule out calls of this kind, or `call_arguments` show this particular call doing something the criteria forbid.",
85
+ not_for: "A call that merely looks consequential but that the criteria do not forbid; that is `escalate`.",
86
+ },
87
+ escalate: {
88
+ what: "Hold the call and put it in front of a person, who approves or denies it themselves.",
89
+ when: "`approval_criteria` do not settle this call: they are silent on this kind of call, they are ambiguous, or `call_arguments` are not specific enough to tell which side of the criteria the call falls on.",
90
+ not_for: "A call the criteria plainly permit or plainly forbid — deciding those is the point of having them.",
91
+ },
92
+ },
93
+ },
94
+ irreversible: {
95
+ type: "noul",
96
+ instructions: "Independently of whether the call should be allowed: if this call runs and turns out to have been a mistake, is its effect one that cannot be taken back?",
97
+ criteria: {
98
+ true: "Running it causes something that cannot be undone from inside this system afterwards — data or a resource destroyed, money moved, a message or request sent to someone outside it.",
99
+ false: "It only reads or computes, or whatever it changes can be changed back by a later call.",
100
+ },
101
+ },
102
+ },
103
+ };
104
+ }
105
+ const pct = (x) => `${Math.round(x * 100)}%`;
106
+ // Jev answers in the wire vocabulary its questions offer.
107
+ const VERDICTS = {
108
+ approve: "approved",
109
+ deny: "denied",
110
+ escalate: "user-approval",
111
+ };
112
+ /** Turn Jev's answer into a decision under the operator's thresholds. Escalates when unsure. */
113
+ export function decide(answer, opts) {
114
+ const verdict = VERDICTS[answer.verdict];
115
+ const stats = `jev/${answer.model}: verdict=${answer.verdict} (confidence ${pct(answer.verdict_confidence)}), irreversible ${pct(answer.irreversible)}`;
116
+ const details = {
117
+ model: answer.model,
118
+ request_id: answer.request_id,
119
+ verdict: answer.verdict,
120
+ confidence: answer.verdict_confidence,
121
+ probabilities: { ...answer.verdict_probabilities },
122
+ irreversible: answer.irreversible,
123
+ usage: { input_tokens: answer.input_tokens, output_tokens: answer.output_tokens },
124
+ thresholds: {
125
+ min_confidence: opts.minConfidence,
126
+ escalate_if_irreversible_above: opts.escalateIfIrreversibleAbove,
127
+ },
128
+ criteria_set: opts.criteriaSetName ?? "",
129
+ criteria_version: opts.criteriaVersion ?? 0,
130
+ };
131
+ const escalate = (why) => ({
132
+ verdict: ApprovalVerdict.USER_APPROVAL,
133
+ reason: `${stats} — ${why}`,
134
+ details,
135
+ });
136
+ if (answer.verdict_confidence < opts.minConfidence) {
137
+ return escalate(`below the ${pct(opts.minConfidence)} confidence needed to decide automatically; escalated to a human.`);
138
+ }
139
+ if (verdict === undefined)
140
+ return escalate("unrecognized verdict; escalated to a human.");
141
+ if (verdict === ApprovalVerdict.USER_APPROVAL)
142
+ return escalate("the approval criteria do not settle this call.");
143
+ if (verdict === ApprovalVerdict.DENIED) {
144
+ return {
145
+ verdict: ApprovalVerdict.DENIED,
146
+ reason: `${stats} — the approval criteria forbid this call.`,
147
+ details,
148
+ };
149
+ }
150
+ if (opts.escalateIfIrreversibleAbove !== null &&
151
+ answer.irreversible > opts.escalateIfIrreversibleAbove) {
152
+ return escalate(`approved on the criteria, but the effect looks irreversible (over the ${pct(opts.escalateIfIrreversibleAbove)} ceiling); escalated to a human.`);
153
+ }
154
+ return { verdict: ApprovalVerdict.APPROVED, reason: `${stats} — auto-approved.`, details };
155
+ }