@oh-my-pi/pi-ai 13.13.0 → 13.13.2

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,11 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [13.13.2] - 2026-03-18
6
+ ### Changed
7
+
8
+ - Modified tool result handling for aborted assistant messages to preserve existing tool results when already recorded, instead of always replacing them with synthetic 'aborted' results
9
+
5
10
  ## [13.13.0] - 2026-03-18
6
11
  ### Changed
7
12
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@oh-my-pi/pi-ai",
4
- "version": "13.13.0",
4
+ "version": "13.13.2",
5
5
  "description": "Unified LLM API with automatic model discovery and provider configuration",
6
6
  "homepage": "https://github.com/can1357/oh-my-pi",
7
7
  "author": "Can Boluk",
@@ -41,7 +41,7 @@
41
41
  "@aws-sdk/client-bedrock-runtime": "^3",
42
42
  "@bufbuild/protobuf": "^2.11",
43
43
  "@google/genai": "^1.43",
44
- "@oh-my-pi/pi-utils": "13.13.0",
44
+ "@oh-my-pi/pi-utils": "13.13.2",
45
45
  "@sinclair/typebox": "^0.34",
46
46
  "@smithy/node-http-handler": "^4.4",
47
47
  "ajv": "^8.18",
@@ -2134,7 +2134,7 @@ function convertMessages(model: Model<"openai-codex-responses">, context: Contex
2134
2134
  } satisfies ResponseOutputMessage);
2135
2135
  continue;
2136
2136
  }
2137
- if (block.type === "toolCall" && msg.stopReason !== "error") {
2137
+ if (block.type === "toolCall") {
2138
2138
  const toolCall = block as ToolCall;
2139
2139
  const normalized = normalizeResponsesToolCallId(toolCall.id);
2140
2140
  outputItems.push({
@@ -134,7 +134,7 @@ export function convertResponsesAssistantMessage<TApi extends Api>(
134
134
  continue;
135
135
  }
136
136
 
137
- if (block.type !== "toolCall" || assistantMsg.stopReason === "error") {
137
+ if (block.type !== "toolCall") {
138
138
  continue;
139
139
  }
140
140
 
@@ -124,101 +124,105 @@ export function transformMessages<TApi extends Api>(
124
124
  });
125
125
 
126
126
  // Second pass: insert synthetic empty tool results for orphaned tool calls
127
- // This preserves thinking signatures and satisfies API requirements
127
+ // and preserve aborted/errored tool results when they were already persisted.
128
128
  const result: Message[] = [];
129
129
  let pendingToolCalls: ToolCall[] = [];
130
- // Track tool call status: whether resolved (has result) or aborted (skip real results)
130
+ let pendingAbortedToolCalls = new Map<string, ToolCall>();
131
+ let pendingAbortedTimestamp: number | undefined;
132
+ // Track tool call status: whether resolved (has result) or aborted (synthetic result injected, skip later real results)
131
133
  const toolCallStatus = new Map<string, ToolCallStatus>();
132
134
 
135
+ const flushPendingToolCalls = (timestamp: number): void => {
136
+ if (pendingToolCalls.length === 0) return;
137
+ for (const tc of pendingToolCalls) {
138
+ if (!toolCallStatus.has(tc.id)) {
139
+ result.push({
140
+ role: "toolResult",
141
+ toolCallId: tc.id,
142
+ toolName: tc.name,
143
+ content: [{ type: "text", text: "No result provided" }],
144
+ isError: true,
145
+ timestamp,
146
+ } as ToolResultMessage);
147
+ toolCallStatus.set(tc.id, ToolCallStatus.Resolved);
148
+ }
149
+ }
150
+ pendingToolCalls = [];
151
+ };
152
+
153
+ const flushPendingAbortedToolCalls = (): void => {
154
+ if (pendingAbortedTimestamp === undefined) return;
155
+ for (const tc of pendingAbortedToolCalls.values()) {
156
+ if (!toolCallStatus.has(tc.id)) {
157
+ result.push({
158
+ role: "toolResult",
159
+ toolCallId: tc.id,
160
+ toolName: tc.name,
161
+ content: [{ type: "text", text: "aborted" }],
162
+ isError: true,
163
+ timestamp: pendingAbortedTimestamp,
164
+ } as ToolResultMessage);
165
+ toolCallStatus.set(tc.id, ToolCallStatus.Aborted);
166
+ }
167
+ }
168
+ result.push({
169
+ role: "developer",
170
+ content: turnAbortedGuidance,
171
+ timestamp: pendingAbortedTimestamp + 1,
172
+ } as DeveloperMessage);
173
+ pendingAbortedToolCalls = new Map();
174
+ pendingAbortedTimestamp = undefined;
175
+ };
176
+
133
177
  for (let i = 0; i < transformed.length; i++) {
134
178
  const msg = transformed[i];
179
+ const messageTimestamp = "timestamp" in msg && typeof msg.timestamp === "number" ? msg.timestamp : Date.now();
135
180
 
136
181
  if (msg.role === "assistant") {
137
- // If we have pending orphaned tool calls from a previous assistant, insert synthetic results now
138
- if (pendingToolCalls.length > 0) {
139
- for (const tc of pendingToolCalls) {
140
- if (!toolCallStatus.has(tc.id)) {
141
- result.push({
142
- role: "toolResult",
143
- toolCallId: tc.id,
144
- toolName: tc.name,
145
- content: [{ type: "text", text: "No result provided" }],
146
- isError: true,
147
- timestamp: Date.now(),
148
- } as ToolResultMessage);
149
- toolCallStatus.set(tc.id, ToolCallStatus.Resolved);
150
- }
151
- }
152
- pendingToolCalls = [];
153
- }
182
+ flushPendingToolCalls(messageTimestamp);
183
+ flushPendingAbortedToolCalls();
154
184
 
155
- // For errored/aborted assistant messages: keep tool calls intact,
156
- // inject synthetic "aborted" results, and add guidance marker.
157
- // This preserves structure so the model knows what was attempted.
158
185
  const assistantMsg = msg as AssistantMessage;
159
186
  const toolCalls = assistantMsg.content.filter(b => b.type === "toolCall") as ToolCall[];
160
187
 
161
188
  if (assistantMsg.stopReason === "error" || assistantMsg.stopReason === "aborted") {
162
- // Push the assistant message with tool calls intact
189
+ // Keep the assistant message with tool calls intact. If real tool results follow, preserve them;
190
+ // otherwise synthesize aborted results before the next turn boundary.
163
191
  result.push(msg);
164
-
165
- // Inject synthetic "aborted" results for each tool call
166
- for (const tc of toolCalls) {
167
- toolCallStatus.set(tc.id, ToolCallStatus.Aborted);
168
- result.push({
169
- role: "toolResult",
170
- toolCallId: tc.id,
171
- toolName: tc.name,
172
- content: [{ type: "text", text: "aborted" }],
173
- isError: true,
174
- timestamp: assistantMsg.timestamp,
175
- } as ToolResultMessage);
176
- }
177
-
178
- // Inject turn-aborted guidance marker as developer message
179
- result.push({
180
- role: "developer",
181
- content: turnAbortedGuidance,
182
- timestamp: assistantMsg.timestamp + 1,
183
- } as DeveloperMessage);
184
-
192
+ pendingAbortedToolCalls = new Map(toolCalls.map(toolCall => [toolCall.id, toolCall] as const));
193
+ pendingAbortedTimestamp = assistantMsg.timestamp;
185
194
  continue;
186
195
  }
187
196
 
188
- // Track tool calls from this normal assistant message
189
197
  if (toolCalls.length > 0) {
190
198
  pendingToolCalls = toolCalls;
191
199
  }
192
200
 
193
201
  result.push(msg);
194
202
  } else if (msg.role === "toolResult") {
195
- // Skip tool results for aborted tool calls (we already injected synthetic ones)
203
+ if (pendingAbortedToolCalls.has(msg.toolCallId)) {
204
+ pendingAbortedToolCalls.delete(msg.toolCallId);
205
+ toolCallStatus.set(msg.toolCallId, ToolCallStatus.Resolved);
206
+ result.push(msg);
207
+ continue;
208
+ }
209
+
196
210
  if (toolCallStatus.get(msg.toolCallId) === ToolCallStatus.Aborted) continue;
197
211
  toolCallStatus.set(msg.toolCallId, ToolCallStatus.Resolved);
198
212
  result.push(msg);
199
213
  } else if (msg.role === "user" || msg.role === "developer") {
200
- // User/developer message interrupts tool flow - insert synthetic results for orphaned calls
201
- if (pendingToolCalls.length > 0) {
202
- for (const tc of pendingToolCalls) {
203
- if (!toolCallStatus.has(tc.id)) {
204
- result.push({
205
- role: "toolResult",
206
- toolCallId: tc.id,
207
- toolName: tc.name,
208
- content: [{ type: "text", text: "No result provided" }],
209
- isError: true,
210
- timestamp: Date.now(),
211
- } as ToolResultMessage);
212
- toolCallStatus.set(tc.id, ToolCallStatus.Resolved);
213
- }
214
- }
215
- pendingToolCalls = [];
216
- }
214
+ flushPendingToolCalls(messageTimestamp);
215
+ flushPendingAbortedToolCalls();
217
216
  result.push(msg);
218
217
  } else {
218
+ flushPendingToolCalls(messageTimestamp);
219
+ flushPendingAbortedToolCalls();
219
220
  result.push(msg);
220
221
  }
221
222
  }
222
223
 
224
+ flushPendingToolCalls(Date.now());
225
+ flushPendingAbortedToolCalls();
226
+
223
227
  return result;
224
228
  }