@kodelyth/codex 2026.5.42 → 2026.6.1
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/package.json +16 -1
- package/doctor-contract-api.test.ts +0 -44
- package/doctor-contract-api.ts +0 -68
- package/harness.ts +0 -72
- package/index.test.ts +0 -230
- package/index.ts +0 -66
- package/media-understanding-provider.test.ts +0 -486
- package/media-understanding-provider.ts +0 -521
- package/prompt-overlay-runtime-contract.test.ts +0 -48
- package/prompt-overlay.ts +0 -21
- package/provider-catalog.ts +0 -83
- package/provider-discovery.ts +0 -45
- package/provider.test.ts +0 -384
- package/provider.ts +0 -243
- package/src/app-server/app-inventory-cache.test.ts +0 -176
- package/src/app-server/app-inventory-cache.ts +0 -324
- package/src/app-server/approval-bridge.test.ts +0 -1471
- package/src/app-server/approval-bridge.ts +0 -1211
- package/src/app-server/auth-bridge.test.ts +0 -1449
- package/src/app-server/auth-bridge.ts +0 -614
- package/src/app-server/auth-profile-runtime-contract.test.ts +0 -239
- package/src/app-server/capabilities.ts +0 -27
- package/src/app-server/client-factory.ts +0 -24
- package/src/app-server/client.test.ts +0 -563
- package/src/app-server/client.ts +0 -715
- package/src/app-server/compact.test.ts +0 -710
- package/src/app-server/compact.ts +0 -500
- package/src/app-server/computer-use.test.ts +0 -788
- package/src/app-server/computer-use.ts +0 -683
- package/src/app-server/config.test.ts +0 -879
- package/src/app-server/config.ts +0 -1038
- package/src/app-server/context-engine-projection.test.ts +0 -252
- package/src/app-server/context-engine-projection.ts +0 -403
- package/src/app-server/delivery-no-reply-runtime-contract.test.ts +0 -80
- package/src/app-server/dynamic-tool-diagnostics.ts +0 -73
- package/src/app-server/dynamic-tool-profile.ts +0 -69
- package/src/app-server/dynamic-tools.test.ts +0 -1302
- package/src/app-server/dynamic-tools.ts +0 -623
- package/src/app-server/elicitation-bridge.test.ts +0 -1056
- package/src/app-server/elicitation-bridge.ts +0 -783
- package/src/app-server/event-projector.test.ts +0 -2668
- package/src/app-server/event-projector.ts +0 -2057
- package/src/app-server/image-payload-sanitizer.test.ts +0 -49
- package/src/app-server/image-payload-sanitizer.ts +0 -167
- package/src/app-server/klaw-owned-tool-runtime-contract.test.ts +0 -456
- package/src/app-server/local-runtime-attribution.ts +0 -39
- package/src/app-server/managed-binary.test.ts +0 -139
- package/src/app-server/managed-binary.ts +0 -193
- package/src/app-server/models.test.ts +0 -246
- package/src/app-server/models.ts +0 -172
- package/src/app-server/native-hook-relay.test.ts +0 -271
- package/src/app-server/native-hook-relay.ts +0 -150
- package/src/app-server/native-subagent-task-mirror.test.ts +0 -573
- package/src/app-server/native-subagent-task-mirror.ts +0 -497
- package/src/app-server/outcome-fallback-runtime-contract.test.ts +0 -404
- package/src/app-server/plugin-activation.test.ts +0 -336
- package/src/app-server/plugin-activation.ts +0 -283
- package/src/app-server/plugin-app-cache-key.ts +0 -74
- package/src/app-server/plugin-approval-roundtrip.ts +0 -122
- package/src/app-server/plugin-inventory.test.ts +0 -355
- package/src/app-server/plugin-inventory.ts +0 -357
- package/src/app-server/plugin-thread-config.test.ts +0 -865
- package/src/app-server/plugin-thread-config.ts +0 -455
- package/src/app-server/protocol-generated/json/DynamicToolCallParams.json +0 -33
- package/src/app-server/protocol-generated/json/v2/ErrorNotification.json +0 -199
- package/src/app-server/protocol-generated/json/v2/GetAccountResponse.json +0 -102
- package/src/app-server/protocol-generated/json/v2/ModelListResponse.json +0 -227
- package/src/app-server/protocol-generated/json/v2/ThreadResumeResponse.json +0 -2630
- package/src/app-server/protocol-generated/json/v2/ThreadStartResponse.json +0 -2630
- package/src/app-server/protocol-generated/json/v2/TurnCompletedNotification.json +0 -1659
- package/src/app-server/protocol-generated/json/v2/TurnStartResponse.json +0 -1655
- package/src/app-server/protocol-validators.test.ts +0 -75
- package/src/app-server/protocol-validators.ts +0 -203
- package/src/app-server/protocol.ts +0 -520
- package/src/app-server/rate-limit-cache.ts +0 -48
- package/src/app-server/rate-limits.test.ts +0 -202
- package/src/app-server/rate-limits.ts +0 -583
- package/src/app-server/request.ts +0 -73
- package/src/app-server/run-attempt.context-engine.test.ts +0 -1004
- package/src/app-server/run-attempt.test.ts +0 -9477
- package/src/app-server/run-attempt.ts +0 -4683
- package/src/app-server/run-attempt.vision-tools.test.ts +0 -35
- package/src/app-server/schema-normalization-runtime-contract.test.ts +0 -206
- package/src/app-server/session-binding.test.ts +0 -303
- package/src/app-server/session-binding.ts +0 -398
- package/src/app-server/session-history.ts +0 -44
- package/src/app-server/shared-client.test.ts +0 -589
- package/src/app-server/shared-client.ts +0 -289
- package/src/app-server/side-question.test.ts +0 -1175
- package/src/app-server/side-question.ts +0 -1007
- package/src/app-server/test-support.ts +0 -48
- package/src/app-server/thread-lifecycle.test.ts +0 -447
- package/src/app-server/thread-lifecycle.ts +0 -939
- package/src/app-server/thread-lifecycle.user-mcp-servers.test.ts +0 -442
- package/src/app-server/timeout.ts +0 -9
- package/src/app-server/tool-progress-normalization.ts +0 -77
- package/src/app-server/trajectory.test.ts +0 -205
- package/src/app-server/trajectory.ts +0 -365
- package/src/app-server/transcript-mirror.test.ts +0 -524
- package/src/app-server/transcript-mirror.ts +0 -208
- package/src/app-server/transcript-repair-runtime-contract.test.ts +0 -44
- package/src/app-server/transport-stdio.test.ts +0 -171
- package/src/app-server/transport-stdio.ts +0 -107
- package/src/app-server/transport-websocket.test.ts +0 -69
- package/src/app-server/transport-websocket.ts +0 -90
- package/src/app-server/transport.ts +0 -117
- package/src/app-server/user-input-bridge.test.ts +0 -249
- package/src/app-server/user-input-bridge.ts +0 -316
- package/src/app-server/version.ts +0 -4
- package/src/app-server/vision-tools.ts +0 -12
- package/src/command-account.ts +0 -544
- package/src/command-formatters.ts +0 -425
- package/src/command-handlers.ts +0 -2004
- package/src/command-rpc.test.ts +0 -16
- package/src/command-rpc.ts +0 -142
- package/src/commands.test.ts +0 -3312
- package/src/commands.ts +0 -65
- package/src/conversation-binding-data.ts +0 -124
- package/src/conversation-binding.test.ts +0 -599
- package/src/conversation-binding.ts +0 -561
- package/src/conversation-control.test.ts +0 -126
- package/src/conversation-control.ts +0 -303
- package/src/conversation-turn-collector.test.ts +0 -191
- package/src/conversation-turn-collector.ts +0 -186
- package/src/conversation-turn-input.test.ts +0 -141
- package/src/conversation-turn-input.ts +0 -106
- package/src/manifest.test.ts +0 -20
- package/src/migration/apply.ts +0 -501
- package/src/migration/helpers.ts +0 -55
- package/src/migration/plan.ts +0 -461
- package/src/migration/provider.test.ts +0 -1741
- package/src/migration/provider.ts +0 -41
- package/src/migration/source.ts +0 -643
- package/src/migration/targets.ts +0 -25
- package/src/node-cli-sessions.test.ts +0 -180
- package/src/node-cli-sessions.ts +0 -711
- package/test-api.ts +0 -82
- package/tsconfig.json +0 -16
|
@@ -1,1302 +0,0 @@
|
|
|
1
|
-
import type { AgentToolResult } from "@earendil-works/pi-agent-core";
|
|
2
|
-
import type { AnyAgentTool } from "klaw/plugin-sdk/agent-harness";
|
|
3
|
-
import {
|
|
4
|
-
HEARTBEAT_RESPONSE_TOOL_NAME,
|
|
5
|
-
wrapToolWithBeforeToolCallHook,
|
|
6
|
-
} from "klaw/plugin-sdk/agent-harness-runtime";
|
|
7
|
-
import { initializeGlobalHookRunner, resetGlobalHookRunner } from "klaw/plugin-sdk/hook-runtime";
|
|
8
|
-
import {
|
|
9
|
-
createEmptyPluginRegistry,
|
|
10
|
-
createMockPluginRegistry,
|
|
11
|
-
setActivePluginRegistry,
|
|
12
|
-
} from "klaw/plugin-sdk/plugin-test-runtime";
|
|
13
|
-
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
14
|
-
import {
|
|
15
|
-
CODEX_KLAW_DYNAMIC_TOOL_NAMESPACE,
|
|
16
|
-
createCodexDynamicToolBridge,
|
|
17
|
-
} from "./dynamic-tools.js";
|
|
18
|
-
import type { JsonValue } from "./protocol.js";
|
|
19
|
-
|
|
20
|
-
function createTool(overrides: Partial<AnyAgentTool>): AnyAgentTool {
|
|
21
|
-
return {
|
|
22
|
-
name: "tts",
|
|
23
|
-
description: "Convert text to speech.",
|
|
24
|
-
parameters: { type: "object", properties: {} },
|
|
25
|
-
execute: vi.fn(),
|
|
26
|
-
...overrides,
|
|
27
|
-
} as unknown as AnyAgentTool;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
function mediaResult(mediaUrl: string, audioAsVoice?: boolean): AgentToolResult<unknown> {
|
|
31
|
-
return {
|
|
32
|
-
content: [{ type: "text", text: "Generated media reply." }],
|
|
33
|
-
details: {
|
|
34
|
-
media: {
|
|
35
|
-
mediaUrl,
|
|
36
|
-
...(audioAsVoice === true ? { audioAsVoice: true } : {}),
|
|
37
|
-
},
|
|
38
|
-
},
|
|
39
|
-
};
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
function textToolResult(text: string, details: unknown = {}): AgentToolResult<unknown> {
|
|
43
|
-
return {
|
|
44
|
-
content: [{ type: "text", text }],
|
|
45
|
-
details,
|
|
46
|
-
};
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
function createBridgeWithToolResult(
|
|
50
|
-
toolName: string,
|
|
51
|
-
toolResult: AgentToolResult<unknown>,
|
|
52
|
-
hookContext?: Parameters<typeof createCodexDynamicToolBridge>[0]["hookContext"],
|
|
53
|
-
) {
|
|
54
|
-
return createCodexDynamicToolBridge({
|
|
55
|
-
tools: [
|
|
56
|
-
createTool({
|
|
57
|
-
name: toolName,
|
|
58
|
-
execute: vi.fn(async () => toolResult),
|
|
59
|
-
}),
|
|
60
|
-
],
|
|
61
|
-
signal: new AbortController().signal,
|
|
62
|
-
hookContext,
|
|
63
|
-
});
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
function expectInputText(text: string) {
|
|
67
|
-
return {
|
|
68
|
-
success: true,
|
|
69
|
-
contentItems: [{ type: "inputText", text }],
|
|
70
|
-
};
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
function requireRecord(value: unknown, label: string): Record<string, unknown> {
|
|
74
|
-
if (!value || typeof value !== "object") {
|
|
75
|
-
throw new Error(`expected ${label}`);
|
|
76
|
-
}
|
|
77
|
-
return value as Record<string, unknown>;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
function requireArray(value: unknown, label: string): Array<unknown> {
|
|
81
|
-
expect(Array.isArray(value), label).toBe(true);
|
|
82
|
-
return value as Array<unknown>;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
function callArg(
|
|
86
|
-
mock: { mock: { calls: Array<Array<unknown>> } },
|
|
87
|
-
callIndex: number,
|
|
88
|
-
argIndex: number,
|
|
89
|
-
label: string,
|
|
90
|
-
) {
|
|
91
|
-
const call = mock.mock.calls.at(callIndex);
|
|
92
|
-
if (!call) {
|
|
93
|
-
throw new Error(`Expected ${label}`);
|
|
94
|
-
}
|
|
95
|
-
return call[argIndex];
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
function expectDynamicSpec(
|
|
99
|
-
spec: unknown,
|
|
100
|
-
fields: { name: string; namespace?: string; deferLoading?: boolean },
|
|
101
|
-
) {
|
|
102
|
-
const record = requireRecord(spec, `${fields.name} spec`);
|
|
103
|
-
expect(record.name).toBe(fields.name);
|
|
104
|
-
if (fields.namespace !== undefined) {
|
|
105
|
-
expect(record.namespace).toBe(fields.namespace);
|
|
106
|
-
}
|
|
107
|
-
if (fields.deferLoading !== undefined) {
|
|
108
|
-
expect(record.deferLoading).toBe(fields.deferLoading);
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
function expectNoNamespace(spec: unknown) {
|
|
113
|
-
const record = requireRecord(spec, "tool spec");
|
|
114
|
-
expect(record).not.toHaveProperty("namespace");
|
|
115
|
-
expect(record).not.toHaveProperty("deferLoading");
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
function expectContextFields(context: unknown, fields: Record<string, unknown>) {
|
|
119
|
-
const record = requireRecord(context, "hook context");
|
|
120
|
-
for (const [key, value] of Object.entries(fields)) {
|
|
121
|
-
expect(record[key]).toEqual(value);
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
function expectToolResult(value: unknown, expected: AgentToolResult<unknown>) {
|
|
126
|
-
const result = requireRecord(value, "tool result");
|
|
127
|
-
expect(result.content).toEqual(expected.content);
|
|
128
|
-
expect(result.details).toEqual(expected.details);
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
function expectExecuteCall(
|
|
132
|
-
execute: { mock: { calls: Array<Array<unknown>> } },
|
|
133
|
-
expected: { callId: string; args: Record<string, unknown> },
|
|
134
|
-
) {
|
|
135
|
-
expect(callArg(execute, 0, 0, "execute call id")).toBe(expected.callId);
|
|
136
|
-
expect(callArg(execute, 0, 1, "execute args")).toEqual(expected.args);
|
|
137
|
-
expect(callArg(execute, 0, 2, "execute signal")).toBeInstanceOf(AbortSignal);
|
|
138
|
-
expect(callArg(execute, 0, 3, "execute extra")).toBeUndefined();
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
async function handleMessageToolCall(
|
|
142
|
-
bridge: ReturnType<typeof createCodexDynamicToolBridge>,
|
|
143
|
-
arguments_: JsonValue,
|
|
144
|
-
) {
|
|
145
|
-
return await bridge.handleToolCall({
|
|
146
|
-
threadId: "thread-1",
|
|
147
|
-
turnId: "turn-1",
|
|
148
|
-
callId: "call-1",
|
|
149
|
-
namespace: null,
|
|
150
|
-
tool: "message",
|
|
151
|
-
arguments: arguments_,
|
|
152
|
-
});
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
afterEach(() => {
|
|
156
|
-
resetGlobalHookRunner();
|
|
157
|
-
setActivePluginRegistry(createEmptyPluginRegistry());
|
|
158
|
-
});
|
|
159
|
-
|
|
160
|
-
describe("createCodexDynamicToolBridge", () => {
|
|
161
|
-
it("keeps turn-yield direct while deferring Klaw session spawn", () => {
|
|
162
|
-
const bridge = createCodexDynamicToolBridge({
|
|
163
|
-
tools: [
|
|
164
|
-
createTool({ name: "web_search" }),
|
|
165
|
-
createTool({ name: "message" }),
|
|
166
|
-
createTool({ name: HEARTBEAT_RESPONSE_TOOL_NAME }),
|
|
167
|
-
createTool({ name: "sessions_spawn" }),
|
|
168
|
-
createTool({ name: "sessions_yield" }),
|
|
169
|
-
],
|
|
170
|
-
signal: new AbortController().signal,
|
|
171
|
-
});
|
|
172
|
-
|
|
173
|
-
const webSearch = bridge.specs.find((tool) => tool.name === "web_search");
|
|
174
|
-
const message = bridge.specs.find((tool) => tool.name === "message");
|
|
175
|
-
const heartbeat = bridge.specs.find((tool) => tool.name === HEARTBEAT_RESPONSE_TOOL_NAME);
|
|
176
|
-
const sessionsSpawn = bridge.specs.find((tool) => tool.name === "sessions_spawn");
|
|
177
|
-
const sessionsYield = bridge.specs.find((tool) => tool.name === "sessions_yield");
|
|
178
|
-
|
|
179
|
-
expectDynamicSpec(webSearch, {
|
|
180
|
-
name: "web_search",
|
|
181
|
-
namespace: CODEX_KLAW_DYNAMIC_TOOL_NAMESPACE,
|
|
182
|
-
deferLoading: true,
|
|
183
|
-
});
|
|
184
|
-
expectDynamicSpec(message, {
|
|
185
|
-
name: "message",
|
|
186
|
-
namespace: CODEX_KLAW_DYNAMIC_TOOL_NAMESPACE,
|
|
187
|
-
deferLoading: true,
|
|
188
|
-
});
|
|
189
|
-
expectDynamicSpec(heartbeat, {
|
|
190
|
-
name: HEARTBEAT_RESPONSE_TOOL_NAME,
|
|
191
|
-
namespace: CODEX_KLAW_DYNAMIC_TOOL_NAMESPACE,
|
|
192
|
-
deferLoading: true,
|
|
193
|
-
});
|
|
194
|
-
expectDynamicSpec(sessionsSpawn, {
|
|
195
|
-
name: "sessions_spawn",
|
|
196
|
-
namespace: CODEX_KLAW_DYNAMIC_TOOL_NAMESPACE,
|
|
197
|
-
deferLoading: true,
|
|
198
|
-
});
|
|
199
|
-
expectNoNamespace(sessionsYield);
|
|
200
|
-
});
|
|
201
|
-
|
|
202
|
-
it("keeps configured direct tools in the initial Codex tool context", () => {
|
|
203
|
-
const bridge = createCodexDynamicToolBridge({
|
|
204
|
-
tools: [createTool({ name: "message" }), createTool({ name: "web_search" })],
|
|
205
|
-
signal: new AbortController().signal,
|
|
206
|
-
directToolNames: ["message"],
|
|
207
|
-
});
|
|
208
|
-
|
|
209
|
-
expect(bridge.specs).toHaveLength(2);
|
|
210
|
-
expectDynamicSpec(bridge.specs[0], { name: "message" });
|
|
211
|
-
expectDynamicSpec(bridge.specs[1], {
|
|
212
|
-
name: "web_search",
|
|
213
|
-
namespace: CODEX_KLAW_DYNAMIC_TOOL_NAMESPACE,
|
|
214
|
-
deferLoading: true,
|
|
215
|
-
});
|
|
216
|
-
expectNoNamespace(bridge.specs[0]);
|
|
217
|
-
});
|
|
218
|
-
|
|
219
|
-
it("can expose all dynamic tools directly for compatibility", () => {
|
|
220
|
-
const bridge = createCodexDynamicToolBridge({
|
|
221
|
-
tools: [createTool({ name: "web_search" }), createTool({ name: "message" })],
|
|
222
|
-
signal: new AbortController().signal,
|
|
223
|
-
loading: "direct",
|
|
224
|
-
});
|
|
225
|
-
|
|
226
|
-
expect(bridge.specs).toHaveLength(2);
|
|
227
|
-
expectDynamicSpec(bridge.specs[0], { name: "web_search" });
|
|
228
|
-
expectDynamicSpec(bridge.specs[1], { name: "message" });
|
|
229
|
-
expectNoNamespace(bridge.specs[0]);
|
|
230
|
-
expectNoNamespace(bridge.specs[1]);
|
|
231
|
-
});
|
|
232
|
-
|
|
233
|
-
it("truncates configured text tool results before returning them to Codex", async () => {
|
|
234
|
-
const longText = "x".repeat(400);
|
|
235
|
-
const bridge = createCodexDynamicToolBridge({
|
|
236
|
-
tools: [
|
|
237
|
-
createTool({
|
|
238
|
-
name: "large_lookup",
|
|
239
|
-
execute: vi.fn(async () => textToolResult(longText)),
|
|
240
|
-
}),
|
|
241
|
-
],
|
|
242
|
-
signal: new AbortController().signal,
|
|
243
|
-
hookContext: {
|
|
244
|
-
agentId: "main",
|
|
245
|
-
config: {
|
|
246
|
-
agents: {
|
|
247
|
-
defaults: {
|
|
248
|
-
contextLimits: {
|
|
249
|
-
toolResultMaxChars: 180,
|
|
250
|
-
},
|
|
251
|
-
},
|
|
252
|
-
},
|
|
253
|
-
} as never,
|
|
254
|
-
},
|
|
255
|
-
});
|
|
256
|
-
|
|
257
|
-
const result = await bridge.handleToolCall({
|
|
258
|
-
threadId: "thread-1",
|
|
259
|
-
turnId: "turn-1",
|
|
260
|
-
callId: "call-1",
|
|
261
|
-
namespace: null,
|
|
262
|
-
tool: "large_lookup",
|
|
263
|
-
arguments: {},
|
|
264
|
-
});
|
|
265
|
-
|
|
266
|
-
expect(result.success).toBe(true);
|
|
267
|
-
const firstItem = result.contentItems[0];
|
|
268
|
-
if (firstItem?.type !== "inputText" || typeof firstItem.text !== "string") {
|
|
269
|
-
throw new Error("expected inputText tool result");
|
|
270
|
-
}
|
|
271
|
-
const text = firstItem.text;
|
|
272
|
-
expect(text.length).toBeLessThanOrEqual(180);
|
|
273
|
-
expect(text).toContain("Klaw truncated dynamic tool result");
|
|
274
|
-
expect(text).toContain("original 400 chars");
|
|
275
|
-
expect(text).toContain("rerun with narrower args");
|
|
276
|
-
});
|
|
277
|
-
|
|
278
|
-
it("honors normalized per-agent dynamic tool result caps", async () => {
|
|
279
|
-
const bridge = createCodexDynamicToolBridge({
|
|
280
|
-
tools: [
|
|
281
|
-
createTool({
|
|
282
|
-
name: "large_lookup",
|
|
283
|
-
execute: vi.fn(async () => textToolResult("x".repeat(400))),
|
|
284
|
-
}),
|
|
285
|
-
],
|
|
286
|
-
signal: new AbortController().signal,
|
|
287
|
-
hookContext: {
|
|
288
|
-
agentId: "research-bot",
|
|
289
|
-
config: {
|
|
290
|
-
agents: {
|
|
291
|
-
defaults: {
|
|
292
|
-
contextLimits: {
|
|
293
|
-
toolResultMaxChars: 1_000,
|
|
294
|
-
},
|
|
295
|
-
},
|
|
296
|
-
list: [
|
|
297
|
-
{
|
|
298
|
-
id: "Research Bot",
|
|
299
|
-
contextLimits: {
|
|
300
|
-
toolResultMaxChars: 180,
|
|
301
|
-
},
|
|
302
|
-
},
|
|
303
|
-
],
|
|
304
|
-
},
|
|
305
|
-
} as never,
|
|
306
|
-
},
|
|
307
|
-
});
|
|
308
|
-
|
|
309
|
-
const result = await bridge.handleToolCall({
|
|
310
|
-
threadId: "thread-1",
|
|
311
|
-
turnId: "turn-1",
|
|
312
|
-
callId: "call-1",
|
|
313
|
-
namespace: null,
|
|
314
|
-
tool: "large_lookup",
|
|
315
|
-
arguments: {},
|
|
316
|
-
});
|
|
317
|
-
|
|
318
|
-
expect(result.success).toBe(true);
|
|
319
|
-
const firstItem = result.contentItems[0];
|
|
320
|
-
if (firstItem?.type !== "inputText" || typeof firstItem.text !== "string") {
|
|
321
|
-
throw new Error("expected inputText tool result");
|
|
322
|
-
}
|
|
323
|
-
expect(firstItem.text.length).toBeLessThanOrEqual(180);
|
|
324
|
-
expect(firstItem.text).toContain("Klaw truncated dynamic tool result");
|
|
325
|
-
});
|
|
326
|
-
|
|
327
|
-
it("keeps truncation notices within tiny configured caps", async () => {
|
|
328
|
-
const bridge = createCodexDynamicToolBridge({
|
|
329
|
-
tools: [
|
|
330
|
-
createTool({
|
|
331
|
-
name: "large_lookup",
|
|
332
|
-
execute: vi.fn(async () => textToolResult("x".repeat(400))),
|
|
333
|
-
}),
|
|
334
|
-
],
|
|
335
|
-
signal: new AbortController().signal,
|
|
336
|
-
hookContext: {
|
|
337
|
-
agentId: "main",
|
|
338
|
-
config: {
|
|
339
|
-
agents: {
|
|
340
|
-
defaults: {
|
|
341
|
-
contextLimits: {
|
|
342
|
-
toolResultMaxChars: 32,
|
|
343
|
-
},
|
|
344
|
-
},
|
|
345
|
-
},
|
|
346
|
-
} as never,
|
|
347
|
-
},
|
|
348
|
-
});
|
|
349
|
-
|
|
350
|
-
const result = await bridge.handleToolCall({
|
|
351
|
-
threadId: "thread-1",
|
|
352
|
-
turnId: "turn-1",
|
|
353
|
-
callId: "call-1",
|
|
354
|
-
namespace: null,
|
|
355
|
-
tool: "large_lookup",
|
|
356
|
-
arguments: {},
|
|
357
|
-
});
|
|
358
|
-
|
|
359
|
-
expect(result.success).toBe(true);
|
|
360
|
-
const firstItem = result.contentItems[0];
|
|
361
|
-
if (firstItem?.type !== "inputText" || typeof firstItem.text !== "string") {
|
|
362
|
-
throw new Error("expected inputText tool result");
|
|
363
|
-
}
|
|
364
|
-
expect(firstItem.text.length).toBeLessThanOrEqual(32);
|
|
365
|
-
expect(firstItem.text).toBe("...(Klaw truncated dynamic tool".slice(0, 32));
|
|
366
|
-
});
|
|
367
|
-
|
|
368
|
-
it("budgets configured truncation across all text result blocks", async () => {
|
|
369
|
-
const bridge = createCodexDynamicToolBridge({
|
|
370
|
-
tools: [
|
|
371
|
-
createTool({
|
|
372
|
-
name: "large_lookup",
|
|
373
|
-
execute: vi.fn(async () => ({
|
|
374
|
-
content: [
|
|
375
|
-
{ type: "text" as const, text: "a".repeat(200) },
|
|
376
|
-
{ type: "text" as const, text: "b".repeat(200) },
|
|
377
|
-
],
|
|
378
|
-
details: {},
|
|
379
|
-
})),
|
|
380
|
-
}),
|
|
381
|
-
],
|
|
382
|
-
signal: new AbortController().signal,
|
|
383
|
-
hookContext: {
|
|
384
|
-
agentId: "main",
|
|
385
|
-
config: {
|
|
386
|
-
agents: {
|
|
387
|
-
defaults: {
|
|
388
|
-
contextLimits: {
|
|
389
|
-
toolResultMaxChars: 180,
|
|
390
|
-
},
|
|
391
|
-
},
|
|
392
|
-
},
|
|
393
|
-
} as never,
|
|
394
|
-
},
|
|
395
|
-
});
|
|
396
|
-
|
|
397
|
-
const result = await bridge.handleToolCall({
|
|
398
|
-
threadId: "thread-1",
|
|
399
|
-
turnId: "turn-1",
|
|
400
|
-
callId: "call-1",
|
|
401
|
-
namespace: null,
|
|
402
|
-
tool: "large_lookup",
|
|
403
|
-
arguments: {},
|
|
404
|
-
});
|
|
405
|
-
|
|
406
|
-
expect(result.success).toBe(true);
|
|
407
|
-
const text = result.contentItems
|
|
408
|
-
.map((item) => (item.type === "inputText" && typeof item.text === "string" ? item.text : ""))
|
|
409
|
-
.join("");
|
|
410
|
-
expect(text.length).toBeLessThanOrEqual(180);
|
|
411
|
-
expect(text).toContain("Klaw truncated dynamic tool result");
|
|
412
|
-
expect(text).toContain("original 400 chars");
|
|
413
|
-
expect(text).not.toContain("b".repeat(100));
|
|
414
|
-
});
|
|
415
|
-
|
|
416
|
-
it.each([
|
|
417
|
-
{ toolName: "tts", mediaUrl: "/tmp/reply.opus", audioAsVoice: true },
|
|
418
|
-
{ toolName: "image_generate", mediaUrl: "/tmp/generated.png" },
|
|
419
|
-
{ toolName: "video_generate", mediaUrl: "https://media.example/video.mp4" },
|
|
420
|
-
{ toolName: "music_generate", mediaUrl: "https://media.example/music.wav" },
|
|
421
|
-
])(
|
|
422
|
-
"preserves structured media artifacts from $toolName tool results",
|
|
423
|
-
async ({ toolName, mediaUrl, audioAsVoice }) => {
|
|
424
|
-
const bridge = createBridgeWithToolResult(toolName, mediaResult(mediaUrl, audioAsVoice));
|
|
425
|
-
|
|
426
|
-
const result = await bridge.handleToolCall({
|
|
427
|
-
threadId: "thread-1",
|
|
428
|
-
turnId: "turn-1",
|
|
429
|
-
callId: "call-1",
|
|
430
|
-
namespace: null,
|
|
431
|
-
tool: toolName,
|
|
432
|
-
arguments: { prompt: "hello" },
|
|
433
|
-
});
|
|
434
|
-
|
|
435
|
-
expect(result).toEqual(expectInputText("Generated media reply."));
|
|
436
|
-
expect(bridge.telemetry.toolMediaUrls).toEqual([mediaUrl]);
|
|
437
|
-
expect(bridge.telemetry.toolAudioAsVoice).toBe(audioAsVoice === true);
|
|
438
|
-
},
|
|
439
|
-
);
|
|
440
|
-
|
|
441
|
-
it("preserves audio-as-voice metadata from tts results", async () => {
|
|
442
|
-
const toolResult = {
|
|
443
|
-
content: [{ type: "text", text: "(spoken) hello" }],
|
|
444
|
-
details: {
|
|
445
|
-
media: {
|
|
446
|
-
mediaUrl: "/tmp/reply.opus",
|
|
447
|
-
audioAsVoice: true,
|
|
448
|
-
},
|
|
449
|
-
},
|
|
450
|
-
} satisfies AgentToolResult<unknown>;
|
|
451
|
-
const tool = createTool({
|
|
452
|
-
execute: vi.fn(async () => toolResult),
|
|
453
|
-
});
|
|
454
|
-
const bridge = createCodexDynamicToolBridge({
|
|
455
|
-
tools: [tool],
|
|
456
|
-
signal: new AbortController().signal,
|
|
457
|
-
});
|
|
458
|
-
|
|
459
|
-
const result = await bridge.handleToolCall({
|
|
460
|
-
threadId: "thread-1",
|
|
461
|
-
turnId: "turn-1",
|
|
462
|
-
callId: "call-1",
|
|
463
|
-
namespace: null,
|
|
464
|
-
tool: "tts",
|
|
465
|
-
arguments: { text: "hello" },
|
|
466
|
-
});
|
|
467
|
-
|
|
468
|
-
expect(result).toEqual({
|
|
469
|
-
success: true,
|
|
470
|
-
contentItems: [{ type: "inputText", text: "(spoken) hello" }],
|
|
471
|
-
});
|
|
472
|
-
expect(bridge.telemetry.toolMediaUrls).toEqual(["/tmp/reply.opus"]);
|
|
473
|
-
expect(bridge.telemetry.toolAudioAsVoice).toBe(true);
|
|
474
|
-
});
|
|
475
|
-
|
|
476
|
-
it("records messaging tool side effects while returning concise text to app-server", async () => {
|
|
477
|
-
const toolResult = {
|
|
478
|
-
content: [{ type: "text", text: "Sent." }],
|
|
479
|
-
details: { messageId: "message-1" },
|
|
480
|
-
} satisfies AgentToolResult<unknown>;
|
|
481
|
-
const tool = createTool({
|
|
482
|
-
name: "message",
|
|
483
|
-
execute: vi.fn(async () => toolResult),
|
|
484
|
-
});
|
|
485
|
-
const bridge = createCodexDynamicToolBridge({
|
|
486
|
-
tools: [tool],
|
|
487
|
-
signal: new AbortController().signal,
|
|
488
|
-
});
|
|
489
|
-
|
|
490
|
-
const result = await handleMessageToolCall(bridge, {
|
|
491
|
-
action: "send",
|
|
492
|
-
text: "hello from Codex",
|
|
493
|
-
mediaUrl: "/tmp/reply.png",
|
|
494
|
-
provider: "telegram",
|
|
495
|
-
to: "chat-1",
|
|
496
|
-
threadId: "thread-ts-1",
|
|
497
|
-
});
|
|
498
|
-
|
|
499
|
-
expect(result).toEqual(expectInputText("Sent."));
|
|
500
|
-
expect(bridge.telemetry.didSendViaMessagingTool).toBe(true);
|
|
501
|
-
expect(bridge.telemetry.messagingToolSentTexts).toEqual(["hello from Codex"]);
|
|
502
|
-
expect(bridge.telemetry.messagingToolSentMediaUrls).toEqual(["/tmp/reply.png"]);
|
|
503
|
-
expect(bridge.telemetry.messagingToolSentTargets).toEqual([
|
|
504
|
-
{
|
|
505
|
-
tool: "message",
|
|
506
|
-
provider: "telegram",
|
|
507
|
-
to: "chat-1",
|
|
508
|
-
threadId: "thread-ts-1",
|
|
509
|
-
text: "hello from Codex",
|
|
510
|
-
mediaUrls: ["/tmp/reply.png"],
|
|
511
|
-
},
|
|
512
|
-
]);
|
|
513
|
-
});
|
|
514
|
-
|
|
515
|
-
it("records message tool media attachment aliases as delivery evidence", async () => {
|
|
516
|
-
const toolResult = {
|
|
517
|
-
content: [{ type: "text", text: "Sent." }],
|
|
518
|
-
details: { messageId: "message-1" },
|
|
519
|
-
} satisfies AgentToolResult<unknown>;
|
|
520
|
-
const tool = createTool({
|
|
521
|
-
name: "message",
|
|
522
|
-
execute: vi.fn(async () => toolResult),
|
|
523
|
-
});
|
|
524
|
-
const bridge = createCodexDynamicToolBridge({
|
|
525
|
-
tools: [tool],
|
|
526
|
-
signal: new AbortController().signal,
|
|
527
|
-
});
|
|
528
|
-
|
|
529
|
-
const result = await handleMessageToolCall(bridge, {
|
|
530
|
-
action: "send",
|
|
531
|
-
text: "song attached",
|
|
532
|
-
media: "/tmp/generated-song.mp3",
|
|
533
|
-
attachments: [{ filePath: "/tmp/generated-cover.png" }],
|
|
534
|
-
});
|
|
535
|
-
|
|
536
|
-
expect(result).toEqual(expectInputText("Sent."));
|
|
537
|
-
expect(bridge.telemetry.didSendViaMessagingTool).toBe(true);
|
|
538
|
-
expect(bridge.telemetry.messagingToolSentMediaUrls).toEqual([
|
|
539
|
-
"/tmp/generated-song.mp3",
|
|
540
|
-
"/tmp/generated-cover.png",
|
|
541
|
-
]);
|
|
542
|
-
expect(bridge.telemetry.messagingToolSentTargets).toEqual([
|
|
543
|
-
{
|
|
544
|
-
tool: "message",
|
|
545
|
-
provider: "message",
|
|
546
|
-
to: undefined,
|
|
547
|
-
threadId: undefined,
|
|
548
|
-
text: "song attached",
|
|
549
|
-
mediaUrls: ["/tmp/generated-song.mp3", "/tmp/generated-cover.png"],
|
|
550
|
-
},
|
|
551
|
-
]);
|
|
552
|
-
});
|
|
553
|
-
|
|
554
|
-
it("records internal UI source replies separately from outbound messaging evidence", async () => {
|
|
555
|
-
const toolResult = textToolResult("Sent to current chat.", {
|
|
556
|
-
status: "ok",
|
|
557
|
-
deliveryStatus: "sent",
|
|
558
|
-
sourceReplySink: "internal-ui",
|
|
559
|
-
sourceReply: {
|
|
560
|
-
text: "visible reply",
|
|
561
|
-
mediaUrls: ["/tmp/reply.png"],
|
|
562
|
-
},
|
|
563
|
-
});
|
|
564
|
-
const bridge = createBridgeWithToolResult("message", toolResult);
|
|
565
|
-
|
|
566
|
-
const result = await handleMessageToolCall(bridge, {
|
|
567
|
-
action: "send",
|
|
568
|
-
message: "<think>private</think>visible reply",
|
|
569
|
-
});
|
|
570
|
-
|
|
571
|
-
expect(result).toEqual(expectInputText("Sent to current chat."));
|
|
572
|
-
expect(bridge.telemetry.didSendViaMessagingTool).toBe(true);
|
|
573
|
-
expect(bridge.telemetry.messagingToolSentTexts).toEqual([]);
|
|
574
|
-
expect(bridge.telemetry.messagingToolSentMediaUrls).toEqual([]);
|
|
575
|
-
expect(bridge.telemetry.messagingToolSentTargets).toEqual([]);
|
|
576
|
-
expect(bridge.telemetry.messagingToolSourceReplyPayloads).toEqual([
|
|
577
|
-
{
|
|
578
|
-
text: "visible reply",
|
|
579
|
-
mediaUrl: "/tmp/reply.png",
|
|
580
|
-
mediaUrls: ["/tmp/reply.png"],
|
|
581
|
-
},
|
|
582
|
-
]);
|
|
583
|
-
});
|
|
584
|
-
|
|
585
|
-
it("does not record messaging side effects when the send fails", async () => {
|
|
586
|
-
const tool = createTool({
|
|
587
|
-
name: "message",
|
|
588
|
-
execute: vi.fn(async () => {
|
|
589
|
-
throw new Error("send failed");
|
|
590
|
-
}),
|
|
591
|
-
});
|
|
592
|
-
const bridge = createCodexDynamicToolBridge({
|
|
593
|
-
tools: [tool],
|
|
594
|
-
signal: new AbortController().signal,
|
|
595
|
-
});
|
|
596
|
-
|
|
597
|
-
const result = await handleMessageToolCall(bridge, {
|
|
598
|
-
action: "send",
|
|
599
|
-
text: "not delivered",
|
|
600
|
-
provider: "slack",
|
|
601
|
-
to: "C123",
|
|
602
|
-
});
|
|
603
|
-
|
|
604
|
-
expect(result).toEqual({
|
|
605
|
-
success: false,
|
|
606
|
-
contentItems: [{ type: "inputText", text: "send failed" }],
|
|
607
|
-
});
|
|
608
|
-
expect(bridge.telemetry.didSendViaMessagingTool).toBe(false);
|
|
609
|
-
expect(bridge.telemetry.messagingToolSentTexts).toEqual([]);
|
|
610
|
-
expect(bridge.telemetry.messagingToolSentMediaUrls).toEqual([]);
|
|
611
|
-
expect(bridge.telemetry.messagingToolSentTargets).toEqual([]);
|
|
612
|
-
});
|
|
613
|
-
|
|
614
|
-
it("records heartbeat response tool outcomes", async () => {
|
|
615
|
-
const bridge = createBridgeWithToolResult(
|
|
616
|
-
HEARTBEAT_RESPONSE_TOOL_NAME,
|
|
617
|
-
textToolResult("Recorded.", {
|
|
618
|
-
status: "recorded",
|
|
619
|
-
outcome: "needs_attention",
|
|
620
|
-
notify: true,
|
|
621
|
-
summary: "Build is blocked.",
|
|
622
|
-
notificationText: "Build is blocked on missing credentials.",
|
|
623
|
-
priority: "high",
|
|
624
|
-
}),
|
|
625
|
-
);
|
|
626
|
-
|
|
627
|
-
const result = await bridge.handleToolCall({
|
|
628
|
-
threadId: "thread-1",
|
|
629
|
-
turnId: "turn-1",
|
|
630
|
-
callId: "call-1",
|
|
631
|
-
namespace: null,
|
|
632
|
-
tool: HEARTBEAT_RESPONSE_TOOL_NAME,
|
|
633
|
-
arguments: {},
|
|
634
|
-
});
|
|
635
|
-
|
|
636
|
-
expect(result).toEqual(expectInputText("Recorded."));
|
|
637
|
-
expect(bridge.telemetry.heartbeatToolResponse).toEqual({
|
|
638
|
-
outcome: "needs_attention",
|
|
639
|
-
notify: true,
|
|
640
|
-
summary: "Build is blocked.",
|
|
641
|
-
notificationText: "Build is blocked on missing credentials.",
|
|
642
|
-
priority: "high",
|
|
643
|
-
});
|
|
644
|
-
});
|
|
645
|
-
|
|
646
|
-
it("applies agent tool result middleware from the active plugin registry", async () => {
|
|
647
|
-
const registry = createEmptyPluginRegistry();
|
|
648
|
-
const handler = vi.fn(
|
|
649
|
-
async (event: { result: AgentToolResult<unknown>; toolName: string }) => ({
|
|
650
|
-
result: {
|
|
651
|
-
...event.result,
|
|
652
|
-
content: [{ type: "text" as const, text: `${event.toolName} compacted` }],
|
|
653
|
-
},
|
|
654
|
-
}),
|
|
655
|
-
);
|
|
656
|
-
registry.agentToolResultMiddlewares.push({
|
|
657
|
-
pluginId: "tokenjuice",
|
|
658
|
-
pluginName: "Tokenjuice",
|
|
659
|
-
rawHandler: handler,
|
|
660
|
-
handler,
|
|
661
|
-
runtimes: ["codex"],
|
|
662
|
-
source: "test",
|
|
663
|
-
});
|
|
664
|
-
setActivePluginRegistry(registry);
|
|
665
|
-
|
|
666
|
-
const bridge = createBridgeWithToolResult("exec", {
|
|
667
|
-
content: [{ type: "text", text: "raw output" }],
|
|
668
|
-
details: {},
|
|
669
|
-
});
|
|
670
|
-
|
|
671
|
-
const result = await bridge.handleToolCall({
|
|
672
|
-
threadId: "thread-1",
|
|
673
|
-
turnId: "turn-1",
|
|
674
|
-
callId: "call-1",
|
|
675
|
-
namespace: null,
|
|
676
|
-
tool: "exec",
|
|
677
|
-
arguments: { command: "git status" },
|
|
678
|
-
});
|
|
679
|
-
|
|
680
|
-
expect(result).toEqual(expectInputText("exec compacted"));
|
|
681
|
-
const event = requireRecord(callArg(handler, 0, 0, "middleware event"), "middleware event");
|
|
682
|
-
expect(event.threadId).toBe("thread-1");
|
|
683
|
-
expect(event.turnId).toBe("turn-1");
|
|
684
|
-
expect(event.toolCallId).toBe("call-1");
|
|
685
|
-
expect(event.toolName).toBe("exec");
|
|
686
|
-
expect(event.args).toEqual({ command: "git status" });
|
|
687
|
-
expectContextFields(callArg(handler, 0, 1, "middleware context"), { runtime: "codex" });
|
|
688
|
-
});
|
|
689
|
-
|
|
690
|
-
it("preserves nested toolResult content after no-op middleware", async () => {
|
|
691
|
-
const registry = createEmptyPluginRegistry();
|
|
692
|
-
const handler = vi.fn(async () => undefined);
|
|
693
|
-
registry.agentToolResultMiddlewares.push({
|
|
694
|
-
pluginId: "tokenjuice",
|
|
695
|
-
pluginName: "Tokenjuice",
|
|
696
|
-
rawHandler: handler,
|
|
697
|
-
handler,
|
|
698
|
-
runtimes: ["codex"],
|
|
699
|
-
source: "test",
|
|
700
|
-
});
|
|
701
|
-
setActivePluginRegistry(registry);
|
|
702
|
-
|
|
703
|
-
const bridge = createBridgeWithToolResult("message", {
|
|
704
|
-
content: [
|
|
705
|
-
{
|
|
706
|
-
type: "toolResult",
|
|
707
|
-
toolUseId: "call-1",
|
|
708
|
-
content: [{ type: "text", text: "message sent: msg_123" }],
|
|
709
|
-
} as never,
|
|
710
|
-
],
|
|
711
|
-
details: { messageId: "msg_123" },
|
|
712
|
-
});
|
|
713
|
-
|
|
714
|
-
const result = await bridge.handleToolCall({
|
|
715
|
-
threadId: "thread-1",
|
|
716
|
-
turnId: "turn-1",
|
|
717
|
-
callId: "call-1",
|
|
718
|
-
namespace: null,
|
|
719
|
-
tool: "message",
|
|
720
|
-
arguments: { text: "hello" },
|
|
721
|
-
});
|
|
722
|
-
|
|
723
|
-
expect(result).toEqual(expectInputText("message sent: msg_123"));
|
|
724
|
-
expect(handler).toHaveBeenCalledTimes(1);
|
|
725
|
-
});
|
|
726
|
-
|
|
727
|
-
it("passes raw tool failure state into agent tool result middleware", async () => {
|
|
728
|
-
const registry = createEmptyPluginRegistry();
|
|
729
|
-
const handler = vi.fn(async (eventValue: { isError?: boolean }) => undefined);
|
|
730
|
-
registry.agentToolResultMiddlewares.push({
|
|
731
|
-
pluginId: "tokenjuice",
|
|
732
|
-
pluginName: "Tokenjuice",
|
|
733
|
-
rawHandler: handler,
|
|
734
|
-
handler,
|
|
735
|
-
runtimes: ["codex"],
|
|
736
|
-
source: "test",
|
|
737
|
-
});
|
|
738
|
-
setActivePluginRegistry(registry);
|
|
739
|
-
|
|
740
|
-
const bridge = createBridgeWithToolResult("exec", {
|
|
741
|
-
content: [{ type: "text", text: "failed output" }],
|
|
742
|
-
details: { status: "failed", exitCode: 1 },
|
|
743
|
-
});
|
|
744
|
-
|
|
745
|
-
const result = await bridge.handleToolCall({
|
|
746
|
-
threadId: "thread-1",
|
|
747
|
-
turnId: "turn-1",
|
|
748
|
-
callId: "call-1",
|
|
749
|
-
namespace: null,
|
|
750
|
-
tool: "exec",
|
|
751
|
-
arguments: { command: "false" },
|
|
752
|
-
});
|
|
753
|
-
|
|
754
|
-
expect(result).toEqual({
|
|
755
|
-
success: false,
|
|
756
|
-
contentItems: [{ type: "inputText", text: "failed output" }],
|
|
757
|
-
});
|
|
758
|
-
const event = requireRecord(callArg(handler, 0, 0, "middleware event"), "middleware event");
|
|
759
|
-
expect(event.isError).toBe(true);
|
|
760
|
-
expectContextFields(callArg(handler, 0, 1, "middleware context"), { runtime: "codex" });
|
|
761
|
-
});
|
|
762
|
-
|
|
763
|
-
it("uses raw tool provenance for media trust after middleware rewrites details", async () => {
|
|
764
|
-
const registry = createEmptyPluginRegistry();
|
|
765
|
-
const handler = vi.fn(async (event: { result: AgentToolResult<unknown> }) => ({
|
|
766
|
-
result: {
|
|
767
|
-
...event.result,
|
|
768
|
-
content: [{ type: "text" as const, text: "Generated media reply." }],
|
|
769
|
-
details: {
|
|
770
|
-
media: {
|
|
771
|
-
mediaUrl: "/tmp/unsafe.png",
|
|
772
|
-
},
|
|
773
|
-
},
|
|
774
|
-
},
|
|
775
|
-
}));
|
|
776
|
-
registry.agentToolResultMiddlewares.push({
|
|
777
|
-
pluginId: "tokenjuice",
|
|
778
|
-
pluginName: "Tokenjuice",
|
|
779
|
-
rawHandler: handler,
|
|
780
|
-
handler,
|
|
781
|
-
runtimes: ["codex"],
|
|
782
|
-
source: "test",
|
|
783
|
-
});
|
|
784
|
-
setActivePluginRegistry(registry);
|
|
785
|
-
|
|
786
|
-
const bridge = createBridgeWithToolResult("browser", {
|
|
787
|
-
content: [{ type: "text", text: "raw output" }],
|
|
788
|
-
details: {
|
|
789
|
-
mcpServer: "external",
|
|
790
|
-
mcpTool: "browser",
|
|
791
|
-
},
|
|
792
|
-
});
|
|
793
|
-
|
|
794
|
-
const result = await bridge.handleToolCall({
|
|
795
|
-
threadId: "thread-1",
|
|
796
|
-
turnId: "turn-1",
|
|
797
|
-
callId: "call-1",
|
|
798
|
-
namespace: null,
|
|
799
|
-
tool: "browser",
|
|
800
|
-
arguments: {},
|
|
801
|
-
});
|
|
802
|
-
|
|
803
|
-
expect(result).toEqual(expectInputText("Generated media reply."));
|
|
804
|
-
expect(bridge.telemetry.toolMediaUrls).toStrictEqual([]);
|
|
805
|
-
});
|
|
806
|
-
|
|
807
|
-
it("still applies legacy codex app-server extension factories after middleware", async () => {
|
|
808
|
-
const registry = createEmptyPluginRegistry();
|
|
809
|
-
const factory = async (codex: {
|
|
810
|
-
on: (
|
|
811
|
-
event: "tool_result",
|
|
812
|
-
handler: (event: any) => Promise<{ result: AgentToolResult<unknown> }>,
|
|
813
|
-
) => void;
|
|
814
|
-
}) => {
|
|
815
|
-
codex.on("tool_result", async (event) => ({
|
|
816
|
-
result: {
|
|
817
|
-
...event.result,
|
|
818
|
-
content: [{ type: "text", text: "legacy compacted" }],
|
|
819
|
-
},
|
|
820
|
-
}));
|
|
821
|
-
};
|
|
822
|
-
registry.codexAppServerExtensionFactories.push({
|
|
823
|
-
pluginId: "tokenjuice",
|
|
824
|
-
pluginName: "Tokenjuice",
|
|
825
|
-
rawFactory: factory,
|
|
826
|
-
factory,
|
|
827
|
-
source: "test",
|
|
828
|
-
});
|
|
829
|
-
setActivePluginRegistry(registry);
|
|
830
|
-
|
|
831
|
-
const bridge = createBridgeWithToolResult("exec", {
|
|
832
|
-
content: [{ type: "text", text: "raw output" }],
|
|
833
|
-
details: {},
|
|
834
|
-
});
|
|
835
|
-
|
|
836
|
-
const result = await bridge.handleToolCall({
|
|
837
|
-
threadId: "thread-1",
|
|
838
|
-
turnId: "turn-1",
|
|
839
|
-
callId: "call-1",
|
|
840
|
-
namespace: null,
|
|
841
|
-
tool: "exec",
|
|
842
|
-
arguments: { command: "git status" },
|
|
843
|
-
});
|
|
844
|
-
|
|
845
|
-
expect(result).toEqual(expectInputText("legacy compacted"));
|
|
846
|
-
});
|
|
847
|
-
|
|
848
|
-
it("keeps config out of Codex tool-result contexts", async () => {
|
|
849
|
-
const config = { session: { store: "/tmp/klaw-session-store.json" } };
|
|
850
|
-
const registry = createEmptyPluginRegistry();
|
|
851
|
-
const middlewareContexts: Record<string, unknown>[] = [];
|
|
852
|
-
const legacyContexts: Record<string, unknown>[] = [];
|
|
853
|
-
const middleware = vi.fn(async (eventValue: unknown, ctx: Record<string, unknown>) => {
|
|
854
|
-
middlewareContexts.push(ctx);
|
|
855
|
-
return undefined;
|
|
856
|
-
});
|
|
857
|
-
const factory = async (codex: {
|
|
858
|
-
on: (
|
|
859
|
-
event: "tool_result",
|
|
860
|
-
handler: (
|
|
861
|
-
event: unknown,
|
|
862
|
-
ctx: Record<string, unknown>,
|
|
863
|
-
) => Promise<{ result: AgentToolResult<unknown> } | void>,
|
|
864
|
-
) => void;
|
|
865
|
-
}) => {
|
|
866
|
-
codex.on("tool_result", async (eventValue, ctx) => {
|
|
867
|
-
legacyContexts.push(ctx);
|
|
868
|
-
});
|
|
869
|
-
};
|
|
870
|
-
registry.agentToolResultMiddlewares.push({
|
|
871
|
-
pluginId: "tokenjuice",
|
|
872
|
-
pluginName: "Tokenjuice",
|
|
873
|
-
rawHandler: middleware,
|
|
874
|
-
handler: middleware,
|
|
875
|
-
runtimes: ["codex"],
|
|
876
|
-
source: "test",
|
|
877
|
-
});
|
|
878
|
-
registry.codexAppServerExtensionFactories.push({
|
|
879
|
-
pluginId: "legacy",
|
|
880
|
-
pluginName: "Legacy",
|
|
881
|
-
rawFactory: factory,
|
|
882
|
-
factory,
|
|
883
|
-
source: "test",
|
|
884
|
-
});
|
|
885
|
-
setActivePluginRegistry(registry);
|
|
886
|
-
|
|
887
|
-
const execute = vi.fn(async () => textToolResult("done"));
|
|
888
|
-
const bridge = createCodexDynamicToolBridge({
|
|
889
|
-
tools: [createTool({ name: "exec", execute })],
|
|
890
|
-
signal: new AbortController().signal,
|
|
891
|
-
hookContext: {
|
|
892
|
-
agentId: "agent-1",
|
|
893
|
-
config: config as never,
|
|
894
|
-
sessionId: "session-1",
|
|
895
|
-
sessionKey: "agent:agent-1:session-1",
|
|
896
|
-
runId: "run-1",
|
|
897
|
-
},
|
|
898
|
-
});
|
|
899
|
-
|
|
900
|
-
await bridge.handleToolCall({
|
|
901
|
-
threadId: "thread-1",
|
|
902
|
-
turnId: "turn-1",
|
|
903
|
-
callId: "call-1",
|
|
904
|
-
namespace: null,
|
|
905
|
-
tool: "exec",
|
|
906
|
-
arguments: { command: "pwd" },
|
|
907
|
-
});
|
|
908
|
-
|
|
909
|
-
expectExecuteCall(execute, { callId: "call-1", args: { command: "pwd" } });
|
|
910
|
-
expect(middlewareContexts).toHaveLength(1);
|
|
911
|
-
expectContextFields(middlewareContexts[0], {
|
|
912
|
-
runtime: "codex",
|
|
913
|
-
agentId: "agent-1",
|
|
914
|
-
sessionId: "session-1",
|
|
915
|
-
sessionKey: "agent:agent-1:session-1",
|
|
916
|
-
runId: "run-1",
|
|
917
|
-
});
|
|
918
|
-
expect(middlewareContexts[0]).not.toHaveProperty("config");
|
|
919
|
-
expect(legacyContexts).toHaveLength(1);
|
|
920
|
-
expectContextFields(legacyContexts[0], {
|
|
921
|
-
agentId: "agent-1",
|
|
922
|
-
sessionId: "session-1",
|
|
923
|
-
sessionKey: "agent:agent-1:session-1",
|
|
924
|
-
runId: "run-1",
|
|
925
|
-
});
|
|
926
|
-
expect(legacyContexts[0]).not.toHaveProperty("config");
|
|
927
|
-
});
|
|
928
|
-
|
|
929
|
-
it("fires after_tool_call for successful codex tool executions", async () => {
|
|
930
|
-
const afterToolCall = vi.fn();
|
|
931
|
-
initializeGlobalHookRunner(
|
|
932
|
-
createMockPluginRegistry([{ hookName: "after_tool_call", handler: afterToolCall }]),
|
|
933
|
-
);
|
|
934
|
-
|
|
935
|
-
const bridge = createBridgeWithToolResult(
|
|
936
|
-
"exec",
|
|
937
|
-
{
|
|
938
|
-
content: [{ type: "text", text: "done" }],
|
|
939
|
-
details: {},
|
|
940
|
-
},
|
|
941
|
-
{
|
|
942
|
-
agentId: "agent-1",
|
|
943
|
-
sessionId: "session-1",
|
|
944
|
-
sessionKey: "agent:agent-1:session-1",
|
|
945
|
-
runId: "run-1",
|
|
946
|
-
channelId: "voice-room",
|
|
947
|
-
},
|
|
948
|
-
);
|
|
949
|
-
|
|
950
|
-
await bridge.handleToolCall({
|
|
951
|
-
threadId: "thread-1",
|
|
952
|
-
turnId: "turn-1",
|
|
953
|
-
callId: "call-1",
|
|
954
|
-
namespace: null,
|
|
955
|
-
tool: "exec",
|
|
956
|
-
arguments: { command: "pwd" },
|
|
957
|
-
});
|
|
958
|
-
|
|
959
|
-
await vi.waitFor(() => {
|
|
960
|
-
expect(afterToolCall).toHaveBeenCalledTimes(1);
|
|
961
|
-
});
|
|
962
|
-
const event = requireRecord(callArg(afterToolCall, 0, 0, "after_tool_call event"), "event");
|
|
963
|
-
expect(event.toolName).toBe("exec");
|
|
964
|
-
expect(event.toolCallId).toBe("call-1");
|
|
965
|
-
expect(event.params).toEqual({ command: "pwd" });
|
|
966
|
-
expectToolResult(event.result, {
|
|
967
|
-
content: [{ type: "text", text: "done" }],
|
|
968
|
-
details: {},
|
|
969
|
-
});
|
|
970
|
-
expectContextFields(callArg(afterToolCall, 0, 1, "after_tool_call context"), {
|
|
971
|
-
agentId: "agent-1",
|
|
972
|
-
sessionId: "session-1",
|
|
973
|
-
sessionKey: "agent:agent-1:session-1",
|
|
974
|
-
runId: "run-1",
|
|
975
|
-
channelId: "voice-room",
|
|
976
|
-
toolName: "exec",
|
|
977
|
-
toolCallId: "call-1",
|
|
978
|
-
});
|
|
979
|
-
});
|
|
980
|
-
|
|
981
|
-
it("runs before_tool_call for unwrapped dynamic tools before execution", async () => {
|
|
982
|
-
const beforeToolCall = vi.fn(async () => ({ params: { mode: "safe" } }));
|
|
983
|
-
const afterToolCall = vi.fn();
|
|
984
|
-
initializeGlobalHookRunner(
|
|
985
|
-
createMockPluginRegistry([
|
|
986
|
-
{ hookName: "before_tool_call", handler: beforeToolCall },
|
|
987
|
-
{ hookName: "after_tool_call", handler: afterToolCall },
|
|
988
|
-
]),
|
|
989
|
-
);
|
|
990
|
-
|
|
991
|
-
const execute = vi.fn(async () => textToolResult("done", { ok: true }));
|
|
992
|
-
const bridge = createCodexDynamicToolBridge({
|
|
993
|
-
tools: [createTool({ name: "exec", execute })],
|
|
994
|
-
signal: new AbortController().signal,
|
|
995
|
-
hookContext: {
|
|
996
|
-
agentId: "agent-1",
|
|
997
|
-
sessionId: "session-1",
|
|
998
|
-
sessionKey: "agent:agent-1:session-1",
|
|
999
|
-
runId: "run-1",
|
|
1000
|
-
},
|
|
1001
|
-
});
|
|
1002
|
-
|
|
1003
|
-
const result = await bridge.handleToolCall({
|
|
1004
|
-
threadId: "thread-1",
|
|
1005
|
-
turnId: "turn-1",
|
|
1006
|
-
callId: "call-1",
|
|
1007
|
-
namespace: null,
|
|
1008
|
-
tool: "exec",
|
|
1009
|
-
arguments: { command: "pwd" },
|
|
1010
|
-
});
|
|
1011
|
-
|
|
1012
|
-
expect(result).toEqual(expectInputText("done"));
|
|
1013
|
-
const beforeEvent = requireRecord(
|
|
1014
|
-
callArg(beforeToolCall, 0, 0, "before_tool_call event"),
|
|
1015
|
-
"before event",
|
|
1016
|
-
);
|
|
1017
|
-
expect(beforeEvent.toolName).toBe("exec");
|
|
1018
|
-
expect(beforeEvent.toolCallId).toBe("call-1");
|
|
1019
|
-
expect(beforeEvent.runId).toBe("run-1");
|
|
1020
|
-
expect(beforeEvent.params).toEqual({ command: "pwd" });
|
|
1021
|
-
expectContextFields(callArg(beforeToolCall, 0, 1, "before_tool_call context"), {
|
|
1022
|
-
agentId: "agent-1",
|
|
1023
|
-
sessionId: "session-1",
|
|
1024
|
-
sessionKey: "agent:agent-1:session-1",
|
|
1025
|
-
runId: "run-1",
|
|
1026
|
-
toolCallId: "call-1",
|
|
1027
|
-
});
|
|
1028
|
-
expectExecuteCall(execute, { callId: "call-1", args: { command: "pwd", mode: "safe" } });
|
|
1029
|
-
await vi.waitFor(() => {
|
|
1030
|
-
expect(afterToolCall).toHaveBeenCalledTimes(1);
|
|
1031
|
-
});
|
|
1032
|
-
const afterEvent = requireRecord(
|
|
1033
|
-
callArg(afterToolCall, 0, 0, "after_tool_call event"),
|
|
1034
|
-
"after event",
|
|
1035
|
-
);
|
|
1036
|
-
expect(afterEvent.toolName).toBe("exec");
|
|
1037
|
-
expect(afterEvent.toolCallId).toBe("call-1");
|
|
1038
|
-
expect(afterEvent.params).toEqual({ command: "pwd", mode: "safe" });
|
|
1039
|
-
expectToolResult(afterEvent.result, {
|
|
1040
|
-
content: [{ type: "text", text: "done" }],
|
|
1041
|
-
details: { ok: true },
|
|
1042
|
-
});
|
|
1043
|
-
expectContextFields(callArg(afterToolCall, 0, 1, "after_tool_call context"), {
|
|
1044
|
-
agentId: "agent-1",
|
|
1045
|
-
sessionId: "session-1",
|
|
1046
|
-
sessionKey: "agent:agent-1:session-1",
|
|
1047
|
-
runId: "run-1",
|
|
1048
|
-
toolCallId: "call-1",
|
|
1049
|
-
});
|
|
1050
|
-
});
|
|
1051
|
-
|
|
1052
|
-
it("does not execute dynamic tools blocked by before_tool_call", async () => {
|
|
1053
|
-
const beforeToolCall = vi.fn(async () => ({
|
|
1054
|
-
block: true,
|
|
1055
|
-
blockReason: "blocked by policy",
|
|
1056
|
-
}));
|
|
1057
|
-
const afterToolCall = vi.fn();
|
|
1058
|
-
initializeGlobalHookRunner(
|
|
1059
|
-
createMockPluginRegistry([
|
|
1060
|
-
{ hookName: "before_tool_call", handler: beforeToolCall },
|
|
1061
|
-
{ hookName: "after_tool_call", handler: afterToolCall },
|
|
1062
|
-
]),
|
|
1063
|
-
);
|
|
1064
|
-
const execute = vi.fn(async () => textToolResult("should not run"));
|
|
1065
|
-
const bridge = createCodexDynamicToolBridge({
|
|
1066
|
-
tools: [createTool({ name: "message", execute })],
|
|
1067
|
-
signal: new AbortController().signal,
|
|
1068
|
-
hookContext: { runId: "run-blocked" },
|
|
1069
|
-
});
|
|
1070
|
-
|
|
1071
|
-
const result = await handleMessageToolCall(bridge, {
|
|
1072
|
-
action: "send",
|
|
1073
|
-
text: "blocked",
|
|
1074
|
-
provider: "telegram",
|
|
1075
|
-
to: "chat-1",
|
|
1076
|
-
});
|
|
1077
|
-
|
|
1078
|
-
expect(result).toEqual({
|
|
1079
|
-
success: false,
|
|
1080
|
-
contentItems: [{ type: "inputText", text: "blocked by policy" }],
|
|
1081
|
-
});
|
|
1082
|
-
expect(execute).not.toHaveBeenCalled();
|
|
1083
|
-
expect(bridge.telemetry.didSendViaMessagingTool).toBe(false);
|
|
1084
|
-
await vi.waitFor(() => {
|
|
1085
|
-
expect(afterToolCall).toHaveBeenCalledTimes(1);
|
|
1086
|
-
});
|
|
1087
|
-
const event = requireRecord(callArg(afterToolCall, 0, 0, "after_tool_call event"), "event");
|
|
1088
|
-
expect(event.toolName).toBe("message");
|
|
1089
|
-
expect(event.toolCallId).toBe("call-1");
|
|
1090
|
-
expect(event.params).toEqual({
|
|
1091
|
-
action: "send",
|
|
1092
|
-
text: "blocked",
|
|
1093
|
-
provider: "telegram",
|
|
1094
|
-
to: "chat-1",
|
|
1095
|
-
});
|
|
1096
|
-
expectToolResult(event.result, {
|
|
1097
|
-
content: [{ type: "text", text: "blocked by policy" }],
|
|
1098
|
-
details: {
|
|
1099
|
-
status: "blocked",
|
|
1100
|
-
deniedReason: "plugin-before-tool-call",
|
|
1101
|
-
reason: "blocked by policy",
|
|
1102
|
-
},
|
|
1103
|
-
});
|
|
1104
|
-
expectContextFields(callArg(afterToolCall, 0, 1, "after_tool_call context"), {
|
|
1105
|
-
runId: "run-blocked",
|
|
1106
|
-
toolCallId: "call-1",
|
|
1107
|
-
});
|
|
1108
|
-
});
|
|
1109
|
-
|
|
1110
|
-
it("applies dynamic tool result middleware before after_tool_call observes the result", async () => {
|
|
1111
|
-
const events: string[] = [];
|
|
1112
|
-
const beforeToolCall = vi.fn(async () => {
|
|
1113
|
-
events.push("before_tool_call");
|
|
1114
|
-
return { params: { mode: "safe" } };
|
|
1115
|
-
});
|
|
1116
|
-
const afterToolCall = vi.fn(async (event) => {
|
|
1117
|
-
events.push("after_tool_call");
|
|
1118
|
-
const record = requireRecord(event, "after_tool_call event");
|
|
1119
|
-
expect(record.params).toEqual({ command: "status", mode: "safe" });
|
|
1120
|
-
expectToolResult(record.result, {
|
|
1121
|
-
content: [{ type: "text", text: "compacted output" }],
|
|
1122
|
-
details: { stage: "middleware" },
|
|
1123
|
-
});
|
|
1124
|
-
});
|
|
1125
|
-
initializeGlobalHookRunner(
|
|
1126
|
-
createMockPluginRegistry([
|
|
1127
|
-
{ hookName: "before_tool_call", handler: beforeToolCall },
|
|
1128
|
-
{ hookName: "after_tool_call", handler: afterToolCall },
|
|
1129
|
-
]),
|
|
1130
|
-
);
|
|
1131
|
-
const registry = createEmptyPluginRegistry();
|
|
1132
|
-
const handler = vi.fn(
|
|
1133
|
-
async (event: { args: Record<string, unknown>; result: AgentToolResult<unknown> }) => {
|
|
1134
|
-
events.push("middleware");
|
|
1135
|
-
expect(event.args).toEqual({ command: "status" });
|
|
1136
|
-
return {
|
|
1137
|
-
result: {
|
|
1138
|
-
...event.result,
|
|
1139
|
-
content: [{ type: "text" as const, text: "compacted output" }],
|
|
1140
|
-
details: { stage: "middleware" },
|
|
1141
|
-
},
|
|
1142
|
-
};
|
|
1143
|
-
},
|
|
1144
|
-
);
|
|
1145
|
-
registry.agentToolResultMiddlewares.push({
|
|
1146
|
-
pluginId: "tokenjuice",
|
|
1147
|
-
pluginName: "Tokenjuice",
|
|
1148
|
-
rawHandler: handler,
|
|
1149
|
-
handler,
|
|
1150
|
-
runtimes: ["codex"],
|
|
1151
|
-
source: "test",
|
|
1152
|
-
});
|
|
1153
|
-
setActivePluginRegistry(registry);
|
|
1154
|
-
const execute = vi.fn(async () => {
|
|
1155
|
-
events.push("execute");
|
|
1156
|
-
return textToolResult("raw output", { stage: "execute" });
|
|
1157
|
-
});
|
|
1158
|
-
const bridge = createCodexDynamicToolBridge({
|
|
1159
|
-
tools: [createTool({ name: "exec", execute })],
|
|
1160
|
-
signal: new AbortController().signal,
|
|
1161
|
-
hookContext: { runId: "run-middleware" },
|
|
1162
|
-
});
|
|
1163
|
-
|
|
1164
|
-
const result = await bridge.handleToolCall({
|
|
1165
|
-
threadId: "thread-1",
|
|
1166
|
-
turnId: "turn-1",
|
|
1167
|
-
callId: "call-1",
|
|
1168
|
-
namespace: null,
|
|
1169
|
-
tool: "exec",
|
|
1170
|
-
arguments: { command: "status" },
|
|
1171
|
-
});
|
|
1172
|
-
|
|
1173
|
-
expect(result).toEqual(expectInputText("compacted output"));
|
|
1174
|
-
await vi.waitFor(() => {
|
|
1175
|
-
expect(events).toEqual(["before_tool_call", "execute", "middleware", "after_tool_call"]);
|
|
1176
|
-
});
|
|
1177
|
-
});
|
|
1178
|
-
|
|
1179
|
-
it("reports dynamic tool execution errors through after_tool_call without stranding the turn", async () => {
|
|
1180
|
-
const beforeToolCall = vi.fn(async () => ({ params: { timeoutSec: 1 } }));
|
|
1181
|
-
const afterToolCall = vi.fn();
|
|
1182
|
-
initializeGlobalHookRunner(
|
|
1183
|
-
createMockPluginRegistry([
|
|
1184
|
-
{ hookName: "before_tool_call", handler: beforeToolCall },
|
|
1185
|
-
{ hookName: "after_tool_call", handler: afterToolCall },
|
|
1186
|
-
]),
|
|
1187
|
-
);
|
|
1188
|
-
const execute = vi.fn(async () => {
|
|
1189
|
-
throw new Error("tool failed");
|
|
1190
|
-
});
|
|
1191
|
-
const bridge = createCodexDynamicToolBridge({
|
|
1192
|
-
tools: [createTool({ name: "exec", execute })],
|
|
1193
|
-
signal: new AbortController().signal,
|
|
1194
|
-
hookContext: { runId: "run-error" },
|
|
1195
|
-
});
|
|
1196
|
-
|
|
1197
|
-
const result = await bridge.handleToolCall({
|
|
1198
|
-
threadId: "thread-1",
|
|
1199
|
-
turnId: "turn-1",
|
|
1200
|
-
callId: "call-err",
|
|
1201
|
-
namespace: null,
|
|
1202
|
-
tool: "exec",
|
|
1203
|
-
arguments: { command: "false" },
|
|
1204
|
-
});
|
|
1205
|
-
|
|
1206
|
-
expect(result).toEqual({
|
|
1207
|
-
success: false,
|
|
1208
|
-
contentItems: [{ type: "inputText", text: "tool failed" }],
|
|
1209
|
-
});
|
|
1210
|
-
expectExecuteCall(execute, {
|
|
1211
|
-
callId: "call-err",
|
|
1212
|
-
args: { command: "false", timeoutSec: 1 },
|
|
1213
|
-
});
|
|
1214
|
-
await vi.waitFor(() => {
|
|
1215
|
-
expect(afterToolCall).toHaveBeenCalledTimes(1);
|
|
1216
|
-
});
|
|
1217
|
-
const event = requireRecord(callArg(afterToolCall, 0, 0, "after_tool_call event"), "event");
|
|
1218
|
-
expect(event.toolName).toBe("exec");
|
|
1219
|
-
expect(event.toolCallId).toBe("call-err");
|
|
1220
|
-
expect(event.params).toEqual({ command: "false", timeoutSec: 1 });
|
|
1221
|
-
expect(event.error).toBe("tool failed");
|
|
1222
|
-
expectContextFields(callArg(afterToolCall, 0, 1, "after_tool_call context"), {
|
|
1223
|
-
runId: "run-error",
|
|
1224
|
-
toolCallId: "call-err",
|
|
1225
|
-
});
|
|
1226
|
-
});
|
|
1227
|
-
|
|
1228
|
-
it("passes per-call abort signals into dynamic tool execution", async () => {
|
|
1229
|
-
let capturedSignal: AbortSignal | undefined;
|
|
1230
|
-
let resolveTool: ((result: AgentToolResult<unknown>) => void) | undefined;
|
|
1231
|
-
const execute = vi.fn(
|
|
1232
|
-
async (_callId: string, _args: Record<string, unknown>, signal: AbortSignal) =>
|
|
1233
|
-
await new Promise<AgentToolResult<unknown>>((resolve) => {
|
|
1234
|
-
capturedSignal = signal;
|
|
1235
|
-
resolveTool = resolve;
|
|
1236
|
-
}),
|
|
1237
|
-
);
|
|
1238
|
-
const runController = new AbortController();
|
|
1239
|
-
const callController = new AbortController();
|
|
1240
|
-
const bridge = createCodexDynamicToolBridge({
|
|
1241
|
-
tools: [createTool({ name: "exec", execute })],
|
|
1242
|
-
signal: runController.signal,
|
|
1243
|
-
});
|
|
1244
|
-
|
|
1245
|
-
const result = bridge.handleToolCall(
|
|
1246
|
-
{
|
|
1247
|
-
threadId: "thread-1",
|
|
1248
|
-
turnId: "turn-1",
|
|
1249
|
-
callId: "call-signal",
|
|
1250
|
-
namespace: null,
|
|
1251
|
-
tool: "exec",
|
|
1252
|
-
arguments: { command: "sleep" },
|
|
1253
|
-
},
|
|
1254
|
-
{ signal: callController.signal },
|
|
1255
|
-
);
|
|
1256
|
-
await vi.waitFor(() => {
|
|
1257
|
-
if (!capturedSignal) {
|
|
1258
|
-
throw new Error("expected dynamic tool call signal");
|
|
1259
|
-
}
|
|
1260
|
-
});
|
|
1261
|
-
if (!capturedSignal) {
|
|
1262
|
-
throw new Error("expected dynamic tool call signal");
|
|
1263
|
-
}
|
|
1264
|
-
|
|
1265
|
-
callController.abort(new Error("deadline"));
|
|
1266
|
-
expect(capturedSignal.aborted).toBe(true);
|
|
1267
|
-
resolveTool?.(textToolResult("done"));
|
|
1268
|
-
|
|
1269
|
-
await expect(result).resolves.toEqual(expectInputText("done"));
|
|
1270
|
-
});
|
|
1271
|
-
|
|
1272
|
-
it("does not double-wrap dynamic tools that already have before_tool_call", async () => {
|
|
1273
|
-
const beforeToolCall = vi.fn(async () => ({ params: { mode: "safe" } }));
|
|
1274
|
-
initializeGlobalHookRunner(
|
|
1275
|
-
createMockPluginRegistry([{ hookName: "before_tool_call", handler: beforeToolCall }]),
|
|
1276
|
-
);
|
|
1277
|
-
const execute = vi.fn(async () => textToolResult("done"));
|
|
1278
|
-
const tool = wrapToolWithBeforeToolCallHook(createTool({ name: "exec", execute }), {
|
|
1279
|
-
runId: "run-wrapped",
|
|
1280
|
-
});
|
|
1281
|
-
const bridge = createCodexDynamicToolBridge({
|
|
1282
|
-
tools: [tool],
|
|
1283
|
-
signal: new AbortController().signal,
|
|
1284
|
-
hookContext: { runId: "run-wrapped" },
|
|
1285
|
-
});
|
|
1286
|
-
|
|
1287
|
-
await bridge.handleToolCall({
|
|
1288
|
-
threadId: "thread-1",
|
|
1289
|
-
turnId: "turn-1",
|
|
1290
|
-
callId: "call-wrapped",
|
|
1291
|
-
namespace: null,
|
|
1292
|
-
tool: "exec",
|
|
1293
|
-
arguments: { command: "pwd" },
|
|
1294
|
-
});
|
|
1295
|
-
|
|
1296
|
-
expect(beforeToolCall).toHaveBeenCalledTimes(1);
|
|
1297
|
-
expectExecuteCall(execute, {
|
|
1298
|
-
callId: "call-wrapped",
|
|
1299
|
-
args: { command: "pwd", mode: "safe" },
|
|
1300
|
-
});
|
|
1301
|
-
});
|
|
1302
|
-
});
|