@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.
Files changed (138) hide show
  1. package/package.json +16 -1
  2. package/doctor-contract-api.test.ts +0 -44
  3. package/doctor-contract-api.ts +0 -68
  4. package/harness.ts +0 -72
  5. package/index.test.ts +0 -230
  6. package/index.ts +0 -66
  7. package/media-understanding-provider.test.ts +0 -486
  8. package/media-understanding-provider.ts +0 -521
  9. package/prompt-overlay-runtime-contract.test.ts +0 -48
  10. package/prompt-overlay.ts +0 -21
  11. package/provider-catalog.ts +0 -83
  12. package/provider-discovery.ts +0 -45
  13. package/provider.test.ts +0 -384
  14. package/provider.ts +0 -243
  15. package/src/app-server/app-inventory-cache.test.ts +0 -176
  16. package/src/app-server/app-inventory-cache.ts +0 -324
  17. package/src/app-server/approval-bridge.test.ts +0 -1471
  18. package/src/app-server/approval-bridge.ts +0 -1211
  19. package/src/app-server/auth-bridge.test.ts +0 -1449
  20. package/src/app-server/auth-bridge.ts +0 -614
  21. package/src/app-server/auth-profile-runtime-contract.test.ts +0 -239
  22. package/src/app-server/capabilities.ts +0 -27
  23. package/src/app-server/client-factory.ts +0 -24
  24. package/src/app-server/client.test.ts +0 -563
  25. package/src/app-server/client.ts +0 -715
  26. package/src/app-server/compact.test.ts +0 -710
  27. package/src/app-server/compact.ts +0 -500
  28. package/src/app-server/computer-use.test.ts +0 -788
  29. package/src/app-server/computer-use.ts +0 -683
  30. package/src/app-server/config.test.ts +0 -879
  31. package/src/app-server/config.ts +0 -1038
  32. package/src/app-server/context-engine-projection.test.ts +0 -252
  33. package/src/app-server/context-engine-projection.ts +0 -403
  34. package/src/app-server/delivery-no-reply-runtime-contract.test.ts +0 -80
  35. package/src/app-server/dynamic-tool-diagnostics.ts +0 -73
  36. package/src/app-server/dynamic-tool-profile.ts +0 -69
  37. package/src/app-server/dynamic-tools.test.ts +0 -1302
  38. package/src/app-server/dynamic-tools.ts +0 -623
  39. package/src/app-server/elicitation-bridge.test.ts +0 -1056
  40. package/src/app-server/elicitation-bridge.ts +0 -783
  41. package/src/app-server/event-projector.test.ts +0 -2668
  42. package/src/app-server/event-projector.ts +0 -2057
  43. package/src/app-server/image-payload-sanitizer.test.ts +0 -49
  44. package/src/app-server/image-payload-sanitizer.ts +0 -167
  45. package/src/app-server/klaw-owned-tool-runtime-contract.test.ts +0 -456
  46. package/src/app-server/local-runtime-attribution.ts +0 -39
  47. package/src/app-server/managed-binary.test.ts +0 -139
  48. package/src/app-server/managed-binary.ts +0 -193
  49. package/src/app-server/models.test.ts +0 -246
  50. package/src/app-server/models.ts +0 -172
  51. package/src/app-server/native-hook-relay.test.ts +0 -271
  52. package/src/app-server/native-hook-relay.ts +0 -150
  53. package/src/app-server/native-subagent-task-mirror.test.ts +0 -573
  54. package/src/app-server/native-subagent-task-mirror.ts +0 -497
  55. package/src/app-server/outcome-fallback-runtime-contract.test.ts +0 -404
  56. package/src/app-server/plugin-activation.test.ts +0 -336
  57. package/src/app-server/plugin-activation.ts +0 -283
  58. package/src/app-server/plugin-app-cache-key.ts +0 -74
  59. package/src/app-server/plugin-approval-roundtrip.ts +0 -122
  60. package/src/app-server/plugin-inventory.test.ts +0 -355
  61. package/src/app-server/plugin-inventory.ts +0 -357
  62. package/src/app-server/plugin-thread-config.test.ts +0 -865
  63. package/src/app-server/plugin-thread-config.ts +0 -455
  64. package/src/app-server/protocol-generated/json/DynamicToolCallParams.json +0 -33
  65. package/src/app-server/protocol-generated/json/v2/ErrorNotification.json +0 -199
  66. package/src/app-server/protocol-generated/json/v2/GetAccountResponse.json +0 -102
  67. package/src/app-server/protocol-generated/json/v2/ModelListResponse.json +0 -227
  68. package/src/app-server/protocol-generated/json/v2/ThreadResumeResponse.json +0 -2630
  69. package/src/app-server/protocol-generated/json/v2/ThreadStartResponse.json +0 -2630
  70. package/src/app-server/protocol-generated/json/v2/TurnCompletedNotification.json +0 -1659
  71. package/src/app-server/protocol-generated/json/v2/TurnStartResponse.json +0 -1655
  72. package/src/app-server/protocol-validators.test.ts +0 -75
  73. package/src/app-server/protocol-validators.ts +0 -203
  74. package/src/app-server/protocol.ts +0 -520
  75. package/src/app-server/rate-limit-cache.ts +0 -48
  76. package/src/app-server/rate-limits.test.ts +0 -202
  77. package/src/app-server/rate-limits.ts +0 -583
  78. package/src/app-server/request.ts +0 -73
  79. package/src/app-server/run-attempt.context-engine.test.ts +0 -1004
  80. package/src/app-server/run-attempt.test.ts +0 -9477
  81. package/src/app-server/run-attempt.ts +0 -4683
  82. package/src/app-server/run-attempt.vision-tools.test.ts +0 -35
  83. package/src/app-server/schema-normalization-runtime-contract.test.ts +0 -206
  84. package/src/app-server/session-binding.test.ts +0 -303
  85. package/src/app-server/session-binding.ts +0 -398
  86. package/src/app-server/session-history.ts +0 -44
  87. package/src/app-server/shared-client.test.ts +0 -589
  88. package/src/app-server/shared-client.ts +0 -289
  89. package/src/app-server/side-question.test.ts +0 -1175
  90. package/src/app-server/side-question.ts +0 -1007
  91. package/src/app-server/test-support.ts +0 -48
  92. package/src/app-server/thread-lifecycle.test.ts +0 -447
  93. package/src/app-server/thread-lifecycle.ts +0 -939
  94. package/src/app-server/thread-lifecycle.user-mcp-servers.test.ts +0 -442
  95. package/src/app-server/timeout.ts +0 -9
  96. package/src/app-server/tool-progress-normalization.ts +0 -77
  97. package/src/app-server/trajectory.test.ts +0 -205
  98. package/src/app-server/trajectory.ts +0 -365
  99. package/src/app-server/transcript-mirror.test.ts +0 -524
  100. package/src/app-server/transcript-mirror.ts +0 -208
  101. package/src/app-server/transcript-repair-runtime-contract.test.ts +0 -44
  102. package/src/app-server/transport-stdio.test.ts +0 -171
  103. package/src/app-server/transport-stdio.ts +0 -107
  104. package/src/app-server/transport-websocket.test.ts +0 -69
  105. package/src/app-server/transport-websocket.ts +0 -90
  106. package/src/app-server/transport.ts +0 -117
  107. package/src/app-server/user-input-bridge.test.ts +0 -249
  108. package/src/app-server/user-input-bridge.ts +0 -316
  109. package/src/app-server/version.ts +0 -4
  110. package/src/app-server/vision-tools.ts +0 -12
  111. package/src/command-account.ts +0 -544
  112. package/src/command-formatters.ts +0 -425
  113. package/src/command-handlers.ts +0 -2004
  114. package/src/command-rpc.test.ts +0 -16
  115. package/src/command-rpc.ts +0 -142
  116. package/src/commands.test.ts +0 -3312
  117. package/src/commands.ts +0 -65
  118. package/src/conversation-binding-data.ts +0 -124
  119. package/src/conversation-binding.test.ts +0 -599
  120. package/src/conversation-binding.ts +0 -561
  121. package/src/conversation-control.test.ts +0 -126
  122. package/src/conversation-control.ts +0 -303
  123. package/src/conversation-turn-collector.test.ts +0 -191
  124. package/src/conversation-turn-collector.ts +0 -186
  125. package/src/conversation-turn-input.test.ts +0 -141
  126. package/src/conversation-turn-input.ts +0 -106
  127. package/src/manifest.test.ts +0 -20
  128. package/src/migration/apply.ts +0 -501
  129. package/src/migration/helpers.ts +0 -55
  130. package/src/migration/plan.ts +0 -461
  131. package/src/migration/provider.test.ts +0 -1741
  132. package/src/migration/provider.ts +0 -41
  133. package/src/migration/source.ts +0 -643
  134. package/src/migration/targets.ts +0 -25
  135. package/src/node-cli-sessions.test.ts +0 -180
  136. package/src/node-cli-sessions.ts +0 -711
  137. package/test-api.ts +0 -82
  138. 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
- });