@oh-my-pi/pi-agent-core 17.1.0 → 17.1.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/CHANGELOG.md +10 -0
- package/dist/types/types.d.ts +7 -1
- package/package.json +7 -7
- package/src/agent-loop.ts +180 -3
- package/src/agent.ts +3 -0
- package/src/compaction/openai.ts +153 -15
- package/src/types.ts +8 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [17.1.1] - 2026-07-24
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- Added the provider-neutral native computer-call lifecycle, preserving observation outputs and input actions across pending and acknowledged tool results.
|
|
10
|
+
|
|
11
|
+
### Changed
|
|
12
|
+
|
|
13
|
+
- Queued steering no longer hard-aborts non-interruptible tools (e.g. `bash`): it aborts interruptible waits only and raises a cooperative steering signal (`ToolCallContext.steeringSignal`) that long-running tools may observe to finish early or background themselves. The mid-batch steering/IRC watch now runs for every tool batch instead of only batches containing an interruptible tool.
|
|
14
|
+
|
|
5
15
|
## [17.1.0] - 2026-07-24
|
|
6
16
|
|
|
7
17
|
### Added
|
package/dist/types/types.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ApiKey, AssistantMessage, AssistantMessageEvent, AssistantMessageEventStream, Context, Effort, ImageContent, Message, Model, ServiceTier, SimpleStreamOptions, Static, streamSimple, TextContent, Tool, ToolChoice, ToolResultMessage, TSchema } from "@oh-my-pi/pi-ai";
|
|
1
|
+
import type { ApiKey, AssistantMessage, AssistantMessageEvent, AssistantMessageEventStream, Context, Effort, ImageContent, Message, Model, ServiceTier, SimpleStreamOptions, Static, streamSimple, TextContent, Tool, ToolCallProviderMetadata, ToolChoice, ToolResultMessage, ToolResultProviderMetadata, TSchema } from "@oh-my-pi/pi-ai";
|
|
2
2
|
import type { Dialect } from "@oh-my-pi/pi-ai/dialect";
|
|
3
3
|
import type { HarmonyAuditEvent } from "@oh-my-pi/pi-ai/utils/harmony-leak";
|
|
4
4
|
import type { AppendOnlyContextManager } from "./append-only-context.js";
|
|
@@ -399,6 +399,8 @@ export interface ToolCallContext {
|
|
|
399
399
|
id: string;
|
|
400
400
|
name: string;
|
|
401
401
|
}>;
|
|
402
|
+
/** Provider-native metadata for the current call, when present. */
|
|
403
|
+
providerMetadata?: ToolCallProviderMetadata;
|
|
402
404
|
/**
|
|
403
405
|
* Cooperative steering signal: aborted when a queued user/steering message
|
|
404
406
|
* (or an interrupting peer IRC) is detected while this tool batch runs.
|
|
@@ -437,6 +439,8 @@ export interface AfterToolCallResult {
|
|
|
437
439
|
content?: (TextContent | ImageContent)[];
|
|
438
440
|
/** If provided, replaces the tool result details payload in full. */
|
|
439
441
|
details?: unknown;
|
|
442
|
+
/** If provided, replaces the provider-native result metadata in full. */
|
|
443
|
+
providerMetadata?: ToolResultProviderMetadata;
|
|
440
444
|
/** If provided, replaces the error flag carried with the tool result. */
|
|
441
445
|
isError?: boolean;
|
|
442
446
|
/** If provided, replaces the contextually-useless flag carried with the tool result. */
|
|
@@ -512,6 +516,8 @@ export interface AgentToolResult<T = any, _TInput = unknown> {
|
|
|
512
516
|
content: (TextContent | ImageContent)[];
|
|
513
517
|
details?: T;
|
|
514
518
|
isError?: boolean;
|
|
519
|
+
/** Provider-native metadata that must survive into history replay unchanged. */
|
|
520
|
+
providerMetadata?: ToolResultProviderMetadata;
|
|
515
521
|
/** Marks the result as contextually useless: safe for compaction to elide once consumed (e.g. zero matches, wait timeout). Ignored when isError is set. */
|
|
516
522
|
useless?: boolean;
|
|
517
523
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@oh-my-pi/pi-agent-core",
|
|
4
|
-
"version": "17.1.
|
|
4
|
+
"version": "17.1.1",
|
|
5
5
|
"description": "General-purpose agent with transport abstraction, state management, and attachment support",
|
|
6
6
|
"homepage": "https://omp.sh",
|
|
7
7
|
"author": "Can Boluk",
|
|
@@ -35,12 +35,12 @@
|
|
|
35
35
|
"fmt": "biome format --write ."
|
|
36
36
|
},
|
|
37
37
|
"dependencies": {
|
|
38
|
-
"@oh-my-pi/pi-ai": "17.1.
|
|
39
|
-
"@oh-my-pi/pi-catalog": "17.1.
|
|
40
|
-
"@oh-my-pi/pi-natives": "17.1.
|
|
41
|
-
"@oh-my-pi/pi-utils": "17.1.
|
|
42
|
-
"@oh-my-pi/pi-wire": "17.1.
|
|
43
|
-
"@oh-my-pi/snapcompact": "17.1.
|
|
38
|
+
"@oh-my-pi/pi-ai": "17.1.1",
|
|
39
|
+
"@oh-my-pi/pi-catalog": "17.1.1",
|
|
40
|
+
"@oh-my-pi/pi-natives": "17.1.1",
|
|
41
|
+
"@oh-my-pi/pi-utils": "17.1.1",
|
|
42
|
+
"@oh-my-pi/pi-wire": "17.1.1",
|
|
43
|
+
"@oh-my-pi/snapcompact": "17.1.1",
|
|
44
44
|
"@opentelemetry/api": "^1.9.1"
|
|
45
45
|
},
|
|
46
46
|
"devDependencies": {
|
package/src/agent-loop.ts
CHANGED
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
import {
|
|
6
6
|
type AssistantMessage,
|
|
7
7
|
type AssistantMessageEvent,
|
|
8
|
+
type ComputerAction,
|
|
9
|
+
type ComputerSafetyCheck,
|
|
8
10
|
type Context,
|
|
9
11
|
EventStream,
|
|
10
12
|
isApiKeyResolver,
|
|
@@ -12,8 +14,10 @@ import {
|
|
|
12
14
|
seedApiKeyResolver,
|
|
13
15
|
streamSimple,
|
|
14
16
|
stripSchemaDescriptions,
|
|
17
|
+
type ToolCallProviderMetadata,
|
|
15
18
|
type ToolChoice,
|
|
16
19
|
type ToolResultMessage,
|
|
20
|
+
type ToolResultProviderMetadata,
|
|
17
21
|
type TSchema,
|
|
18
22
|
toolWireSchema,
|
|
19
23
|
validateToolArguments,
|
|
@@ -106,6 +110,7 @@ const MAX_SOFT_TOOL_ESCALATIONS = 3;
|
|
|
106
110
|
function hardToolChoiceBlocks(choice: ToolChoice | undefined, requiredTool: string): boolean {
|
|
107
111
|
if (choice === undefined) return false;
|
|
108
112
|
if (typeof choice === "string") return choice === "none";
|
|
113
|
+
if (choice.type === "computer") return requiredTool !== "computer";
|
|
109
114
|
const name = choice.type === "tool" ? choice.name : "function" in choice ? choice.function.name : choice.name;
|
|
110
115
|
return name !== requiredTool;
|
|
111
116
|
}
|
|
@@ -180,6 +185,160 @@ export function resolveOwnedDialectFromEnv(value: string | undefined): Dialect |
|
|
|
180
185
|
type AssistantContentBlock = AssistantMessage["content"][number];
|
|
181
186
|
type AssistantToolCallBlock = Extract<AssistantContentBlock, { type: "toolCall" }>;
|
|
182
187
|
|
|
188
|
+
function snapshotComputerSafetyChecks(value: unknown): ComputerSafetyCheck[] | undefined {
|
|
189
|
+
if (!Array.isArray(value)) return undefined;
|
|
190
|
+
const checks: ComputerSafetyCheck[] = [];
|
|
191
|
+
for (const raw of value) {
|
|
192
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return undefined;
|
|
193
|
+
const check = raw as Record<string, unknown>;
|
|
194
|
+
if (typeof check.id !== "string" || check.id.length === 0) return undefined;
|
|
195
|
+
if (check.code !== undefined && check.code !== null && typeof check.code !== "string") return undefined;
|
|
196
|
+
if (check.message !== undefined && check.message !== null && typeof check.message !== "string") return undefined;
|
|
197
|
+
checks.push({
|
|
198
|
+
id: check.id,
|
|
199
|
+
...(check.code !== undefined ? { code: check.code as string | null } : {}),
|
|
200
|
+
...(check.message !== undefined ? { message: check.message as string | null } : {}),
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
return checks;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function isFiniteCoordinate(value: unknown): value is number {
|
|
207
|
+
return typeof value === "number" && Number.isFinite(value);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function hasValidComputerKeys(value: unknown, optional: boolean): boolean {
|
|
211
|
+
return (
|
|
212
|
+
(optional && value === undefined) ||
|
|
213
|
+
value === null ||
|
|
214
|
+
(Array.isArray(value) && value.every(key => typeof key === "string"))
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function snapshotComputerAction(value: unknown): ComputerAction | undefined {
|
|
219
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
|
|
220
|
+
const action = value as Record<string, unknown>;
|
|
221
|
+
switch (action.type) {
|
|
222
|
+
case "click":
|
|
223
|
+
if (
|
|
224
|
+
!(["left", "right", "wheel", "back", "forward"] as unknown[]).includes(action.button) ||
|
|
225
|
+
!isFiniteCoordinate(action.x) ||
|
|
226
|
+
!isFiniteCoordinate(action.y) ||
|
|
227
|
+
!hasValidComputerKeys(action.keys, true)
|
|
228
|
+
)
|
|
229
|
+
return undefined;
|
|
230
|
+
break;
|
|
231
|
+
case "double_click":
|
|
232
|
+
if (
|
|
233
|
+
!isFiniteCoordinate(action.x) ||
|
|
234
|
+
!isFiniteCoordinate(action.y) ||
|
|
235
|
+
!hasValidComputerKeys(action.keys, false)
|
|
236
|
+
)
|
|
237
|
+
return undefined;
|
|
238
|
+
break;
|
|
239
|
+
case "drag":
|
|
240
|
+
if (
|
|
241
|
+
!Array.isArray(action.path) ||
|
|
242
|
+
!action.path.every(
|
|
243
|
+
point =>
|
|
244
|
+
point &&
|
|
245
|
+
typeof point === "object" &&
|
|
246
|
+
isFiniteCoordinate((point as Record<string, unknown>).x) &&
|
|
247
|
+
isFiniteCoordinate((point as Record<string, unknown>).y),
|
|
248
|
+
) ||
|
|
249
|
+
!hasValidComputerKeys(action.keys, true)
|
|
250
|
+
)
|
|
251
|
+
return undefined;
|
|
252
|
+
break;
|
|
253
|
+
case "keypress":
|
|
254
|
+
if (!Array.isArray(action.keys) || !action.keys.every(key => typeof key === "string")) return undefined;
|
|
255
|
+
break;
|
|
256
|
+
case "move":
|
|
257
|
+
if (!isFiniteCoordinate(action.x) || !isFiniteCoordinate(action.y) || !hasValidComputerKeys(action.keys, true))
|
|
258
|
+
return undefined;
|
|
259
|
+
break;
|
|
260
|
+
case "screenshot":
|
|
261
|
+
case "wait":
|
|
262
|
+
break;
|
|
263
|
+
case "scroll":
|
|
264
|
+
if (
|
|
265
|
+
!isFiniteCoordinate(action.x) ||
|
|
266
|
+
!isFiniteCoordinate(action.y) ||
|
|
267
|
+
!isFiniteCoordinate(action.scroll_x) ||
|
|
268
|
+
!isFiniteCoordinate(action.scroll_y) ||
|
|
269
|
+
!hasValidComputerKeys(action.keys, true)
|
|
270
|
+
)
|
|
271
|
+
return undefined;
|
|
272
|
+
break;
|
|
273
|
+
case "type":
|
|
274
|
+
if (typeof action.text !== "string") return undefined;
|
|
275
|
+
break;
|
|
276
|
+
default:
|
|
277
|
+
return undefined;
|
|
278
|
+
}
|
|
279
|
+
return structuredCloneJSON(action) as ComputerAction;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function snapshotToolCallProviderMetadata(value: unknown): ToolCallProviderMetadata | undefined {
|
|
283
|
+
if (value === undefined) return undefined;
|
|
284
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
|
|
285
|
+
const metadata = value as Record<string, unknown>;
|
|
286
|
+
if (
|
|
287
|
+
metadata.type !== "computer" ||
|
|
288
|
+
typeof metadata.providerItemId !== "string" ||
|
|
289
|
+
metadata.providerItemId.length === 0
|
|
290
|
+
)
|
|
291
|
+
return undefined;
|
|
292
|
+
if (!Array.isArray(metadata.actions) || metadata.actions.length === 0) return undefined;
|
|
293
|
+
const actions = metadata.actions.map(snapshotComputerAction);
|
|
294
|
+
if (actions.some(action => action === undefined)) return undefined;
|
|
295
|
+
const pendingSafetyChecks = snapshotComputerSafetyChecks(metadata.pendingSafetyChecks);
|
|
296
|
+
if (!pendingSafetyChecks) return undefined;
|
|
297
|
+
return {
|
|
298
|
+
type: "computer",
|
|
299
|
+
providerItemId: metadata.providerItemId,
|
|
300
|
+
actions: actions as ComputerAction[],
|
|
301
|
+
pendingSafetyChecks,
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function snapshotToolResultProviderMetadata(value: unknown): {
|
|
306
|
+
metadata?: ToolResultProviderMetadata;
|
|
307
|
+
malformed: boolean;
|
|
308
|
+
} {
|
|
309
|
+
if (value === undefined) return { malformed: false };
|
|
310
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return { malformed: true };
|
|
311
|
+
const metadata = value as Record<string, unknown>;
|
|
312
|
+
if (
|
|
313
|
+
metadata.type !== "computer" ||
|
|
314
|
+
!metadata.screenshot ||
|
|
315
|
+
typeof metadata.screenshot !== "object" ||
|
|
316
|
+
Array.isArray(metadata.screenshot)
|
|
317
|
+
) {
|
|
318
|
+
return { malformed: true };
|
|
319
|
+
}
|
|
320
|
+
const screenshot = metadata.screenshot as Record<string, unknown>;
|
|
321
|
+
const hasImageUrl = Object.hasOwn(screenshot, "image_url");
|
|
322
|
+
const hasFileId = Object.hasOwn(screenshot, "file_id");
|
|
323
|
+
if (screenshot.type !== "computer_screenshot" || hasImageUrl === hasFileId) return { malformed: true };
|
|
324
|
+
if (hasImageUrl && (typeof screenshot.image_url !== "string" || screenshot.image_url.length === 0))
|
|
325
|
+
return { malformed: true };
|
|
326
|
+
if (hasFileId && (typeof screenshot.file_id !== "string" || screenshot.file_id.length === 0))
|
|
327
|
+
return { malformed: true };
|
|
328
|
+
const acknowledgedSafetyChecks = snapshotComputerSafetyChecks(metadata.acknowledgedSafetyChecks);
|
|
329
|
+
if (!acknowledgedSafetyChecks) return { malformed: true };
|
|
330
|
+
return {
|
|
331
|
+
malformed: false,
|
|
332
|
+
metadata: {
|
|
333
|
+
type: "computer",
|
|
334
|
+
screenshot: hasImageUrl
|
|
335
|
+
? { type: "computer_screenshot", image_url: screenshot.image_url as string }
|
|
336
|
+
: { type: "computer_screenshot", file_id: screenshot.file_id as string },
|
|
337
|
+
acknowledgedSafetyChecks,
|
|
338
|
+
},
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
|
|
183
342
|
function snapshotAssistantContentBlock(block: AssistantContentBlock): AssistantContentBlock {
|
|
184
343
|
switch (block.type) {
|
|
185
344
|
case "text":
|
|
@@ -192,7 +351,11 @@ function snapshotAssistantContentBlock(block: AssistantContentBlock): AssistantC
|
|
|
192
351
|
case "fallback":
|
|
193
352
|
return { ...block, from: { ...block.from }, to: { ...block.to } };
|
|
194
353
|
case "toolCall":
|
|
195
|
-
return {
|
|
354
|
+
return {
|
|
355
|
+
...block,
|
|
356
|
+
arguments: structuredCloneJSON(block.arguments),
|
|
357
|
+
providerMetadata: snapshotToolCallProviderMetadata(block.providerMetadata),
|
|
358
|
+
};
|
|
196
359
|
}
|
|
197
360
|
}
|
|
198
361
|
|
|
@@ -268,6 +431,10 @@ function coerceToolResult(raw: unknown): { result: AgentToolResult<unknown>; mal
|
|
|
268
431
|
const rawObj = raw && typeof raw === "object" ? (raw as Record<string, unknown>) : null;
|
|
269
432
|
const rawContent = rawObj?.content;
|
|
270
433
|
const details = rawObj && "details" in rawObj ? rawObj.details : {};
|
|
434
|
+
const providerMetadataResult = snapshotToolResultProviderMetadata(
|
|
435
|
+
rawObj && "providerMetadata" in rawObj ? rawObj.providerMetadata : undefined,
|
|
436
|
+
);
|
|
437
|
+
const providerMetadata = providerMetadataResult.metadata;
|
|
271
438
|
// Tools may flag a non-throwing failure on the result itself (e.g. an
|
|
272
439
|
// aggregator that catches per-entry errors and synthesizes a combined
|
|
273
440
|
// result). Preserve the flag so agent-loop can surface it on the wire.
|
|
@@ -312,7 +479,13 @@ function coerceToolResult(raw: unknown): { result: AgentToolResult<unknown>; mal
|
|
|
312
479
|
text: `Tool returned an invalid result: ${invalidBlocks} content block${invalidBlocks === 1 ? "" : "s"} had an unsupported shape.`,
|
|
313
480
|
});
|
|
314
481
|
}
|
|
315
|
-
|
|
482
|
+
if (providerMetadataResult.malformed) {
|
|
483
|
+
content.push({
|
|
484
|
+
type: "text",
|
|
485
|
+
text: "Tool returned an invalid result: computer providerMetadata had an unsupported shape.",
|
|
486
|
+
});
|
|
487
|
+
}
|
|
488
|
+
const isError = explicitError || invalidBlocks > 0 || providerMetadataResult.malformed;
|
|
316
489
|
// Anthropic rejects tool_result blocks with is_error: true and empty content.
|
|
317
490
|
if (isError && !hasSubstantiveToolResultContent(content)) {
|
|
318
491
|
content.length = 0;
|
|
@@ -322,10 +495,11 @@ function coerceToolResult(raw: unknown): { result: AgentToolResult<unknown>; mal
|
|
|
322
495
|
result: {
|
|
323
496
|
content,
|
|
324
497
|
details,
|
|
498
|
+
providerMetadata,
|
|
325
499
|
...(isError ? { isError: true } : {}),
|
|
326
500
|
...(useless && !isError ? { useless: true } : {}),
|
|
327
501
|
},
|
|
328
|
-
malformed: invalidBlocks > 0,
|
|
502
|
+
malformed: invalidBlocks > 0 || providerMetadataResult.malformed,
|
|
329
503
|
};
|
|
330
504
|
}
|
|
331
505
|
|
|
@@ -1947,6 +2121,7 @@ async function executeToolCalls(
|
|
|
1947
2121
|
toolName: toolCall.name,
|
|
1948
2122
|
content: result.content,
|
|
1949
2123
|
details: result.details,
|
|
2124
|
+
providerMetadata: result.providerMetadata,
|
|
1950
2125
|
isError,
|
|
1951
2126
|
...(result.useless && !isError ? { useless: true } : {}),
|
|
1952
2127
|
timestamp: Date.now(),
|
|
@@ -2111,6 +2286,7 @@ async function executeToolCalls(
|
|
|
2111
2286
|
total: toolCalls.length,
|
|
2112
2287
|
toolCalls: toolCallInfos,
|
|
2113
2288
|
steeringSignal: steeringSoftController.signal,
|
|
2289
|
+
providerMetadata: toolCall.providerMetadata,
|
|
2114
2290
|
})
|
|
2115
2291
|
: undefined;
|
|
2116
2292
|
const rawResult = await tool.execute(
|
|
@@ -2163,6 +2339,7 @@ async function executeToolCalls(
|
|
|
2163
2339
|
content: after.content ?? result.content,
|
|
2164
2340
|
details: after.details ?? result.details,
|
|
2165
2341
|
isError: after.isError ?? result.isError,
|
|
2342
|
+
providerMetadata: after.providerMetadata ?? result.providerMetadata,
|
|
2166
2343
|
useless: after.useless ?? result.useless,
|
|
2167
2344
|
});
|
|
2168
2345
|
result = coerced.result;
|
package/src/agent.ts
CHANGED
|
@@ -72,6 +72,9 @@ function refreshToolChoiceForActiveTools(
|
|
|
72
72
|
if (!toolChoice || typeof toolChoice === "string") {
|
|
73
73
|
return toolChoice;
|
|
74
74
|
}
|
|
75
|
+
if (toolChoice.type === "computer") {
|
|
76
|
+
return tools.some(tool => tool.native?.type === "computer") ? toolChoice : undefined;
|
|
77
|
+
}
|
|
75
78
|
|
|
76
79
|
const toolName =
|
|
77
80
|
toolChoice.type === "tool"
|
package/src/compaction/openai.ts
CHANGED
|
@@ -20,6 +20,7 @@ import { applyCodexResponsesLiteShape } from "@oh-my-pi/pi-ai/providers/openai-c
|
|
|
20
20
|
import {
|
|
21
21
|
createOpenAICodexCompactionRequestContext,
|
|
22
22
|
createOpenAICodexCompatibilityMetadata,
|
|
23
|
+
getCodexAttestationHeader,
|
|
23
24
|
} from "@oh-my-pi/pi-ai/providers/openai-codex-responses";
|
|
24
25
|
import { parseAzureDeploymentNameMap, parseTextSignature } from "@oh-my-pi/pi-ai/providers/openai-shared";
|
|
25
26
|
import { transformMessages } from "@oh-my-pi/pi-ai/providers/transform-messages";
|
|
@@ -43,7 +44,7 @@ import {
|
|
|
43
44
|
OPENAI_HEADER_VALUES,
|
|
44
45
|
OPENAI_HEADERS,
|
|
45
46
|
} from "@oh-my-pi/pi-catalog/wire/codex";
|
|
46
|
-
import { $env, logger, stringifyJson } from "@oh-my-pi/pi-utils";
|
|
47
|
+
import { $env, logger, stringifyJson, structuredCloneJSON } from "@oh-my-pi/pi-utils";
|
|
47
48
|
|
|
48
49
|
export * from "./compaction-v2-streaming";
|
|
49
50
|
|
|
@@ -254,6 +255,7 @@ function addOpenAiCallIds(
|
|
|
254
255
|
items: Array<Record<string, unknown>>,
|
|
255
256
|
knownCallIds: Set<string>,
|
|
256
257
|
customCallIds: Set<string>,
|
|
258
|
+
computerCallIds: Set<string>,
|
|
257
259
|
): void {
|
|
258
260
|
for (const item of items) {
|
|
259
261
|
if (typeof item.call_id !== "string") continue;
|
|
@@ -262,10 +264,57 @@ function addOpenAiCallIds(
|
|
|
262
264
|
} else if (item.type === "custom_tool_call") {
|
|
263
265
|
knownCallIds.add(item.call_id);
|
|
264
266
|
customCallIds.add(item.call_id);
|
|
267
|
+
} else if (item.type === "computer_call") {
|
|
268
|
+
knownCallIds.add(item.call_id);
|
|
269
|
+
computerCallIds.add(item.call_id);
|
|
265
270
|
}
|
|
266
271
|
}
|
|
267
272
|
}
|
|
268
273
|
|
|
274
|
+
function computerHistoryNote(item: Record<string, unknown>): Record<string, unknown> {
|
|
275
|
+
const serialized = stringifyJson(item) ?? "";
|
|
276
|
+
return {
|
|
277
|
+
type: "message",
|
|
278
|
+
id: `msg_${Bun.hash(`computer-history:${serialized}`).toString(36)}`,
|
|
279
|
+
role: "assistant",
|
|
280
|
+
content: [
|
|
281
|
+
{
|
|
282
|
+
type: "output_text",
|
|
283
|
+
text: `[Previous computer history unavailable to this model]: ${serialized}`,
|
|
284
|
+
annotations: [],
|
|
285
|
+
},
|
|
286
|
+
],
|
|
287
|
+
status: "completed",
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function adaptComputerHistoryForCompaction(
|
|
292
|
+
items: Array<Record<string, unknown>>,
|
|
293
|
+
supportsComputerUse: boolean,
|
|
294
|
+
): Array<Record<string, unknown>> {
|
|
295
|
+
if (supportsComputerUse) return items;
|
|
296
|
+
return items.map(item =>
|
|
297
|
+
item.type === "computer_call" || item.type === "computer_call_output" ? computerHistoryNote(item) : item,
|
|
298
|
+
);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function computerFailureNote(call: Record<string, unknown>, output: string): Record<string, unknown> {
|
|
302
|
+
const serialized = stringifyJson(call) ?? "";
|
|
303
|
+
return {
|
|
304
|
+
type: "message",
|
|
305
|
+
id: `msg_${Bun.hash(`computer-failure:${serialized}:${output}`).toString(36)}`,
|
|
306
|
+
role: "assistant",
|
|
307
|
+
content: [
|
|
308
|
+
{
|
|
309
|
+
type: "output_text",
|
|
310
|
+
text: `[Computer call failed before a screenshot was recorded]: ${serialized}${output ? `\n${output}` : ""}`,
|
|
311
|
+
annotations: [],
|
|
312
|
+
},
|
|
313
|
+
],
|
|
314
|
+
status: "completed",
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
|
|
269
318
|
// ============================================================================
|
|
270
319
|
// Native history construction (responses-API shape)
|
|
271
320
|
// ============================================================================
|
|
@@ -287,20 +336,32 @@ export function buildOpenAiNativeHistory(
|
|
|
287
336
|
model: Model,
|
|
288
337
|
previousReplacementHistory?: Array<Record<string, unknown>>,
|
|
289
338
|
): Array<Record<string, unknown>> {
|
|
290
|
-
const input: Array<Record<string, unknown>> = previousReplacementHistory
|
|
339
|
+
const input: Array<Record<string, unknown>> = previousReplacementHistory
|
|
340
|
+
? adaptComputerHistoryForCompaction([...previousReplacementHistory], model.supportsComputerUse === true)
|
|
341
|
+
: [];
|
|
291
342
|
const transformedMessages = transformMessages(messages, model, id => normalizeOpenAiCompactionToolCallId(id));
|
|
292
343
|
|
|
293
344
|
let msgIndex = 0;
|
|
294
345
|
const knownCallIds = new Set<string>();
|
|
295
346
|
const customCallIds = new Set<string>();
|
|
296
|
-
|
|
347
|
+
const computerCallIds = new Set<string>();
|
|
348
|
+
const demotedComputerCallIds = new Set<string>();
|
|
349
|
+
addOpenAiCallIds(input, knownCallIds, customCallIds, computerCallIds);
|
|
297
350
|
for (const message of transformedMessages) {
|
|
298
351
|
if (message.role === "user" || message.role === "developer") {
|
|
299
352
|
const providerPayload = (message as { providerPayload?: AssistantMessage["providerPayload"] }).providerPayload;
|
|
300
|
-
const
|
|
301
|
-
if (
|
|
353
|
+
const rawHistoryItems = getOpenAIResponsesHistoryItems(providerPayload, model.provider);
|
|
354
|
+
if (rawHistoryItems) {
|
|
355
|
+
if (model.supportsComputerUse !== true) {
|
|
356
|
+
for (const item of rawHistoryItems) {
|
|
357
|
+
if (item.type === "computer_call" && typeof item.call_id === "string") {
|
|
358
|
+
demotedComputerCallIds.add(item.call_id);
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
const historyItems = adaptComputerHistoryForCompaction(rawHistoryItems, model.supportsComputerUse === true);
|
|
302
363
|
input.push(...historyItems);
|
|
303
|
-
addOpenAiCallIds(historyItems, knownCallIds, customCallIds);
|
|
364
|
+
addOpenAiCallIds(historyItems, knownCallIds, customCallIds, computerCallIds);
|
|
304
365
|
msgIndex++;
|
|
305
366
|
continue;
|
|
306
367
|
}
|
|
@@ -341,14 +402,27 @@ export function buildOpenAiNativeHistory(
|
|
|
341
402
|
assistant.provider,
|
|
342
403
|
);
|
|
343
404
|
if (providerPayload) {
|
|
405
|
+
if (!providerPayload.dt) demotedComputerCallIds.clear();
|
|
406
|
+
if (model.supportsComputerUse !== true) {
|
|
407
|
+
for (const item of providerPayload.items) {
|
|
408
|
+
if (item.type === "computer_call" && typeof item.call_id === "string") {
|
|
409
|
+
demotedComputerCallIds.add(item.call_id);
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
const historyItems = adaptComputerHistoryForCompaction(
|
|
414
|
+
providerPayload.items,
|
|
415
|
+
model.supportsComputerUse === true,
|
|
416
|
+
);
|
|
344
417
|
if (providerPayload.dt) {
|
|
345
|
-
input.push(...
|
|
346
|
-
addOpenAiCallIds(
|
|
418
|
+
input.push(...historyItems);
|
|
419
|
+
addOpenAiCallIds(historyItems, knownCallIds, customCallIds, computerCallIds);
|
|
347
420
|
} else {
|
|
348
|
-
input.splice(0, input.length, ...
|
|
421
|
+
input.splice(0, input.length, ...historyItems);
|
|
349
422
|
knownCallIds.clear();
|
|
350
423
|
customCallIds.clear();
|
|
351
|
-
|
|
424
|
+
computerCallIds.clear();
|
|
425
|
+
addOpenAiCallIds(input, knownCallIds, customCallIds, computerCallIds);
|
|
352
426
|
}
|
|
353
427
|
msgIndex++;
|
|
354
428
|
continue;
|
|
@@ -394,6 +468,25 @@ export function buildOpenAiNativeHistory(
|
|
|
394
468
|
|
|
395
469
|
if (block.type === "toolCall") {
|
|
396
470
|
const normalized = normalizeResponsesToolCallId(block.id, block.customWireName ? "ctc" : "fc");
|
|
471
|
+
if (block.providerMetadata?.type === "computer") {
|
|
472
|
+
const computerCall = {
|
|
473
|
+
type: "computer_call",
|
|
474
|
+
id: block.providerMetadata.providerItemId,
|
|
475
|
+
call_id: normalized.callId,
|
|
476
|
+
actions: structuredCloneJSON(block.providerMetadata.actions),
|
|
477
|
+
pending_safety_checks: structuredCloneJSON(block.providerMetadata.pendingSafetyChecks),
|
|
478
|
+
status: "completed",
|
|
479
|
+
};
|
|
480
|
+
if (model.supportsComputerUse !== true) {
|
|
481
|
+
input.push(computerHistoryNote(computerCall));
|
|
482
|
+
demotedComputerCallIds.add(normalized.callId);
|
|
483
|
+
continue;
|
|
484
|
+
}
|
|
485
|
+
knownCallIds.add(normalized.callId);
|
|
486
|
+
computerCallIds.add(normalized.callId);
|
|
487
|
+
input.push(computerCall);
|
|
488
|
+
continue;
|
|
489
|
+
}
|
|
397
490
|
let itemId: string | undefined = normalized.itemId;
|
|
398
491
|
if (
|
|
399
492
|
isDifferentModel &&
|
|
@@ -430,17 +523,58 @@ export function buildOpenAiNativeHistory(
|
|
|
430
523
|
|
|
431
524
|
if (message.role === "toolResult") {
|
|
432
525
|
const normalized = normalizeResponsesToolCallId(message.toolCallId);
|
|
433
|
-
if (!knownCallIds.has(normalized.callId)) {
|
|
434
|
-
msgIndex++;
|
|
435
|
-
continue;
|
|
436
|
-
}
|
|
437
|
-
|
|
438
526
|
const textOutput = message.content
|
|
439
527
|
.filter(block => block.type === "text")
|
|
440
528
|
.map(block => block.text)
|
|
441
529
|
.join("\n");
|
|
442
530
|
const hasImages = message.content.some(block => block.type === "image");
|
|
443
531
|
const outputText = textOutput.length > 0 ? textOutput : hasImages ? "(see attached image)" : "";
|
|
532
|
+
if (demotedComputerCallIds.has(normalized.callId)) {
|
|
533
|
+
const resultItem =
|
|
534
|
+
message.providerMetadata?.type === "computer"
|
|
535
|
+
? {
|
|
536
|
+
type: "computer_call_output",
|
|
537
|
+
call_id: normalized.callId,
|
|
538
|
+
output: structuredCloneJSON(message.providerMetadata.screenshot),
|
|
539
|
+
acknowledged_safety_checks: structuredCloneJSON(
|
|
540
|
+
message.providerMetadata.acknowledgedSafetyChecks,
|
|
541
|
+
),
|
|
542
|
+
}
|
|
543
|
+
: { type: "computer_call_output", call_id: normalized.callId, error: outputText };
|
|
544
|
+
input.push(computerHistoryNote(resultItem));
|
|
545
|
+
demotedComputerCallIds.delete(normalized.callId);
|
|
546
|
+
msgIndex++;
|
|
547
|
+
continue;
|
|
548
|
+
}
|
|
549
|
+
if (!knownCallIds.has(normalized.callId)) {
|
|
550
|
+
msgIndex++;
|
|
551
|
+
continue;
|
|
552
|
+
}
|
|
553
|
+
if (computerCallIds.has(normalized.callId)) {
|
|
554
|
+
if (message.providerMetadata?.type === "computer") {
|
|
555
|
+
input.push({
|
|
556
|
+
type: "computer_call_output",
|
|
557
|
+
call_id: normalized.callId,
|
|
558
|
+
output: structuredCloneJSON(message.providerMetadata.screenshot),
|
|
559
|
+
acknowledged_safety_checks: structuredCloneJSON(message.providerMetadata.acknowledgedSafetyChecks),
|
|
560
|
+
});
|
|
561
|
+
msgIndex++;
|
|
562
|
+
continue;
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
const callIndex = input.findLastIndex(
|
|
566
|
+
item => item.type === "computer_call" && item.call_id === normalized.callId,
|
|
567
|
+
);
|
|
568
|
+
if (callIndex >= 0) {
|
|
569
|
+
const [call] = input.splice(callIndex, 1);
|
|
570
|
+
if (call) input.splice(callIndex, 0, computerFailureNote(call, outputText));
|
|
571
|
+
}
|
|
572
|
+
knownCallIds.delete(normalized.callId);
|
|
573
|
+
computerCallIds.delete(normalized.callId);
|
|
574
|
+
msgIndex++;
|
|
575
|
+
continue;
|
|
576
|
+
}
|
|
577
|
+
|
|
444
578
|
input.push({
|
|
445
579
|
type: customCallIds.has(normalized.callId) ? "custom_tool_call_output" : "function_call_output",
|
|
446
580
|
call_id: normalized.callId,
|
|
@@ -517,6 +651,10 @@ export async function requestOpenAiRemoteCompaction(
|
|
|
517
651
|
if (accountId) {
|
|
518
652
|
headers[OPENAI_HEADERS.ACCOUNT_ID] = accountId;
|
|
519
653
|
}
|
|
654
|
+
const attestation = await getCodexAttestationHeader(accountId);
|
|
655
|
+
if (attestation) {
|
|
656
|
+
headers[OPENAI_HEADERS.ATTESTATION] = attestation;
|
|
657
|
+
}
|
|
520
658
|
headers[OPENAI_HEADERS.BETA] = OPENAI_HEADER_VALUES.BETA_RESPONSES;
|
|
521
659
|
headers[OPENAI_HEADERS.ORIGINATOR] = OPENAI_HEADER_VALUES.ORIGINATOR_CODEX;
|
|
522
660
|
Object.assign(
|
package/src/types.ts
CHANGED
|
@@ -14,8 +14,10 @@ import type {
|
|
|
14
14
|
streamSimple,
|
|
15
15
|
TextContent,
|
|
16
16
|
Tool,
|
|
17
|
+
ToolCallProviderMetadata,
|
|
17
18
|
ToolChoice,
|
|
18
19
|
ToolResultMessage,
|
|
20
|
+
ToolResultProviderMetadata,
|
|
19
21
|
TSchema,
|
|
20
22
|
} from "@oh-my-pi/pi-ai";
|
|
21
23
|
import type { Dialect } from "@oh-my-pi/pi-ai/dialect";
|
|
@@ -458,6 +460,8 @@ export interface ToolCallContext {
|
|
|
458
460
|
index: number;
|
|
459
461
|
total: number;
|
|
460
462
|
toolCalls: Array<{ id: string; name: string }>;
|
|
463
|
+
/** Provider-native metadata for the current call, when present. */
|
|
464
|
+
providerMetadata?: ToolCallProviderMetadata;
|
|
461
465
|
/**
|
|
462
466
|
* Cooperative steering signal: aborted when a queued user/steering message
|
|
463
467
|
* (or an interrupting peer IRC) is detected while this tool batch runs.
|
|
@@ -497,6 +501,8 @@ export interface AfterToolCallResult {
|
|
|
497
501
|
content?: (TextContent | ImageContent)[];
|
|
498
502
|
/** If provided, replaces the tool result details payload in full. */
|
|
499
503
|
details?: unknown;
|
|
504
|
+
/** If provided, replaces the provider-native result metadata in full. */
|
|
505
|
+
providerMetadata?: ToolResultProviderMetadata;
|
|
500
506
|
/** If provided, replaces the error flag carried with the tool result. */
|
|
501
507
|
isError?: boolean;
|
|
502
508
|
/** If provided, replaces the contextually-useless flag carried with the tool result. */
|
|
@@ -583,6 +589,8 @@ export interface AgentToolResult<T = any, _TInput = unknown> {
|
|
|
583
589
|
// Marks a non-throwing failure (e.g. an aggregator catching per-entry errors).
|
|
584
590
|
// agent-loop honors this and surfaces it as a tool error on the wire.
|
|
585
591
|
isError?: boolean;
|
|
592
|
+
/** Provider-native metadata that must survive into history replay unchanged. */
|
|
593
|
+
providerMetadata?: ToolResultProviderMetadata;
|
|
586
594
|
/** Marks the result as contextually useless: safe for compaction to elide once consumed (e.g. zero matches, wait timeout). Ignored when isError is set. */
|
|
587
595
|
useless?: boolean;
|
|
588
596
|
}
|