@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 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
@@ -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.0",
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.0",
39
- "@oh-my-pi/pi-catalog": "17.1.0",
40
- "@oh-my-pi/pi-natives": "17.1.0",
41
- "@oh-my-pi/pi-utils": "17.1.0",
42
- "@oh-my-pi/pi-wire": "17.1.0",
43
- "@oh-my-pi/snapcompact": "17.1.0",
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 { ...block, arguments: structuredCloneJSON(block.arguments) };
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
- const isError = explicitError || invalidBlocks > 0;
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"
@@ -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 ? [...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
- addOpenAiCallIds(input, knownCallIds, customCallIds);
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 historyItems = getOpenAIResponsesHistoryItems(providerPayload, model.provider);
301
- if (historyItems) {
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(...providerPayload.items);
346
- addOpenAiCallIds(providerPayload.items, knownCallIds, customCallIds);
418
+ input.push(...historyItems);
419
+ addOpenAiCallIds(historyItems, knownCallIds, customCallIds, computerCallIds);
347
420
  } else {
348
- input.splice(0, input.length, ...providerPayload.items);
421
+ input.splice(0, input.length, ...historyItems);
349
422
  knownCallIds.clear();
350
423
  customCallIds.clear();
351
- addOpenAiCallIds(input, knownCallIds, customCallIds);
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
  }