@botbotgo/agent-harness 0.0.399 → 0.0.401
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/README.md +17 -0
- package/README.zh.md +17 -0
- package/dist/contracts/runtime-observability.d.ts +25 -0
- package/dist/package-version.d.ts +1 -1
- package/dist/package-version.js +1 -1
- package/dist/runtime/adapter/flow/stream-runtime.js +19 -1
- package/dist/runtime/adapter/local-tool-invocation.js +234 -13
- package/dist/runtime/adapter/middleware-assembly.js +1 -1
- package/dist/runtime/adapter/model/invocation-request.js +1 -1
- package/dist/runtime/adapter/model/model-providers.js +142 -119
- package/dist/runtime/adapter/stream-event-projection.js +73 -5
- package/dist/runtime/adapter/tool/tool-hitl.js +49 -6
- package/dist/runtime/agent-runtime-adapter.js +269 -23
- package/dist/runtime/harness/bindings.js +2 -0
- package/dist/runtime/harness/tool-gateway/index.d.ts +2 -0
- package/dist/runtime/harness/tool-gateway/index.js +2 -0
- package/dist/runtime/harness/tool-gateway/policy.d.ts +2 -0
- package/dist/runtime/harness/tool-gateway/policy.js +45 -0
- package/dist/runtime/harness/tool-gateway/validation.d.ts +33 -0
- package/dist/runtime/harness/tool-gateway/validation.js +176 -0
- package/package.json +15 -15
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
import { createAsyncSubAgentMiddleware, createFilesystemMiddleware, createMemoryMiddleware, createPatchToolCallsMiddleware, createSkillsMiddleware, createSummarizationMiddleware, createSubAgentMiddleware, FilesystemBackend, StateBackend, } from "deepagents";
|
|
3
|
-
import { createAgent, humanInTheLoopMiddleware, todoListMiddleware } from "langchain";
|
|
3
|
+
import { AIMessage, createAgent, createMiddleware, humanInTheLoopMiddleware, todoListMiddleware, ToolMessage } from "langchain";
|
|
4
4
|
import { sanitizeVisibleText, tryParseJson, wrapResolvedModel, } from "./parsing/output-parsing.js";
|
|
5
5
|
import { salvageJsonToolCalls } from "./parsing/output-tool-args.js";
|
|
6
6
|
import { extractMessageText } from "../utils/message-content.js";
|
|
@@ -27,7 +27,7 @@ export { computeRemainingTimeoutMs, isRetryableProviderError, resolveBindingTime
|
|
|
27
27
|
import { getBindingAdapterKind, getBindingBuiltinToolsConfig, getBindingDeepAgentSubagents, getBindingExecutionParams, getBindingExecutionKind, getBindingFilesystemConfig, getBindingMemorySources, getBindingPrimaryModel, getBindingSkills, getBindingSubagents, getBindingToolCount, getBindingPrimaryTools, getBindingSystemPrompt, isDeepAgentBinding, isLangChainBinding, } from "./support/compiled-binding.js";
|
|
28
28
|
class DelegatedExecutionNoToolEvidenceError extends Error {
|
|
29
29
|
constructor(agentId) {
|
|
30
|
-
super(`Delegated agent ${agentId}
|
|
30
|
+
super(`Delegated agent ${agentId} lacked non-planning tool evidence.`);
|
|
31
31
|
this.name = "DelegatedExecutionNoToolEvidenceError";
|
|
32
32
|
}
|
|
33
33
|
}
|
|
@@ -38,11 +38,245 @@ function hasDelegatedExecutionToolEvidence(result) {
|
|
|
38
38
|
return executedToolResults.some((toolResult) => (toolResult.isError !== true
|
|
39
39
|
&& !isPlanToolName(toolResult.toolName)));
|
|
40
40
|
}
|
|
41
|
+
function buildDelegatedPlanEvidenceBlocker(agentId) {
|
|
42
|
+
return [
|
|
43
|
+
"Status: blocked",
|
|
44
|
+
"Summary:",
|
|
45
|
+
`- Delegated agent ${agentId} ended before producing the required TODO plan evidence.`,
|
|
46
|
+
"",
|
|
47
|
+
"Blockers:",
|
|
48
|
+
"- The delegated run did not expose a valid planning trace, so the framework cannot treat the task as complete.",
|
|
49
|
+
"",
|
|
50
|
+
"Next Actions:",
|
|
51
|
+
"- Retry with the same request or inspect the delegated agent configuration and model/tool-call behavior.",
|
|
52
|
+
].join("\n");
|
|
53
|
+
}
|
|
54
|
+
function buildDelegatedExecutionEvidenceBlocker(agentId) {
|
|
55
|
+
return [
|
|
56
|
+
"Status: blocked",
|
|
57
|
+
"Summary:",
|
|
58
|
+
`- Delegated agent ${agentId} did not return any non-planning tool evidence after retry.`,
|
|
59
|
+
"",
|
|
60
|
+
"Blockers:",
|
|
61
|
+
"- The TODO board alone is not execution evidence.",
|
|
62
|
+
"- The framework cannot mark the delegated task complete without a non-planning tool result or an explicit blocker from that tool path.",
|
|
63
|
+
"",
|
|
64
|
+
"Next Actions:",
|
|
65
|
+
"- Retry the request or inspect the delegated agent's model/tool-call behavior.",
|
|
66
|
+
].join("\n");
|
|
67
|
+
}
|
|
68
|
+
function normalizePlanToolName(toolName) {
|
|
69
|
+
return typeof toolName === "string" ? toolName.trim().toLowerCase().replace(/[\s-]+/gu, "_") : "";
|
|
70
|
+
}
|
|
41
71
|
function isPlanToolName(toolName) {
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
||
|
|
45
|
-
||
|
|
72
|
+
const normalized = normalizePlanToolName(toolName);
|
|
73
|
+
return normalized === "write_todos"
|
|
74
|
+
|| normalized === "read_todos"
|
|
75
|
+
|| normalized === "tool_call_write_todos"
|
|
76
|
+
|| normalized === "tool_call_read_todos";
|
|
77
|
+
}
|
|
78
|
+
function readConfiguredToolName(value) {
|
|
79
|
+
if (typeof value !== "object" || value === null) {
|
|
80
|
+
return "";
|
|
81
|
+
}
|
|
82
|
+
const typed = value;
|
|
83
|
+
return typeof typed.name === "string" ? typed.name.trim() : "";
|
|
84
|
+
}
|
|
85
|
+
function createBootstrapTodoPlan(toolNames) {
|
|
86
|
+
const evidenceToolName = toolNames.find((toolName) => !isPlanToolName(toolName));
|
|
87
|
+
const contents = evidenceToolName
|
|
88
|
+
? [
|
|
89
|
+
`Run ${evidenceToolName} for the requested evidence`,
|
|
90
|
+
`Inspect the ${evidenceToolName} result and extract concrete findings`,
|
|
91
|
+
"Update TODO status from the observed evidence",
|
|
92
|
+
"Return the final answer grounded in tool output",
|
|
93
|
+
]
|
|
94
|
+
: [
|
|
95
|
+
"Identify the concrete evidence needed for this request",
|
|
96
|
+
"Collect and inspect the available evidence",
|
|
97
|
+
"Update TODO status from the observed evidence",
|
|
98
|
+
"Return the final answer grounded in evidence",
|
|
99
|
+
];
|
|
100
|
+
return contents.map((content, index) => ({
|
|
101
|
+
content,
|
|
102
|
+
status: index === 0 ? "in_progress" : "pending",
|
|
103
|
+
}));
|
|
104
|
+
}
|
|
105
|
+
function readMessageContentText(message) {
|
|
106
|
+
if (typeof message !== "object" || message === null) {
|
|
107
|
+
return "";
|
|
108
|
+
}
|
|
109
|
+
const content = message.content;
|
|
110
|
+
if (typeof content === "string") {
|
|
111
|
+
return content.trim();
|
|
112
|
+
}
|
|
113
|
+
if (!Array.isArray(content)) {
|
|
114
|
+
return "";
|
|
115
|
+
}
|
|
116
|
+
return content
|
|
117
|
+
.map((part) => typeof part === "object" && part !== null && typeof part.text === "string"
|
|
118
|
+
? part.text
|
|
119
|
+
: "")
|
|
120
|
+
.join("")
|
|
121
|
+
.trim();
|
|
122
|
+
}
|
|
123
|
+
function parseToolCallArgs(value) {
|
|
124
|
+
if (typeof value === "object" && value !== null && !Array.isArray(value)) {
|
|
125
|
+
return value;
|
|
126
|
+
}
|
|
127
|
+
if (typeof value !== "string" || value.trim().length === 0) {
|
|
128
|
+
return {};
|
|
129
|
+
}
|
|
130
|
+
try {
|
|
131
|
+
const parsed = JSON.parse(value);
|
|
132
|
+
return typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)
|
|
133
|
+
? parsed
|
|
134
|
+
: {};
|
|
135
|
+
}
|
|
136
|
+
catch {
|
|
137
|
+
return {};
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
function readMessageToolCalls(message) {
|
|
141
|
+
if (typeof message !== "object" || message === null) {
|
|
142
|
+
return [];
|
|
143
|
+
}
|
|
144
|
+
const typed = message;
|
|
145
|
+
const raw = Array.isArray(typed.tool_calls) ? typed.tool_calls
|
|
146
|
+
: Array.isArray(typed.kwargs?.tool_calls) ? typed.kwargs.tool_calls
|
|
147
|
+
: Array.isArray(typed.additional_kwargs?.tool_calls) ? typed.additional_kwargs.tool_calls
|
|
148
|
+
: Array.isArray(typed.kwargs?.additional_kwargs?.tool_calls) ? typed.kwargs.additional_kwargs.tool_calls
|
|
149
|
+
: Array.isArray(typed.lc_kwargs?.tool_calls) ? typed.lc_kwargs.tool_calls
|
|
150
|
+
: Array.isArray(typed.lc_kwargs?.additional_kwargs?.tool_calls) ? typed.lc_kwargs.additional_kwargs.tool_calls
|
|
151
|
+
: [];
|
|
152
|
+
return raw
|
|
153
|
+
.map((toolCall) => {
|
|
154
|
+
if (typeof toolCall !== "object" || toolCall === null) {
|
|
155
|
+
return null;
|
|
156
|
+
}
|
|
157
|
+
const call = toolCall;
|
|
158
|
+
const name = typeof call.name === "string"
|
|
159
|
+
? call.name
|
|
160
|
+
: typeof call.function?.name === "string"
|
|
161
|
+
? call.function.name
|
|
162
|
+
: undefined;
|
|
163
|
+
const args = parseToolCallArgs(call.args ?? call.function?.arguments);
|
|
164
|
+
return {
|
|
165
|
+
...(typeof call.id === "string" ? { id: call.id } : {}),
|
|
166
|
+
...(name ? { name } : {}),
|
|
167
|
+
args,
|
|
168
|
+
};
|
|
169
|
+
})
|
|
170
|
+
.filter((toolCall) => toolCall !== null);
|
|
171
|
+
}
|
|
172
|
+
function todoToolCallIsTerminal(toolCall) {
|
|
173
|
+
const todos = toolCall.args?.todos;
|
|
174
|
+
if (!Array.isArray(todos) || todos.length === 0) {
|
|
175
|
+
return false;
|
|
176
|
+
}
|
|
177
|
+
return todos.every((todo) => {
|
|
178
|
+
if (typeof todo !== "object" || todo === null || typeof todo.status !== "string") {
|
|
179
|
+
return false;
|
|
180
|
+
}
|
|
181
|
+
const status = todo.status.trim().toLowerCase();
|
|
182
|
+
return status !== "pending" && status !== "in_progress";
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
function createTodoPlanGuardMiddleware(options = {}) {
|
|
186
|
+
return createMiddleware({
|
|
187
|
+
name: "harnessTodoPlanGuard",
|
|
188
|
+
wrapToolCall: ((request, handler) => {
|
|
189
|
+
const toolName = typeof request.toolCall?.name === "string"
|
|
190
|
+
? request.toolCall.name
|
|
191
|
+
: typeof request.tool?.name === "string"
|
|
192
|
+
? request.tool.name
|
|
193
|
+
: "";
|
|
194
|
+
const messages = Array.isArray(request.state?.messages) ? request.state.messages : [];
|
|
195
|
+
const hasNonPlanToolResult = messages.some((message) => {
|
|
196
|
+
if (typeof message !== "object" || message === null) {
|
|
197
|
+
return false;
|
|
198
|
+
}
|
|
199
|
+
const typed = message;
|
|
200
|
+
const messageType = typeof typed.type === "string"
|
|
201
|
+
? typed.type
|
|
202
|
+
: typeof typed._getType === "function"
|
|
203
|
+
? String(typed._getType())
|
|
204
|
+
: "";
|
|
205
|
+
if (messageType !== "tool" && typeof typed.tool_call_id !== "string") {
|
|
206
|
+
return false;
|
|
207
|
+
}
|
|
208
|
+
const resultToolName = typeof typed.name === "string" ? typed.name : "";
|
|
209
|
+
return resultToolName.length > 0 && !isPlanToolName(resultToolName);
|
|
210
|
+
});
|
|
211
|
+
if (options.requiresPlan === true
|
|
212
|
+
&& !hasNonPlanToolResult
|
|
213
|
+
&& isPlanToolName(toolName)
|
|
214
|
+
&& normalizePlanToolName(toolName).includes("write_todos")
|
|
215
|
+
&& todoToolCallIsTerminal({ args: parseToolCallArgs(request.toolCall?.args) })) {
|
|
216
|
+
return new ToolMessage({
|
|
217
|
+
content: "Error: write_todos cannot mark every todo as terminal before any non-planning evidence tool returns. Keep one todo in_progress and the remaining todos pending until evidence tools return.",
|
|
218
|
+
tool_call_id: typeof request.toolCall?.id === "string" ? request.toolCall.id : `write-todos-tool-guard-${Math.random().toString(36).slice(2, 10)}`,
|
|
219
|
+
status: "error",
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
return handler(request);
|
|
223
|
+
}),
|
|
224
|
+
afterModel: (state) => {
|
|
225
|
+
if (!Array.isArray(state.messages) || state.messages.length === 0) {
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
const hasNonPlanToolResult = state.messages.some((message) => {
|
|
229
|
+
if (typeof message !== "object" || message === null) {
|
|
230
|
+
return false;
|
|
231
|
+
}
|
|
232
|
+
const typed = message;
|
|
233
|
+
const messageType = typeof typed.type === "string"
|
|
234
|
+
? typed.type
|
|
235
|
+
: typeof typed._getType === "function"
|
|
236
|
+
? String(typed._getType())
|
|
237
|
+
: "";
|
|
238
|
+
if (messageType !== "tool" && typeof typed.tool_call_id !== "string") {
|
|
239
|
+
return false;
|
|
240
|
+
}
|
|
241
|
+
const toolName = typeof typed.name === "string" ? typed.name : "";
|
|
242
|
+
return toolName.length > 0 && !isPlanToolName(toolName);
|
|
243
|
+
});
|
|
244
|
+
if (hasNonPlanToolResult) {
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
const lastAiMessage = [...state.messages].reverse().find((message) => readMessageToolCalls(message).length > 0);
|
|
248
|
+
const lastToolCalls = readMessageToolCalls(lastAiMessage);
|
|
249
|
+
if (!lastAiMessage && options.requiresPlan === true) {
|
|
250
|
+
const latestMessage = state.messages.at(-1);
|
|
251
|
+
const hasVisibleContent = readMessageContentText(latestMessage).length > 0;
|
|
252
|
+
if (!hasVisibleContent) {
|
|
253
|
+
return {
|
|
254
|
+
messages: [new AIMessage({
|
|
255
|
+
content: "",
|
|
256
|
+
tool_calls: [{
|
|
257
|
+
id: `write-todos-bootstrap-${Math.random().toString(36).slice(2, 10)}`,
|
|
258
|
+
name: "write_todos",
|
|
259
|
+
args: { todos: createBootstrapTodoPlan(options.toolNames ?? []) },
|
|
260
|
+
type: "tool_call",
|
|
261
|
+
}],
|
|
262
|
+
})],
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
const writeTodosCalls = lastToolCalls.filter((toolCall) => isPlanToolName(toolCall.name));
|
|
267
|
+
const prematureCompletedCalls = writeTodosCalls.filter(todoToolCallIsTerminal);
|
|
268
|
+
if (prematureCompletedCalls.length === 0) {
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
return {
|
|
272
|
+
messages: prematureCompletedCalls.map((toolCall, index) => new ToolMessage({
|
|
273
|
+
content: "Error: write_todos cannot mark every todo as terminal before any non-planning evidence tool returns. Keep one todo in_progress and the remaining todos pending until evidence tools return.",
|
|
274
|
+
tool_call_id: toolCall.id ?? `write-todos-plan-guard-${index}`,
|
|
275
|
+
status: "error",
|
|
276
|
+
})),
|
|
277
|
+
};
|
|
278
|
+
},
|
|
279
|
+
});
|
|
46
280
|
}
|
|
47
281
|
function shouldUseConfigurableDeepAgentAssembly(binding) {
|
|
48
282
|
return getBindingExecutionKind(binding) === "deepagent";
|
|
@@ -160,25 +394,21 @@ function parseCompactRouterSelection(value, subagentNames) {
|
|
|
160
394
|
}
|
|
161
395
|
function inferCompactRouterSelectionFromRequest(requestText, subagents) {
|
|
162
396
|
const normalized = requestText.toLowerCase();
|
|
397
|
+
const requestTokens = extractRouterMatchTokens(normalized);
|
|
163
398
|
const score = (subagent) => {
|
|
164
399
|
const name = subagent.name.toLowerCase();
|
|
165
400
|
const description = (subagent.description ?? "").toLowerCase();
|
|
401
|
+
const descriptionTokens = extractRouterMatchTokens(`${name} ${description}`);
|
|
166
402
|
let value = 0;
|
|
167
403
|
if (normalized.includes(name))
|
|
168
404
|
value += 4;
|
|
169
|
-
const
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
secretary: ["summary", "transcript", "youtube", "brief", "摘要", "讲稿", "转写"],
|
|
177
|
-
};
|
|
178
|
-
const keywords = keywordGroups[name] ?? [];
|
|
179
|
-
for (const keyword of keywords) {
|
|
180
|
-
if (normalized.includes(keyword))
|
|
181
|
-
value += description.includes(keyword) || name.includes(keyword) ? 3 : 1;
|
|
405
|
+
for (const token of requestTokens) {
|
|
406
|
+
if (token === name) {
|
|
407
|
+
value += 4;
|
|
408
|
+
}
|
|
409
|
+
else if (descriptionTokens.has(token)) {
|
|
410
|
+
value += token.length > 2 ? 2 : 1;
|
|
411
|
+
}
|
|
182
412
|
}
|
|
183
413
|
return value;
|
|
184
414
|
};
|
|
@@ -191,6 +421,16 @@ function inferCompactRouterSelectionFromRequest(requestText, subagents) {
|
|
|
191
421
|
}
|
|
192
422
|
return ranked[0].name;
|
|
193
423
|
}
|
|
424
|
+
function extractRouterMatchTokens(value) {
|
|
425
|
+
const tokens = new Set();
|
|
426
|
+
for (const match of value.matchAll(/[\p{L}\p{N}_-]+/gu)) {
|
|
427
|
+
const token = match[0].toLowerCase();
|
|
428
|
+
if (token.length >= 2) {
|
|
429
|
+
tokens.add(token);
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
return tokens;
|
|
433
|
+
}
|
|
194
434
|
function isDelegationOnlyDeepAgentBinding(binding) {
|
|
195
435
|
return isDeepAgentBinding(binding)
|
|
196
436
|
&& getBindingSubagents(binding).length > 0
|
|
@@ -679,8 +919,14 @@ export class AgentRuntimeAdapter {
|
|
|
679
919
|
const inlineSubagents = input.resolvedSubagents.filter((subagent) => !("graphId" in subagent));
|
|
680
920
|
const asyncSubagents = input.resolvedSubagents.filter((subagent) => "graphId" in subagent);
|
|
681
921
|
const subagents = inlineSubagents;
|
|
922
|
+
const requiresPlan = binding.harnessRuntime.executionContract?.requiresPlan === true;
|
|
923
|
+
const resolvedToolNames = input.resolvedTools.map(readConfiguredToolName).filter((name) => name.length > 0);
|
|
682
924
|
const middleware = [
|
|
683
925
|
...(builtinTools.todos === false ? [] : [todoListMiddleware()]),
|
|
926
|
+
...(builtinTools.todos === false ? [] : [createTodoPlanGuardMiddleware({
|
|
927
|
+
requiresPlan,
|
|
928
|
+
toolNames: resolvedToolNames,
|
|
929
|
+
})]),
|
|
684
930
|
...(input.resolvedSkills.length > 0 ? [createSkillsMiddleware({
|
|
685
931
|
backend,
|
|
686
932
|
sources: resolveDeepAgentSkillSourceRootPaths({
|
|
@@ -1074,7 +1320,7 @@ export class AgentRuntimeAdapter {
|
|
|
1074
1320
|
}
|
|
1075
1321
|
if (selectedBinding.harnessRuntime.executionContract?.requiresPlan === true
|
|
1076
1322
|
&& !hasDelegatedPlanEvidence(delegatedResult)) {
|
|
1077
|
-
const output =
|
|
1323
|
+
const output = buildDelegatedPlanEvidenceBlocker(selectedBinding.agent.id);
|
|
1078
1324
|
return {
|
|
1079
1325
|
toolOutput: output,
|
|
1080
1326
|
delegatedSubagentType: subagentType,
|
|
@@ -1087,7 +1333,7 @@ export class AgentRuntimeAdapter {
|
|
|
1087
1333
|
};
|
|
1088
1334
|
}
|
|
1089
1335
|
if (targetRequiresExecutionToolEvidence && !hasDelegatedExecutionToolEvidence(delegatedResult)) {
|
|
1090
|
-
const output =
|
|
1336
|
+
const output = buildDelegatedExecutionEvidenceBlocker(selectedBinding.agent.id);
|
|
1091
1337
|
return {
|
|
1092
1338
|
toolOutput: output,
|
|
1093
1339
|
delegatedSubagentType: subagentType,
|
|
@@ -1397,7 +1643,7 @@ export class AgentRuntimeAdapter {
|
|
|
1397
1643
|
}
|
|
1398
1644
|
if (selectedBinding.harnessRuntime.executionContract?.requiresPlan === true
|
|
1399
1645
|
&& !hasDelegatedPlanEvidence(delegatedResult)) {
|
|
1400
|
-
const output =
|
|
1646
|
+
const output = buildDelegatedPlanEvidenceBlocker(selectedBinding.agent.id);
|
|
1401
1647
|
delegatedResult = {
|
|
1402
1648
|
...delegatedResult,
|
|
1403
1649
|
state: "failed",
|
|
@@ -1406,7 +1652,7 @@ export class AgentRuntimeAdapter {
|
|
|
1406
1652
|
};
|
|
1407
1653
|
}
|
|
1408
1654
|
if (targetRequiresExecutionToolEvidence && !hasDelegatedExecutionToolEvidence(delegatedResult)) {
|
|
1409
|
-
const output =
|
|
1655
|
+
const output = buildDelegatedExecutionEvidenceBlocker(selectedBinding.agent.id);
|
|
1410
1656
|
delegatedResult = {
|
|
1411
1657
|
...delegatedResult,
|
|
1412
1658
|
state: "failed",
|
|
@@ -2,6 +2,7 @@ import { resolveBindingTimeout, resolveProviderRetryPolicy, resolveStreamIdleTim
|
|
|
2
2
|
import { toolRequiresRuntimeApproval } from "../adapter/tool/tool-hitl.js";
|
|
3
3
|
import { getBindingPrimaryTools } from "../support/compiled-binding.js";
|
|
4
4
|
import { compiledToolHasInputSchema } from "./tool-schema.js";
|
|
5
|
+
import { projectBindingToolGatewayPolicy } from "./tool-gateway/index.js";
|
|
5
6
|
export function getWorkspaceBinding(workspace, agentId) {
|
|
6
7
|
return workspace.bindings.get(agentId);
|
|
7
8
|
}
|
|
@@ -36,6 +37,7 @@ export function projectBindingToolExecutionPolicy(binding) {
|
|
|
36
37
|
const retryableToolCount = projectedTools.filter((tool) => tool.retryable).length;
|
|
37
38
|
return {
|
|
38
39
|
agentId: binding.agent.id,
|
|
40
|
+
gateway: projectBindingToolGatewayPolicy(binding),
|
|
39
41
|
invokeTimeoutMs: resolveBindingTimeout(binding),
|
|
40
42
|
streamIdleTimeoutMs: resolveStreamIdleTimeout(binding) ?? 60_000,
|
|
41
43
|
providerRetries: resolveProviderRetryPolicy(binding),
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { toolRequiresRuntimeApproval } from "../../adapter/tool/tool-hitl.js";
|
|
2
|
+
import { getBindingPrimaryTools } from "../../support/compiled-binding.js";
|
|
3
|
+
import { compiledToolHasInputSchema } from "../tool-schema.js";
|
|
4
|
+
export function projectBindingToolGatewayPolicy(binding) {
|
|
5
|
+
const tools = getBindingPrimaryTools(binding).map((tool) => {
|
|
6
|
+
const hasInputSchema = compiledToolHasInputSchema(tool);
|
|
7
|
+
const requiresApproval = toolRequiresRuntimeApproval(tool);
|
|
8
|
+
return {
|
|
9
|
+
toolId: tool.id,
|
|
10
|
+
name: tool.name,
|
|
11
|
+
retryable: tool.retryable === true,
|
|
12
|
+
hasInputSchema,
|
|
13
|
+
requiresApproval,
|
|
14
|
+
gatewayMode: requiresApproval ? "approval-gated" : hasInputSchema ? "schema-first" : "best-effort",
|
|
15
|
+
modelRole: "propose",
|
|
16
|
+
runtimeRole: requiresApproval
|
|
17
|
+
? "request-approval"
|
|
18
|
+
: hasInputSchema
|
|
19
|
+
? "validate-and-execute"
|
|
20
|
+
: "execute-with-runtime-checks",
|
|
21
|
+
};
|
|
22
|
+
});
|
|
23
|
+
const schemaBoundToolCount = tools.filter((tool) => tool.hasInputSchema).length;
|
|
24
|
+
const approvalRequiredToolCount = tools.filter((tool) => tool.requiresApproval).length;
|
|
25
|
+
return {
|
|
26
|
+
layer: "tool-gateway",
|
|
27
|
+
toolScope: {
|
|
28
|
+
source: "agent-binding",
|
|
29
|
+
exposedToolCount: tools.length,
|
|
30
|
+
schemaBoundToolCount,
|
|
31
|
+
approvalRequiredToolCount,
|
|
32
|
+
},
|
|
33
|
+
validation: {
|
|
34
|
+
strategy: "schema-first",
|
|
35
|
+
runtimeValidationRequired: tools.some((tool) => !tool.hasInputSchema || tool.requiresApproval),
|
|
36
|
+
strictProviderSchemaPreferred: tools.length > 0,
|
|
37
|
+
},
|
|
38
|
+
correction: {
|
|
39
|
+
invalidArguments: "structured-error-retry",
|
|
40
|
+
maxModelRetries: 2,
|
|
41
|
+
highRiskInvalidArguments: "approval-or-deny",
|
|
42
|
+
},
|
|
43
|
+
tools,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
export type ToolGatewayValidationIssue = {
|
|
2
|
+
path: string;
|
|
3
|
+
message: string;
|
|
4
|
+
expected?: string;
|
|
5
|
+
received?: string;
|
|
6
|
+
};
|
|
7
|
+
export type ToolGatewayValidationResult = {
|
|
8
|
+
ok: true;
|
|
9
|
+
input: unknown;
|
|
10
|
+
} | {
|
|
11
|
+
ok: false;
|
|
12
|
+
error: ToolGatewayInvalidArgumentsResult;
|
|
13
|
+
};
|
|
14
|
+
export type ToolGatewayInvalidArgumentsResult = {
|
|
15
|
+
isError: true;
|
|
16
|
+
code: "INVALID_ARGUMENTS";
|
|
17
|
+
toolName: string;
|
|
18
|
+
message: string;
|
|
19
|
+
retryable: boolean;
|
|
20
|
+
requiresApproval: boolean;
|
|
21
|
+
validationErrors: ToolGatewayValidationIssue[];
|
|
22
|
+
};
|
|
23
|
+
export declare function createInvalidToolArgumentsResult(input: {
|
|
24
|
+
toolName: string;
|
|
25
|
+
requiresApproval: boolean;
|
|
26
|
+
issues: ToolGatewayValidationIssue[];
|
|
27
|
+
}): ToolGatewayInvalidArgumentsResult;
|
|
28
|
+
export declare function validateToolGatewayInput(input: {
|
|
29
|
+
toolName: string;
|
|
30
|
+
schema: unknown;
|
|
31
|
+
args: unknown;
|
|
32
|
+
requiresApproval?: boolean;
|
|
33
|
+
}): ToolGatewayValidationResult;
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
function isRecord(value) {
|
|
2
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
3
|
+
}
|
|
4
|
+
function readJsonSchemaType(schema) {
|
|
5
|
+
if (!isRecord(schema)) {
|
|
6
|
+
return [];
|
|
7
|
+
}
|
|
8
|
+
const type = schema.type;
|
|
9
|
+
if (typeof type === "string") {
|
|
10
|
+
return [type];
|
|
11
|
+
}
|
|
12
|
+
return Array.isArray(type) ? type.filter((entry) => typeof entry === "string") : [];
|
|
13
|
+
}
|
|
14
|
+
function typeMatches(value, allowedTypes) {
|
|
15
|
+
if (allowedTypes.length === 0) {
|
|
16
|
+
return true;
|
|
17
|
+
}
|
|
18
|
+
if (value === null) {
|
|
19
|
+
return allowedTypes.includes("null");
|
|
20
|
+
}
|
|
21
|
+
return allowedTypes.some((type) => {
|
|
22
|
+
if (type === "integer") {
|
|
23
|
+
return Number.isInteger(value);
|
|
24
|
+
}
|
|
25
|
+
if (type === "array") {
|
|
26
|
+
return Array.isArray(value);
|
|
27
|
+
}
|
|
28
|
+
if (type === "object") {
|
|
29
|
+
return isRecord(value);
|
|
30
|
+
}
|
|
31
|
+
return typeof value === type;
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
function toPath(path) {
|
|
35
|
+
if (path.length === 0) {
|
|
36
|
+
return "$";
|
|
37
|
+
}
|
|
38
|
+
let rendered = "$";
|
|
39
|
+
for (const part of path) {
|
|
40
|
+
rendered = typeof part === "number" ? `${rendered}[${part}]` : `${rendered}.${part}`;
|
|
41
|
+
}
|
|
42
|
+
return rendered;
|
|
43
|
+
}
|
|
44
|
+
function zodIssues(error) {
|
|
45
|
+
const issues = isRecord(error) && Array.isArray(error.issues) ? error.issues : [];
|
|
46
|
+
return issues
|
|
47
|
+
.map((issue) => {
|
|
48
|
+
if (!isRecord(issue)) {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
const path = Array.isArray(issue.path) ? issue.path.filter((part) => typeof part === "string" || typeof part === "number") : [];
|
|
52
|
+
const message = typeof issue.message === "string" ? issue.message : "Invalid value";
|
|
53
|
+
return {
|
|
54
|
+
path: toPath(path),
|
|
55
|
+
message,
|
|
56
|
+
...(typeof issue.expected === "string" ? { expected: issue.expected } : {}),
|
|
57
|
+
...(typeof issue.received === "string" ? { received: issue.received } : {}),
|
|
58
|
+
};
|
|
59
|
+
})
|
|
60
|
+
.filter((issue) => issue !== null);
|
|
61
|
+
}
|
|
62
|
+
function validateJsonSchemaObject(schema, input) {
|
|
63
|
+
const issues = [];
|
|
64
|
+
const rootTypes = readJsonSchemaType(schema);
|
|
65
|
+
if (!typeMatches(input, rootTypes.length > 0 ? rootTypes : ["object"])) {
|
|
66
|
+
return [{
|
|
67
|
+
path: "$",
|
|
68
|
+
message: "Tool input must match the declared schema root type.",
|
|
69
|
+
expected: rootTypes.join("|") || "object",
|
|
70
|
+
received: Array.isArray(input) ? "array" : input === null ? "null" : typeof input,
|
|
71
|
+
}];
|
|
72
|
+
}
|
|
73
|
+
if (!isRecord(input)) {
|
|
74
|
+
return issues;
|
|
75
|
+
}
|
|
76
|
+
const properties = isRecord(schema.properties) ? schema.properties : {};
|
|
77
|
+
const required = Array.isArray(schema.required)
|
|
78
|
+
? schema.required.filter((entry) => typeof entry === "string")
|
|
79
|
+
: [];
|
|
80
|
+
for (const key of required) {
|
|
81
|
+
if (!(key in input)) {
|
|
82
|
+
issues.push({
|
|
83
|
+
path: `$.${key}`,
|
|
84
|
+
message: "Required tool argument is missing.",
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
for (const [key, value] of Object.entries(input)) {
|
|
89
|
+
const fieldSchema = properties[key];
|
|
90
|
+
if (!fieldSchema) {
|
|
91
|
+
if (schema.additionalProperties === false) {
|
|
92
|
+
issues.push({
|
|
93
|
+
path: `$.${key}`,
|
|
94
|
+
message: "Unexpected tool argument.",
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
const fieldTypes = readJsonSchemaType(fieldSchema);
|
|
100
|
+
if (!typeMatches(value, fieldTypes)) {
|
|
101
|
+
issues.push({
|
|
102
|
+
path: `$.${key}`,
|
|
103
|
+
message: "Tool argument has the wrong type.",
|
|
104
|
+
...(fieldTypes.length > 0 ? { expected: fieldTypes.join("|") } : {}),
|
|
105
|
+
received: Array.isArray(value) ? "array" : value === null ? "null" : typeof value,
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
if (isRecord(fieldSchema) && Array.isArray(fieldSchema.enum) && !fieldSchema.enum.includes(value)) {
|
|
109
|
+
issues.push({
|
|
110
|
+
path: `$.${key}`,
|
|
111
|
+
message: "Tool argument must be one of the declared enum values.",
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return issues;
|
|
116
|
+
}
|
|
117
|
+
export function createInvalidToolArgumentsResult(input) {
|
|
118
|
+
return {
|
|
119
|
+
isError: true,
|
|
120
|
+
code: "INVALID_ARGUMENTS",
|
|
121
|
+
toolName: input.toolName,
|
|
122
|
+
message: `Tool ${input.toolName} received invalid arguments. Correct the arguments to match the declared schema before retrying.`,
|
|
123
|
+
retryable: !input.requiresApproval,
|
|
124
|
+
requiresApproval: input.requiresApproval,
|
|
125
|
+
validationErrors: input.issues.length > 0
|
|
126
|
+
? input.issues
|
|
127
|
+
: [{ path: "$", message: "Tool input failed schema validation." }],
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
export function validateToolGatewayInput(input) {
|
|
131
|
+
const schema = input.schema;
|
|
132
|
+
const requiresApproval = input.requiresApproval === true;
|
|
133
|
+
if (schema && typeof schema.safeParse === "function") {
|
|
134
|
+
const parsed = schema.safeParse(input.args);
|
|
135
|
+
if (parsed.success) {
|
|
136
|
+
return { ok: true, input: parsed.data };
|
|
137
|
+
}
|
|
138
|
+
return {
|
|
139
|
+
ok: false,
|
|
140
|
+
error: createInvalidToolArgumentsResult({
|
|
141
|
+
toolName: input.toolName,
|
|
142
|
+
requiresApproval,
|
|
143
|
+
issues: zodIssues(parsed.error),
|
|
144
|
+
}),
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
if (schema && typeof schema.parse === "function") {
|
|
148
|
+
try {
|
|
149
|
+
return { ok: true, input: schema.parse(input.args) };
|
|
150
|
+
}
|
|
151
|
+
catch (error) {
|
|
152
|
+
return {
|
|
153
|
+
ok: false,
|
|
154
|
+
error: createInvalidToolArgumentsResult({
|
|
155
|
+
toolName: input.toolName,
|
|
156
|
+
requiresApproval,
|
|
157
|
+
issues: zodIssues(error),
|
|
158
|
+
}),
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
if (isRecord(input.schema) && (input.schema.type === "object" || isRecord(input.schema.properties))) {
|
|
163
|
+
const issues = validateJsonSchemaObject(input.schema, input.args);
|
|
164
|
+
if (issues.length > 0) {
|
|
165
|
+
return {
|
|
166
|
+
ok: false,
|
|
167
|
+
error: createInvalidToolArgumentsResult({
|
|
168
|
+
toolName: input.toolName,
|
|
169
|
+
requiresApproval,
|
|
170
|
+
issues,
|
|
171
|
+
}),
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
return { ok: true, input: input.args };
|
|
176
|
+
}
|