@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,824 @@
|
|
|
1
|
+
// AgentWorkflowRunner: the in-workflow engine every agent runs on. It owns the turn loop, message
|
|
2
|
+
// admission (MidTurn), the event stream, the tool-approval gate (policy -> auto mode -> human),
|
|
3
|
+
// callback tools, subagents, and observable state. `defineAgent` builds one per workflow run;
|
|
4
|
+
// authors reach it through the `runner` handed to their `create` function.
|
|
5
|
+
//
|
|
6
|
+
// Ambient context (Python's contextvars) is AsyncLocalStorage: the participant's message id is
|
|
7
|
+
// set once per handler task, and the in-flight tool call (runner, tool id, injections) is set by
|
|
8
|
+
// `runTool` for exactly one invocation. Both are read only inside this package.
|
|
9
|
+
import { ApplicationFailure, AsyncLocalStorage, CancellationScope, ParentClosePolicy, allHandlersFinished, condition, defineQuery, defineSignal, defineUpdate, getExternalWorkflowHandle, isCancellation, log, proxyActivities, setHandler, startChild, uuid4, ActivityFailure, } from "@temporalio/workflow";
|
|
10
|
+
import { WorkflowStream } from "@temporalio/workflow-streams/workflow";
|
|
11
|
+
import { AGENT_ID_LENGTH, AGENT_INTERFACE_QUERY, AGENT_STATUS_QUERY, APPROVAL_SHORT_ID_LENGTH, AgentMessageSchema, AutoApprovalCriteria, ApprovalVerdict, WIRE_VERDICT, CLOSE_SIGNAL, CallbackResultSchema, DEFAULT_SUBAGENT_HEARTBEAT_TIMEOUT, DEFAULT_SUBAGENT_START_TO_CLOSE_TIMEOUT, MessageDisposition, MidTurn, PROVIDE_CALLBACK_RESULT_UPDATE, SEND_AGENT_MESSAGE_UPDATE, TOOL_APPROVAL_UPDATE, TURN_EVENTS_TOPIC, ToolApprovalDecisionSchema, ToolApprovalPolicy, toJsonSchema, validate, } from "./agent-protocol/index.js";
|
|
12
|
+
import { StateRef } from "./state/index.js";
|
|
13
|
+
import { CallbackToolError, ToolApprovalDenied, assertAutoModeHasEvaluator, assertPortableJson, errorMessage, errorText, isOwnCancellation, } from "./agent-workflow/errors.js";
|
|
14
|
+
export { CallbackToolError, ToolApprovalDenied, errorText, };
|
|
15
|
+
/**
|
|
16
|
+
* The child's accepted turn number, which the run_subagent_turn activity threads through the
|
|
17
|
+
* failure's details; its absence is a protocol violation, not something to guess around.
|
|
18
|
+
*/
|
|
19
|
+
function acceptedTurnFromError(e) {
|
|
20
|
+
for (const detail of e.details ?? []) {
|
|
21
|
+
if (detail !== null && typeof detail === "object" && "subagent_turn" in detail)
|
|
22
|
+
return Number(detail.subagent_turn);
|
|
23
|
+
}
|
|
24
|
+
throw ApplicationFailure.create({
|
|
25
|
+
type: "SubagentProtocolError",
|
|
26
|
+
nonRetryable: true,
|
|
27
|
+
message: `${e.type} error from run_subagent_turn carried no 'subagent_turn' detail`,
|
|
28
|
+
cause: e,
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
export const messageIdStore = new AsyncLocalStorage();
|
|
32
|
+
export const toolCallStore = new AsyncLocalStorage();
|
|
33
|
+
/** The in-flight tool call, or throw: tools must be invoked through `runner.runTool`. */
|
|
34
|
+
export function currentToolCall(what) {
|
|
35
|
+
const call = toolCallStore.getStore();
|
|
36
|
+
if (call === undefined)
|
|
37
|
+
throw new Error(`${what} must be invoked via runner.runTool(...) within an active turn`);
|
|
38
|
+
return call;
|
|
39
|
+
}
|
|
40
|
+
export const AMBIENT = Symbol("ambient message id");
|
|
41
|
+
function shortHex(length) {
|
|
42
|
+
return uuid4().replaceAll("-", "").slice(0, length);
|
|
43
|
+
}
|
|
44
|
+
const sendAgentMessageUpdate = defineUpdate(SEND_AGENT_MESSAGE_UPDATE);
|
|
45
|
+
const toolApprovalUpdate = defineUpdate(TOOL_APPROVAL_UPDATE);
|
|
46
|
+
const provideCallbackResultUpdate = defineUpdate(PROVIDE_CALLBACK_RESULT_UPDATE);
|
|
47
|
+
const agentStatusQuery = defineQuery(AGENT_STATUS_QUERY);
|
|
48
|
+
const agentInterfaceQuery = defineQuery(AGENT_INTERFACE_QUERY);
|
|
49
|
+
const closeSignal = defineSignal(CLOSE_SIGNAL);
|
|
50
|
+
export class AgentWorkflowRunner {
|
|
51
|
+
agentId;
|
|
52
|
+
stream;
|
|
53
|
+
#events;
|
|
54
|
+
#handlers;
|
|
55
|
+
#evaluator;
|
|
56
|
+
#closed = false;
|
|
57
|
+
#currentTurn = 0;
|
|
58
|
+
#currentTurnId = null;
|
|
59
|
+
#participants = 0;
|
|
60
|
+
#pendingTurns = [];
|
|
61
|
+
#joining = [];
|
|
62
|
+
#policy;
|
|
63
|
+
#criteria;
|
|
64
|
+
#criteriaVersion = 0;
|
|
65
|
+
#approvals = new Map();
|
|
66
|
+
#callbacks = new Map();
|
|
67
|
+
#subagents = new Map();
|
|
68
|
+
constructor(config, options) {
|
|
69
|
+
this.#handlers = options.handlers;
|
|
70
|
+
this.#evaluator = options.autoModeEvaluator;
|
|
71
|
+
this.#policy = config.approval_policy ?? options.approvalPolicyDefault;
|
|
72
|
+
this.#criteria =
|
|
73
|
+
config.auto_approval_criteria ??
|
|
74
|
+
options.autoApprovalCriteriaDefault ??
|
|
75
|
+
AutoApprovalCriteria.empty();
|
|
76
|
+
this.agentId = config.agent_id ?? shortHex(AGENT_ID_LENGTH);
|
|
77
|
+
this.stream = new WorkflowStream();
|
|
78
|
+
this.#events = this.stream.topic(TURN_EVENTS_TOPIC);
|
|
79
|
+
// Before any handler is installed: a buffered first message is dispatched by setHandler, and
|
|
80
|
+
// its message_accepted must not precede the state snapshots.
|
|
81
|
+
this.state = this.#registerStates(options.states ?? {}, options.data);
|
|
82
|
+
assertAutoModeHasEvaluator(this.#policy, this.#evaluator);
|
|
83
|
+
if (this.#evaluator !== undefined && !this.#policy.auto_mode_enabled) {
|
|
84
|
+
log.warn("An autoModeEvaluator is wired but the live ToolApprovalPolicy has auto_mode_enabled=False, so it " +
|
|
85
|
+
"will never be consulted and every gated call goes to the human gate.");
|
|
86
|
+
}
|
|
87
|
+
setHandler(sendAgentMessageUpdate, (m) => this.#handleSendAgentMessage(m), {
|
|
88
|
+
validator: (m) => this.#validateSendAgentMessage(m),
|
|
89
|
+
});
|
|
90
|
+
setHandler(toolApprovalUpdate, (d) => this.#handleToolApproval(d), {
|
|
91
|
+
validator: (d) => this.#validateToolApproval(d),
|
|
92
|
+
});
|
|
93
|
+
setHandler(provideCallbackResultUpdate, (r) => this.#handleProvideCallbackResult(r), {
|
|
94
|
+
validator: (r) => this.#validateProvideCallbackResult(r),
|
|
95
|
+
});
|
|
96
|
+
setHandler(agentStatusQuery, () => this.status);
|
|
97
|
+
setHandler(agentInterfaceQuery, () => this.#agentInterface());
|
|
98
|
+
setHandler(closeSignal, () => {
|
|
99
|
+
this.#closed = true;
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
// -- public surface -------------------------------------------------------------------------
|
|
103
|
+
get closed() {
|
|
104
|
+
return this.#closed;
|
|
105
|
+
}
|
|
106
|
+
/** The live turn context, stamped with the ambient participant's message id; null when idle. */
|
|
107
|
+
get currentStreamContext() {
|
|
108
|
+
if (this.#currentTurnId === null)
|
|
109
|
+
return null;
|
|
110
|
+
return {
|
|
111
|
+
turn_id: this.#currentTurnId,
|
|
112
|
+
turn_number: this.#currentTurn,
|
|
113
|
+
message_id: messageIdStore.getStore() ?? null,
|
|
114
|
+
agent_id: this.agentId,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
get approvalPolicy() {
|
|
118
|
+
return this.#policy;
|
|
119
|
+
}
|
|
120
|
+
/** Install a new policy; pending calls it now allow-lists are approved in the same step. */
|
|
121
|
+
setApprovalPolicy(policy) {
|
|
122
|
+
assertAutoModeHasEvaluator(policy, this.#evaluator);
|
|
123
|
+
this.#policy = policy;
|
|
124
|
+
for (const entry of this.#approvals.values()) {
|
|
125
|
+
if (entry.status !== "pending")
|
|
126
|
+
continue;
|
|
127
|
+
if (ToolApprovalPolicy.autoApproves(policy, entry.tool_name, {
|
|
128
|
+
inherentlySafe: entry.inherently_safe,
|
|
129
|
+
})) {
|
|
130
|
+
this.#resolveAndPublish(entry.tool_id, true, "auto-approved by updated policy");
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
get autoApprovalCriteria() {
|
|
135
|
+
return this.#criteria;
|
|
136
|
+
}
|
|
137
|
+
setAutoApprovalCriteria(criteria) {
|
|
138
|
+
this.#criteria = criteria;
|
|
139
|
+
this.#criteriaVersion += 1;
|
|
140
|
+
}
|
|
141
|
+
assignToolCriteria(toolName, criteriaSet) {
|
|
142
|
+
this.setAutoApprovalCriteria(AutoApprovalCriteria.withToolAssigned(this.#criteria, toolName, criteriaSet));
|
|
143
|
+
}
|
|
144
|
+
get status() {
|
|
145
|
+
return {
|
|
146
|
+
agent_id: this.agentId,
|
|
147
|
+
current_turn: this.#currentTurn,
|
|
148
|
+
turn_active: this.#participants > 0,
|
|
149
|
+
turn_participants: this.#participants,
|
|
150
|
+
pending_turns: this.#pendingTurns.map((a, i) => ({
|
|
151
|
+
turn_number: this.#currentTurn + i + 1,
|
|
152
|
+
turn_id: a.turn_id,
|
|
153
|
+
message_id: a.message_id,
|
|
154
|
+
message: JSON.stringify({ type: a.message.type, payload: a.message.payload }),
|
|
155
|
+
})),
|
|
156
|
+
pending_approvals: [...this.#approvals.values()]
|
|
157
|
+
.filter((e) => e.status === "pending")
|
|
158
|
+
.map(({ tool_id, tool_name, tool_input, turn_number, short_id }) => ({
|
|
159
|
+
tool_id,
|
|
160
|
+
tool_name,
|
|
161
|
+
tool_input,
|
|
162
|
+
turn_number,
|
|
163
|
+
short_id,
|
|
164
|
+
})),
|
|
165
|
+
pending_callbacks: [...this.#callbacks.values()]
|
|
166
|
+
.filter((e) => e.status === "pending")
|
|
167
|
+
.map(({ tool_id, tool_name, tool_input, output_schema, turn_number }) => ({
|
|
168
|
+
tool_id,
|
|
169
|
+
tool_name,
|
|
170
|
+
tool_input,
|
|
171
|
+
output_schema,
|
|
172
|
+
turn_number,
|
|
173
|
+
})),
|
|
174
|
+
subagents: [...this.#subagents.values()].map(({ handle, agent_key, workflow_id }) => ({
|
|
175
|
+
subagent_id: handle,
|
|
176
|
+
agent_key,
|
|
177
|
+
workflow_id,
|
|
178
|
+
})),
|
|
179
|
+
approval_policy: this.#policy,
|
|
180
|
+
has_auto_approval_evaluator: this.#evaluator !== undefined,
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
/** Publish a payload on the current turn, attributed to the ambient message. */
|
|
184
|
+
publish(event) {
|
|
185
|
+
const ctx = this.currentStreamContext;
|
|
186
|
+
if (ctx === null)
|
|
187
|
+
throw new Error("publish() called with no active turn");
|
|
188
|
+
this.pub(ctx.turn_id, ctx.turn_number, event);
|
|
189
|
+
}
|
|
190
|
+
runTool(callId, tool, input, { injections = {} } = {}) {
|
|
191
|
+
return toolCallStore.run({ runner: this, toolId: callId, injections }, () => tool.invoke(input));
|
|
192
|
+
}
|
|
193
|
+
/** Declared state: each snapshot was published at construction, then every committed mutation. */
|
|
194
|
+
state;
|
|
195
|
+
#registerStates(decls, data) {
|
|
196
|
+
const refs = {};
|
|
197
|
+
for (const [stateId, decl] of Object.entries(decls)) {
|
|
198
|
+
const ref = new StateRef(stateId, decl, (e) => this.#publishStateEvent(e), data);
|
|
199
|
+
refs[stateId] = ref;
|
|
200
|
+
this.#publishStateEvent(ref.snapshot());
|
|
201
|
+
}
|
|
202
|
+
return refs;
|
|
203
|
+
}
|
|
204
|
+
// -- the turn loop --------------------------------------------------------------------------
|
|
205
|
+
async run(impls) {
|
|
206
|
+
try {
|
|
207
|
+
while (!this.#closed) {
|
|
208
|
+
await condition(() => this.#canOpenTurn ||
|
|
209
|
+
this.#joining.length > 0 ||
|
|
210
|
+
this.#closed);
|
|
211
|
+
if (this.#closed)
|
|
212
|
+
break;
|
|
213
|
+
for (let joined = this.#joining.shift(); joined; joined = this.#joining.shift()) {
|
|
214
|
+
void this.#runParticipant(impls, joined, true);
|
|
215
|
+
}
|
|
216
|
+
if (this.#canOpenTurn) {
|
|
217
|
+
const admitted = this.#pendingTurns.shift();
|
|
218
|
+
this.#currentTurn += 1;
|
|
219
|
+
this.#currentTurnId = admitted.turn_id;
|
|
220
|
+
this.#participants = 1;
|
|
221
|
+
this.pub(admitted.turn_id, admitted.turn_number, { type: "turn_started" }, null);
|
|
222
|
+
void this.#runParticipant(impls, admitted, false);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
await condition(() => this.#participants === 0);
|
|
226
|
+
}
|
|
227
|
+
catch (e) {
|
|
228
|
+
if (isCancellation(e)) {
|
|
229
|
+
await CancellationScope.nonCancellable(async () => {
|
|
230
|
+
this.stream.detachPollers();
|
|
231
|
+
await condition(allHandlersFinished);
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
throw e;
|
|
235
|
+
}
|
|
236
|
+
// Parked polls are handed the last events (turn_end included) before the run completes.
|
|
237
|
+
this.stream.detachPollers();
|
|
238
|
+
await condition(allHandlersFinished);
|
|
239
|
+
}
|
|
240
|
+
// -- the stream -----------------------------------------------------------------------------
|
|
241
|
+
// WorkflowStream keeps its log private; the runner reads its head offset from it. Revisit if the
|
|
242
|
+
// SDK exposes one.
|
|
243
|
+
get #stream() {
|
|
244
|
+
return this.stream;
|
|
245
|
+
}
|
|
246
|
+
get #canOpenTurn() {
|
|
247
|
+
return this.#pendingTurns.length > 0 && this.#participants === 0;
|
|
248
|
+
}
|
|
249
|
+
get #hasPendingWork() {
|
|
250
|
+
return this.#participants > 0 || this.#pendingTurns.length > 0;
|
|
251
|
+
}
|
|
252
|
+
#runParticipant(impls, admitted, joined) {
|
|
253
|
+
return messageIdStore.run(admitted.message_id, async () => {
|
|
254
|
+
const { turn_id, turn_number } = admitted;
|
|
255
|
+
this.pub(turn_id, turn_number, { type: "message_handler_start" });
|
|
256
|
+
try {
|
|
257
|
+
const decl = this.#handlers[admitted.message.type];
|
|
258
|
+
const input = decl.input.parse(admitted.message.payload);
|
|
259
|
+
const ctx = {
|
|
260
|
+
joined_turn: joined,
|
|
261
|
+
turn_id,
|
|
262
|
+
turn_number,
|
|
263
|
+
message_id: admitted.message_id,
|
|
264
|
+
};
|
|
265
|
+
const result = await impls[admitted.message.type](input, ctx);
|
|
266
|
+
const output = decl.output.safeParse(result);
|
|
267
|
+
if (!output.success) {
|
|
268
|
+
throw ApplicationFailure.create({
|
|
269
|
+
type: "BadHandlerReturn",
|
|
270
|
+
nonRetryable: true,
|
|
271
|
+
message: `handler '${admitted.message.type}' returned a value that does not match its output schema: ${output.error.message}`,
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
assertPortableJson(output.data, `Output of handler '${admitted.message.type}'`);
|
|
275
|
+
this.pub(turn_id, turn_number, {
|
|
276
|
+
type: "message_handler_end",
|
|
277
|
+
output: output.data,
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
catch (e) {
|
|
281
|
+
// Only the workflow's own cancellation abandons the turn silently; a cancellation the
|
|
282
|
+
// handler observed (its own timeout, a cancelled activity) is a failure like any other.
|
|
283
|
+
if (isOwnCancellation(e)) {
|
|
284
|
+
this.#leaveTurn();
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
this.pub(turn_id, turn_number, { type: "message_handler_error", message: errorMessage(e) });
|
|
288
|
+
}
|
|
289
|
+
if (this.#leaveTurn()) {
|
|
290
|
+
this.pub(turn_id, turn_number, { type: "turn_end" }, null);
|
|
291
|
+
}
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
#leaveTurn() {
|
|
295
|
+
this.#participants -= 1;
|
|
296
|
+
if (this.#participants > 0)
|
|
297
|
+
return false;
|
|
298
|
+
this.#participants = 0;
|
|
299
|
+
this.#currentTurnId = null;
|
|
300
|
+
return true;
|
|
301
|
+
}
|
|
302
|
+
// -- send_agent_message ---------------------------------------------------------------------
|
|
303
|
+
#validateSendAgentMessage(raw) {
|
|
304
|
+
const message = AgentMessageSchema.parse(raw);
|
|
305
|
+
assertPortableJson(message.payload, `Payload for function '${message.type}'`);
|
|
306
|
+
const handler = Object.hasOwn(this.#handlers, message.type)
|
|
307
|
+
? this.#handlers[message.type]
|
|
308
|
+
: undefined;
|
|
309
|
+
const known = Object.keys(this.#handlers).sort();
|
|
310
|
+
if (handler === undefined) {
|
|
311
|
+
throw ApplicationFailure.create({
|
|
312
|
+
type: "UnknownFunction",
|
|
313
|
+
nonRetryable: true,
|
|
314
|
+
message: `Unknown function '${message.type}'. Known functions: [${known.map((k) => `'${k}'`).join(", ")}].`,
|
|
315
|
+
details: [{ name: message.type, known }],
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
const parsed = handler.input.safeParse(message.payload);
|
|
319
|
+
if (!parsed.success) {
|
|
320
|
+
throw ApplicationFailure.create({
|
|
321
|
+
type: "MalformedMessage",
|
|
322
|
+
nonRetryable: true,
|
|
323
|
+
message: `Payload for function '${message.type}' does not match its input model. Validation error: ${parsed.error.message}`,
|
|
324
|
+
details: [{ function: message.type, error: parsed.error.message }],
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
if (handler.midTurn === MidTurn.REJECT && this.#hasPendingWork) {
|
|
328
|
+
throw ApplicationFailure.create({
|
|
329
|
+
type: "MidTurnRejected",
|
|
330
|
+
nonRetryable: true,
|
|
331
|
+
message: `Function '${message.type}' declares mid_turn='reject' and the agent is busy.`,
|
|
332
|
+
details: [
|
|
333
|
+
{
|
|
334
|
+
function: message.type,
|
|
335
|
+
current_turn: this.#currentTurn,
|
|
336
|
+
turn_participants: this.#participants,
|
|
337
|
+
},
|
|
338
|
+
],
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
#handleSendAgentMessage(raw) {
|
|
343
|
+
const message = AgentMessageSchema.parse(raw);
|
|
344
|
+
const acceptedOffset = this.#streamHead();
|
|
345
|
+
const handler = this.#handlers[message.type];
|
|
346
|
+
const messageId = uuid4();
|
|
347
|
+
let turnId;
|
|
348
|
+
let turnNumber;
|
|
349
|
+
let disposition;
|
|
350
|
+
if (handler.midTurn === MidTurn.ACCEPT &&
|
|
351
|
+
this.#participants > 0 &&
|
|
352
|
+
this.#currentTurnId !== null) {
|
|
353
|
+
this.#participants += 1;
|
|
354
|
+
turnId = this.#currentTurnId;
|
|
355
|
+
turnNumber = this.#currentTurn;
|
|
356
|
+
disposition = MessageDisposition.JOINED;
|
|
357
|
+
this.#joining.push({
|
|
358
|
+
message,
|
|
359
|
+
turn_id: turnId,
|
|
360
|
+
turn_number: turnNumber,
|
|
361
|
+
message_id: messageId,
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
else {
|
|
365
|
+
turnId = uuid4();
|
|
366
|
+
turnNumber = this.#currentTurn + this.#pendingTurns.length + 1;
|
|
367
|
+
disposition = this.#hasPendingWork ? MessageDisposition.QUEUED : MessageDisposition.OPENED;
|
|
368
|
+
this.#pendingTurns.push({
|
|
369
|
+
message,
|
|
370
|
+
turn_id: turnId,
|
|
371
|
+
turn_number: turnNumber,
|
|
372
|
+
message_id: messageId,
|
|
373
|
+
accepted_offset: acceptedOffset,
|
|
374
|
+
});
|
|
375
|
+
}
|
|
376
|
+
this.pub(turnId, turnNumber, { type: "message_accepted", handler: message.type, payload: message.payload, disposition }, messageId);
|
|
377
|
+
return {
|
|
378
|
+
turn_number: turnNumber,
|
|
379
|
+
turn_id: turnId,
|
|
380
|
+
message_id: messageId,
|
|
381
|
+
disposition,
|
|
382
|
+
accepted_offset: acceptedOffset,
|
|
383
|
+
};
|
|
384
|
+
}
|
|
385
|
+
// The stream's head offset at this instant (Python reads it through its private `_on_offset()`).
|
|
386
|
+
#streamHead() {
|
|
387
|
+
return this.#stream.baseOffset + this.#stream.log.length;
|
|
388
|
+
}
|
|
389
|
+
// -- the approval gate ----------------------------------------------------------------------
|
|
390
|
+
/** @internal Enforce the approval policy for the in-flight tool call. Throws ToolApprovalDenied. */
|
|
391
|
+
async applyApprovalPolicy(toolId, toolName, toolInput, opts) {
|
|
392
|
+
if (ToolApprovalPolicy.autoApproves(this.#policy, toolName, {
|
|
393
|
+
inherentlySafe: opts.inherentlySafe,
|
|
394
|
+
}))
|
|
395
|
+
return;
|
|
396
|
+
const ctx = this.currentStreamContext;
|
|
397
|
+
if (ctx === null)
|
|
398
|
+
throw new Error(`gated tool '${toolName}' has no active turn to publish its approval against`);
|
|
399
|
+
const used = new Set([...this.#approvals.values()].map((e) => e.short_id));
|
|
400
|
+
let shortId = shortHex(APPROVAL_SHORT_ID_LENGTH);
|
|
401
|
+
while (used.has(shortId))
|
|
402
|
+
shortId = shortHex(APPROVAL_SHORT_ID_LENGTH);
|
|
403
|
+
this.#approvals.set(toolId, {
|
|
404
|
+
tool_id: toolId,
|
|
405
|
+
tool_name: toolName,
|
|
406
|
+
tool_input: toolInput,
|
|
407
|
+
turn_number: ctx.turn_number,
|
|
408
|
+
turn_id: ctx.turn_id,
|
|
409
|
+
message_id: ctx.message_id,
|
|
410
|
+
short_id: shortId,
|
|
411
|
+
inherently_safe: opts.inherentlySafe,
|
|
412
|
+
status: "pending",
|
|
413
|
+
reason: null,
|
|
414
|
+
remember: false,
|
|
415
|
+
});
|
|
416
|
+
this.pub(ctx.turn_id, ctx.turn_number, {
|
|
417
|
+
type: "tool_approval_requested",
|
|
418
|
+
tool_id: toolId,
|
|
419
|
+
tool_name: toolName,
|
|
420
|
+
tool_input: toolInput,
|
|
421
|
+
});
|
|
422
|
+
const entry = this.#approvals.get(toolId);
|
|
423
|
+
const autoCtx = this.#autoModeContext(toolName, toolInput, opts);
|
|
424
|
+
const decision = autoCtx === null ? null : await this.#runAutoModeEvaluator(autoCtx, toolId, ctx);
|
|
425
|
+
if (decision?.verdict === ApprovalVerdict.APPROVED)
|
|
426
|
+
this.#resolveAndPublish(toolId, true, decision.reason ?? null);
|
|
427
|
+
else if (decision?.verdict === ApprovalVerdict.DENIED)
|
|
428
|
+
this.#resolveAndPublish(toolId, false, decision.reason ?? null);
|
|
429
|
+
await condition(() => this.#isApprovalResolved(toolId) || this.#closed);
|
|
430
|
+
// Resolutions publish at their resolution site, in causal order. Only a close that wakes a
|
|
431
|
+
// still-pending gate is finalized (as an auto-deny) here.
|
|
432
|
+
if (entry.status === "pending") {
|
|
433
|
+
entry.status = "denied";
|
|
434
|
+
entry.reason = this.#closed ? "agent closed before approval" : "approval unresolved";
|
|
435
|
+
this.#publishApprovalResolved(entry);
|
|
436
|
+
}
|
|
437
|
+
if (entry.status !== "approved")
|
|
438
|
+
throw new ToolApprovalDenied(toolName, entry.reason);
|
|
439
|
+
}
|
|
440
|
+
#isApprovalResolved(toolId) {
|
|
441
|
+
const entry = this.#approvals.get(toolId);
|
|
442
|
+
return entry !== undefined && entry.status !== "pending";
|
|
443
|
+
}
|
|
444
|
+
#autoModeContext(toolName, toolInput, opts) {
|
|
445
|
+
if (this.#evaluator === undefined)
|
|
446
|
+
return null;
|
|
447
|
+
if (!ToolApprovalPolicy.consultsAutoMode(this.#policy, toolName, {
|
|
448
|
+
inherentlySafe: opts.inherentlySafe,
|
|
449
|
+
}))
|
|
450
|
+
return null;
|
|
451
|
+
const criteria = this.#criteria;
|
|
452
|
+
const criteriaSet = AutoApprovalCriteria.forTool(criteria, toolName, opts.autoApprovalCriteria);
|
|
453
|
+
const name = AutoApprovalCriteria.setNameFor(criteria, toolName, opts.autoApprovalCriteria);
|
|
454
|
+
if (criteriaSet === null) {
|
|
455
|
+
if (name !== null) {
|
|
456
|
+
log.warn(`Auto mode skipped tool '${toolName}': the criteria set '${name}' it is assigned to is not registered ` +
|
|
457
|
+
"(or carries no rules), so the call went to the human gate.");
|
|
458
|
+
}
|
|
459
|
+
return null;
|
|
460
|
+
}
|
|
461
|
+
return {
|
|
462
|
+
tool_name: toolName,
|
|
463
|
+
tool_input: toolInput,
|
|
464
|
+
inherently_safe: opts.inherentlySafe,
|
|
465
|
+
tool_description: opts.description,
|
|
466
|
+
criteria,
|
|
467
|
+
criteria_version: this.#criteriaVersion,
|
|
468
|
+
criteria_set: criteriaSet,
|
|
469
|
+
criteria_set_name: name ?? "",
|
|
470
|
+
thresholds: AutoApprovalCriteria.thresholdsForSet(criteria, criteriaSet),
|
|
471
|
+
};
|
|
472
|
+
}
|
|
473
|
+
async #runAutoModeEvaluator(ctx, toolId, stream) {
|
|
474
|
+
const evaluatorFn = this.#evaluator;
|
|
475
|
+
const evaluationId = uuid4().replaceAll("-", "");
|
|
476
|
+
const evaluator = evaluatorFn.evaluatorLabel || evaluatorFn.name || "auto_mode_evaluator";
|
|
477
|
+
const ids = {
|
|
478
|
+
tool_id: toolId,
|
|
479
|
+
tool_name: ctx.tool_name,
|
|
480
|
+
evaluation_id: evaluationId,
|
|
481
|
+
evaluator,
|
|
482
|
+
};
|
|
483
|
+
const close = (event) => this.pub(stream.turn_id, stream.turn_number, event);
|
|
484
|
+
close({ type: "auto_approval_evaluation_started", ...ids });
|
|
485
|
+
const scope = new CancellationScope();
|
|
486
|
+
let outcome;
|
|
487
|
+
const running = scope
|
|
488
|
+
.run(() => evaluatorFn(ctx))
|
|
489
|
+
.then((ok) => (outcome = { ok }), (err) => (outcome = { err }));
|
|
490
|
+
const settled = () => this.#isApprovalResolved(toolId) || this.#closed;
|
|
491
|
+
await condition(() => outcome !== undefined || settled());
|
|
492
|
+
if (settled()) {
|
|
493
|
+
const reached = outcome !== undefined && "ok" in outcome
|
|
494
|
+
? (WIRE_VERDICT[outcome.ok?.verdict] ?? null)
|
|
495
|
+
: null;
|
|
496
|
+
scope.cancel();
|
|
497
|
+
await running;
|
|
498
|
+
close({ type: "auto_approval_evaluation_superseded", ...ids, verdict: reached });
|
|
499
|
+
return null;
|
|
500
|
+
}
|
|
501
|
+
if (outcome !== undefined &&
|
|
502
|
+
"ok" in outcome &&
|
|
503
|
+
!Object.hasOwn(WIRE_VERDICT, String(outcome.ok?.verdict))) {
|
|
504
|
+
outcome = {
|
|
505
|
+
err: new TypeError(`auto mode evaluator '${evaluator}' returned ${JSON.stringify(outcome.ok)}; it must return an AutoApprovalDecision`),
|
|
506
|
+
};
|
|
507
|
+
}
|
|
508
|
+
if (outcome !== undefined && "err" in outcome) {
|
|
509
|
+
log.error(`auto mode evaluator '${evaluator}' raised for tool '${ctx.tool_name}'; escalating to the human gate`);
|
|
510
|
+
close({ type: "auto_approval_evaluation_error", ...ids, message: errorText(outcome.err) });
|
|
511
|
+
return null;
|
|
512
|
+
}
|
|
513
|
+
const decision = outcome.ok;
|
|
514
|
+
close({
|
|
515
|
+
type: "auto_approval_evaluation_ended",
|
|
516
|
+
...ids,
|
|
517
|
+
verdict: WIRE_VERDICT[decision.verdict],
|
|
518
|
+
reason: decision.reason ?? null,
|
|
519
|
+
details: decision.details ?? {},
|
|
520
|
+
});
|
|
521
|
+
return decision;
|
|
522
|
+
}
|
|
523
|
+
#validateToolApproval(raw) {
|
|
524
|
+
const decision = ToolApprovalDecisionSchema.parse(raw);
|
|
525
|
+
if (this.#closed) {
|
|
526
|
+
// The gate denies every pending call on close; a decision racing the close must not run one.
|
|
527
|
+
throw ApplicationFailure.create({
|
|
528
|
+
type: "AgentClosed",
|
|
529
|
+
nonRetryable: true,
|
|
530
|
+
message: "the agent is closed; its pending tool calls were denied",
|
|
531
|
+
});
|
|
532
|
+
}
|
|
533
|
+
const entry = this.#approvals.get(decision.tool_id);
|
|
534
|
+
if (entry === undefined) {
|
|
535
|
+
throw ApplicationFailure.create({
|
|
536
|
+
type: "UnknownToolApproval",
|
|
537
|
+
nonRetryable: true,
|
|
538
|
+
message: `no pending tool approval for tool_id='${decision.tool_id}'`,
|
|
539
|
+
});
|
|
540
|
+
}
|
|
541
|
+
if (entry.status !== "pending") {
|
|
542
|
+
throw ApplicationFailure.create({
|
|
543
|
+
type: "ToolApprovalAlreadyResolved",
|
|
544
|
+
nonRetryable: true,
|
|
545
|
+
message: `tool approval for tool_id='${decision.tool_id}' is already ${entry.status}`,
|
|
546
|
+
});
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
#handleToolApproval(raw) {
|
|
550
|
+
const decision = ToolApprovalDecisionSchema.parse(raw);
|
|
551
|
+
const entry = this.#approvals.get(decision.tool_id);
|
|
552
|
+
this.#resolveAndPublish(decision.tool_id, decision.approved, decision.reason, decision.remember);
|
|
553
|
+
if (decision.approved && decision.remember) {
|
|
554
|
+
this.setApprovalPolicy(ToolApprovalPolicy.withToolAllowed(this.#policy, entry.tool_name));
|
|
555
|
+
}
|
|
556
|
+
return { tool_id: decision.tool_id, accepted: true };
|
|
557
|
+
}
|
|
558
|
+
#resolveAndPublish(toolId, approved, reason, remember = false) {
|
|
559
|
+
const entry = this.#approvals.get(toolId);
|
|
560
|
+
entry.status = approved ? "approved" : "denied";
|
|
561
|
+
entry.reason = reason;
|
|
562
|
+
entry.remember = remember;
|
|
563
|
+
this.#publishApprovalResolved(entry);
|
|
564
|
+
}
|
|
565
|
+
#publishApprovalResolved(entry) {
|
|
566
|
+
this.pub(entry.turn_id, entry.turn_number, {
|
|
567
|
+
type: "tool_approval_resolved",
|
|
568
|
+
tool_id: entry.tool_id,
|
|
569
|
+
tool_name: entry.tool_name,
|
|
570
|
+
approved: entry.status === "approved",
|
|
571
|
+
reason: entry.reason,
|
|
572
|
+
remember: entry.remember,
|
|
573
|
+
}, entry.message_id);
|
|
574
|
+
}
|
|
575
|
+
// -- callback tools -------------------------------------------------------------------------
|
|
576
|
+
/** @internal The generic body of a callback tool: park until a client provides the result. */
|
|
577
|
+
async awaitCallbackResult(args) {
|
|
578
|
+
const ctx = this.currentStreamContext;
|
|
579
|
+
if (ctx === null)
|
|
580
|
+
throw new Error(`callback tool '${args.toolName}' has no active turn to await a result against`);
|
|
581
|
+
const entry = {
|
|
582
|
+
tool_id: args.toolId,
|
|
583
|
+
tool_name: args.toolName,
|
|
584
|
+
tool_input: args.toolInput,
|
|
585
|
+
output_schema: args.outputSchema,
|
|
586
|
+
output: args.output,
|
|
587
|
+
turn_number: ctx.turn_number,
|
|
588
|
+
turn_id: ctx.turn_id,
|
|
589
|
+
message_id: ctx.message_id,
|
|
590
|
+
status: "pending",
|
|
591
|
+
outcome: "ok",
|
|
592
|
+
result: null,
|
|
593
|
+
error: null,
|
|
594
|
+
};
|
|
595
|
+
this.#callbacks.set(args.toolId, entry);
|
|
596
|
+
this.pub(ctx.turn_id, ctx.turn_number, {
|
|
597
|
+
type: "callback_requested",
|
|
598
|
+
tool_id: args.toolId,
|
|
599
|
+
tool_name: args.toolName,
|
|
600
|
+
tool_input: args.toolInput,
|
|
601
|
+
output_schema: args.outputSchema,
|
|
602
|
+
});
|
|
603
|
+
const done = () => entry.status === "resolved" || this.#closed;
|
|
604
|
+
const inTime = args.timeout === undefined ? (await condition(done), true) : await condition(done, args.timeout);
|
|
605
|
+
if (entry.status === "pending") {
|
|
606
|
+
entry.status = "resolved";
|
|
607
|
+
entry.outcome = "timeout";
|
|
608
|
+
entry.error = !inTime
|
|
609
|
+
? "callback timed out before a result arrived"
|
|
610
|
+
: this.#closed
|
|
611
|
+
? "agent closed before the callback result arrived"
|
|
612
|
+
: "callback unresolved";
|
|
613
|
+
this.#publishCallbackResolved(entry);
|
|
614
|
+
}
|
|
615
|
+
if (entry.outcome === "ok")
|
|
616
|
+
return entry.result;
|
|
617
|
+
throw new CallbackToolError(args.toolName, entry.error);
|
|
618
|
+
}
|
|
619
|
+
#validateProvideCallbackResult(raw) {
|
|
620
|
+
const result = CallbackResultSchema.parse(raw);
|
|
621
|
+
assertPortableJson(result.result, `Result for callback '${result.tool_id}'`);
|
|
622
|
+
const entry = this.#callbacks.get(result.tool_id);
|
|
623
|
+
if (entry === undefined) {
|
|
624
|
+
throw ApplicationFailure.create({
|
|
625
|
+
type: "UnknownCallback",
|
|
626
|
+
nonRetryable: true,
|
|
627
|
+
message: `no pending callback for tool_id='${result.tool_id}'`,
|
|
628
|
+
});
|
|
629
|
+
}
|
|
630
|
+
if (entry.status !== "pending") {
|
|
631
|
+
throw ApplicationFailure.create({
|
|
632
|
+
type: "CallbackAlreadyResolved",
|
|
633
|
+
nonRetryable: true,
|
|
634
|
+
message: `callback for tool_id='${result.tool_id}' is already ${entry.status}`,
|
|
635
|
+
});
|
|
636
|
+
}
|
|
637
|
+
if (result.error === null) {
|
|
638
|
+
const parsed = validate(entry.output, result.result);
|
|
639
|
+
if (!parsed.ok) {
|
|
640
|
+
throw ApplicationFailure.create({
|
|
641
|
+
type: "MalformedCallbackResult",
|
|
642
|
+
nonRetryable: true,
|
|
643
|
+
message: `callback result for tool_id='${result.tool_id}' does not match the tool's declared output type. Validation error: ${parsed.message}`,
|
|
644
|
+
details: [{ tool_id: result.tool_id, error: parsed.message }],
|
|
645
|
+
});
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
#handleProvideCallbackResult(raw) {
|
|
650
|
+
const result = CallbackResultSchema.parse(raw);
|
|
651
|
+
const entry = this.#callbacks.get(result.tool_id);
|
|
652
|
+
entry.status = "resolved";
|
|
653
|
+
if (result.error !== null) {
|
|
654
|
+
entry.outcome = "error";
|
|
655
|
+
entry.error = result.error;
|
|
656
|
+
}
|
|
657
|
+
else {
|
|
658
|
+
entry.outcome = "ok";
|
|
659
|
+
const parsed = validate(entry.output, result.result);
|
|
660
|
+
entry.result = parsed.ok ? parsed.value : result.result;
|
|
661
|
+
}
|
|
662
|
+
this.#publishCallbackResolved(entry);
|
|
663
|
+
return { tool_id: result.tool_id, accepted: true };
|
|
664
|
+
}
|
|
665
|
+
#publishCallbackResolved(entry) {
|
|
666
|
+
this.pub(entry.turn_id, entry.turn_number, {
|
|
667
|
+
type: "callback_resolved",
|
|
668
|
+
tool_id: entry.tool_id,
|
|
669
|
+
tool_name: entry.tool_name,
|
|
670
|
+
outcome: entry.outcome,
|
|
671
|
+
error: entry.error,
|
|
672
|
+
}, entry.message_id);
|
|
673
|
+
}
|
|
674
|
+
// -- subagents ------------------------------------------------------------------------------
|
|
675
|
+
/** Start a child agent; returns its short, tree-unique handle (also its agent_id). */
|
|
676
|
+
async startSubagent(agentKey, workflowType, taskQueue, { config = {}, data } = {}) {
|
|
677
|
+
let handle = `${this.agentId}-${shortHex(AGENT_ID_LENGTH)}`;
|
|
678
|
+
while (this.#subagents.has(handle))
|
|
679
|
+
handle = `${this.agentId}-${shortHex(AGENT_ID_LENGTH)}`;
|
|
680
|
+
const workflowId = `${agentKey}-subagent-${uuid4()}`;
|
|
681
|
+
const childConfig = { ...config, agent_id: handle };
|
|
682
|
+
await startChild(workflowType, {
|
|
683
|
+
args: data === undefined ? [childConfig] : [childConfig, data],
|
|
684
|
+
workflowId,
|
|
685
|
+
taskQueue,
|
|
686
|
+
// A subagent is owned by its parent and never outlives it: the server terminates it when
|
|
687
|
+
// the parent closes for any reason before stopSubagent. Pinned rather than left to the
|
|
688
|
+
// SDK's default, as in Python.
|
|
689
|
+
parentClosePolicy: ParentClosePolicy.TERMINATE,
|
|
690
|
+
});
|
|
691
|
+
this.#subagents.set(handle, {
|
|
692
|
+
handle,
|
|
693
|
+
workflow_id: workflowId,
|
|
694
|
+
agent_key: agentKey,
|
|
695
|
+
last_consumed_offset: 0,
|
|
696
|
+
next_ticket: 0,
|
|
697
|
+
serving: 0,
|
|
698
|
+
});
|
|
699
|
+
this.publish({
|
|
700
|
+
type: "subagent_started",
|
|
701
|
+
subagent_id: handle,
|
|
702
|
+
agent_key: agentKey,
|
|
703
|
+
workflow_id: workflowId,
|
|
704
|
+
});
|
|
705
|
+
return handle;
|
|
706
|
+
}
|
|
707
|
+
async stopSubagent(handle) {
|
|
708
|
+
const inst = this.#subagent(handle);
|
|
709
|
+
await getExternalWorkflowHandle(inst.workflow_id).signal(CLOSE_SIGNAL);
|
|
710
|
+
this.publish({
|
|
711
|
+
type: "subagent_stopped",
|
|
712
|
+
subagent_id: inst.handle,
|
|
713
|
+
agent_key: inst.agent_key,
|
|
714
|
+
workflow_id: inst.workflow_id,
|
|
715
|
+
});
|
|
716
|
+
this.#subagents.delete(handle);
|
|
717
|
+
}
|
|
718
|
+
/** Send one message to a subagent and await its reply. Turns to one subagent run FIFO. */
|
|
719
|
+
async runSubagentTurn(handle, msgType, payload) {
|
|
720
|
+
const inst = this.#subagent(handle);
|
|
721
|
+
const ticket = inst.next_ticket++;
|
|
722
|
+
await condition(() => inst.serving === ticket || this.#closed);
|
|
723
|
+
if (inst.serving !== ticket) {
|
|
724
|
+
throw ApplicationFailure.create({
|
|
725
|
+
type: "AgentClosed",
|
|
726
|
+
nonRetryable: true,
|
|
727
|
+
message: `agent closed before subagent '${handle}' turn ${ticket} could run`,
|
|
728
|
+
});
|
|
729
|
+
}
|
|
730
|
+
try {
|
|
731
|
+
const ctx = this.currentStreamContext;
|
|
732
|
+
if (ctx === null) {
|
|
733
|
+
throw ApplicationFailure.create({
|
|
734
|
+
type: "NoActiveTurn",
|
|
735
|
+
nonRetryable: true,
|
|
736
|
+
message: "run_subagent_turn called with no active turn",
|
|
737
|
+
});
|
|
738
|
+
}
|
|
739
|
+
const { run_subagent_turn } = proxyActivities({
|
|
740
|
+
// A child turn can outlast any start-to-close, so an attempt that times out resumes from
|
|
741
|
+
// its heartbeat on the next one; the attempts are uncapped, as in Python.
|
|
742
|
+
startToCloseTimeout: DEFAULT_SUBAGENT_START_TO_CLOSE_TIMEOUT,
|
|
743
|
+
heartbeatTimeout: DEFAULT_SUBAGENT_HEARTBEAT_TIMEOUT,
|
|
744
|
+
});
|
|
745
|
+
const input = {
|
|
746
|
+
child_workflow_id: inst.workflow_id,
|
|
747
|
+
type: msgType,
|
|
748
|
+
payload,
|
|
749
|
+
from_offset: inst.last_consumed_offset,
|
|
750
|
+
handle: inst.handle,
|
|
751
|
+
agent_key: inst.agent_key,
|
|
752
|
+
parent_stream_context: ctx,
|
|
753
|
+
};
|
|
754
|
+
let result;
|
|
755
|
+
try {
|
|
756
|
+
result = await run_subagent_turn(input);
|
|
757
|
+
}
|
|
758
|
+
catch (e) {
|
|
759
|
+
const cause = e instanceof ActivityFailure ? e.cause : undefined;
|
|
760
|
+
if (cause instanceof ApplicationFailure &&
|
|
761
|
+
(cause.type === "SubagentTurnError" || cause.type === "SubagentNoReply")) {
|
|
762
|
+
this.#publishReplyReceived(inst, msgType, acceptedTurnFromError(cause), "error");
|
|
763
|
+
}
|
|
764
|
+
throw e;
|
|
765
|
+
}
|
|
766
|
+
inst.last_consumed_offset = result.consumed_offset;
|
|
767
|
+
this.#publishReplyReceived(inst, msgType, result.turn_number, "ok");
|
|
768
|
+
return result.output;
|
|
769
|
+
}
|
|
770
|
+
finally {
|
|
771
|
+
inst.serving += 1;
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
#publishReplyReceived(inst, handler, subagentTurn, outcome) {
|
|
775
|
+
this.publish({
|
|
776
|
+
type: "subagent_reply_received",
|
|
777
|
+
subagent_id: inst.handle,
|
|
778
|
+
agent_key: inst.agent_key,
|
|
779
|
+
workflow_id: inst.workflow_id,
|
|
780
|
+
handler,
|
|
781
|
+
subagent_turn: subagentTurn,
|
|
782
|
+
outcome,
|
|
783
|
+
});
|
|
784
|
+
}
|
|
785
|
+
#subagent(handle) {
|
|
786
|
+
const inst = this.#subagents.get(handle);
|
|
787
|
+
if (inst === undefined) {
|
|
788
|
+
const known = [...this.#subagents.keys()].sort();
|
|
789
|
+
throw ApplicationFailure.create({
|
|
790
|
+
type: "UnknownSubagent",
|
|
791
|
+
nonRetryable: true,
|
|
792
|
+
message: `Unknown subagent '${handle}'. Known subagents: ${JSON.stringify(known)}.`,
|
|
793
|
+
details: [{ handle, known }],
|
|
794
|
+
});
|
|
795
|
+
}
|
|
796
|
+
return inst;
|
|
797
|
+
}
|
|
798
|
+
// -- plumbing -------------------------------------------------------------------------------
|
|
799
|
+
#agentInterface() {
|
|
800
|
+
return Object.entries(this.#handlers).map(([name, h]) => ({
|
|
801
|
+
name,
|
|
802
|
+
description: h.description,
|
|
803
|
+
parameters: toJsonSchema(h.input),
|
|
804
|
+
output: toJsonSchema(h.output),
|
|
805
|
+
mid_turn: h.midTurn,
|
|
806
|
+
model_callable: h.modelCallable,
|
|
807
|
+
}));
|
|
808
|
+
}
|
|
809
|
+
#publishStateEvent(event) {
|
|
810
|
+
const ctx = this.currentStreamContext;
|
|
811
|
+
this.pub(ctx?.turn_id ?? "", ctx?.turn_number ?? 0, event);
|
|
812
|
+
}
|
|
813
|
+
/** @internal Stamp the envelope and publish. `messageId` defaults to the ambient participant. */
|
|
814
|
+
pub(turnId, turnNumber, event, messageId = AMBIENT) {
|
|
815
|
+
this.#events.publish({
|
|
816
|
+
agent_id: this.agentId,
|
|
817
|
+
turn_id: turnId,
|
|
818
|
+
turn_number: turnNumber,
|
|
819
|
+
message_id: messageId === AMBIENT ? (messageIdStore.getStore() ?? null) : messageId,
|
|
820
|
+
timestamp: Date.now() / 1000,
|
|
821
|
+
event,
|
|
822
|
+
});
|
|
823
|
+
}
|
|
824
|
+
}
|