@oh-my-pi/pi-agent-core 14.5.13 → 14.6.0

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
+ ## [14.6.0] - 2026-05-02
6
+ ### Fixed
7
+
8
+ - Fixed request cancellation before provider events by emitting an aborted assistant message and ending the stream with `stopReason: "aborted"`
9
+
5
10
  ## [14.5.10] - 2026-04-30
6
11
 
7
12
  ### Added
@@ -328,4 +333,4 @@ Initial release under @oh-my-pi scope. See previous releases at [badlogic/pi-mon
328
333
 
329
334
  - `Agent` constructor now has all options optional (empty options use defaults).
330
335
 
331
- - `queueMessage()` is now synchronous (no longer returns a Promise).
336
+ - `queueMessage()` is now synchronous (no longer returns a Promise).
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": "14.5.13",
4
+ "version": "14.6.0",
5
5
  "description": "General-purpose agent with transport abstraction, state management, and attachment support",
6
6
  "homepage": "https://github.com/can1357/oh-my-pi",
7
7
  "author": "Can Boluk",
@@ -35,9 +35,9 @@
35
35
  "fmt": "biome format --write ."
36
36
  },
37
37
  "dependencies": {
38
- "@oh-my-pi/pi-ai": "14.5.13",
39
- "@oh-my-pi/pi-natives": "14.5.13",
40
- "@oh-my-pi/pi-utils": "14.5.13"
38
+ "@oh-my-pi/pi-ai": "14.6.0",
39
+ "@oh-my-pi/pi-natives": "14.6.0",
40
+ "@oh-my-pi/pi-utils": "14.6.0"
41
41
  },
42
42
  "devDependencies": {
43
43
  "@sinclair/typebox": "^0.34.49",
package/src/agent-loop.ts CHANGED
@@ -4,6 +4,7 @@
4
4
  */
5
5
  import {
6
6
  type AssistantMessage,
7
+ type AssistantMessageEvent,
7
8
  type Context,
8
9
  EventStream,
9
10
  streamSimple,
@@ -348,38 +349,23 @@ async function streamAssistantResponse(
348
349
  let partialMessage: AssistantMessage | null = null;
349
350
  let addedPartial = false;
350
351
 
351
- for await (const event of response) {
352
+ const responseIterator = response[Symbol.asyncIterator]();
353
+ while (true) {
354
+ const read = await readResponseEvent(responseIterator, signal);
355
+ if (read.type === "aborted") {
356
+ return emitAbortedAssistantMessage(partialMessage, addedPartial, context, config, stream);
357
+ }
358
+ if (read.type === "error") {
359
+ throw read.error;
360
+ }
361
+ if (read.result.done) {
362
+ break;
363
+ }
364
+
365
+ const event = read.result.value;
352
366
  // Check for abort signal before processing each event
353
367
  if (signal?.aborted) {
354
- const errorMessage = "Request was aborted";
355
- const abortedMessage: AssistantMessage = partialMessage
356
- ? { ...partialMessage, stopReason: "aborted", errorMessage }
357
- : {
358
- role: "assistant",
359
- content: [],
360
- api: config.model.api,
361
- provider: config.model.provider,
362
- model: config.model.id,
363
- usage: {
364
- input: 0,
365
- output: 0,
366
- cacheRead: 0,
367
- cacheWrite: 0,
368
- totalTokens: 0,
369
- cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
370
- },
371
- stopReason: "aborted",
372
- errorMessage,
373
- timestamp: Date.now(),
374
- };
375
- if (addedPartial) {
376
- context.messages[context.messages.length - 1] = abortedMessage;
377
- } else {
378
- context.messages.push(abortedMessage);
379
- stream.push({ type: "message_start", message: { ...abortedMessage } });
380
- }
381
- stream.push({ type: "message_end", message: abortedMessage });
382
- return abortedMessage;
368
+ return emitAbortedAssistantMessage(partialMessage, addedPartial, context, config, stream);
383
369
  }
384
370
 
385
371
  switch (event.type) {
@@ -434,6 +420,83 @@ async function streamAssistantResponse(
434
420
  return await response.result();
435
421
  }
436
422
 
423
+ type ResponseEventRead =
424
+ | { type: "event"; result: IteratorResult<AssistantMessageEvent> }
425
+ | { type: "error"; error: unknown }
426
+ | { type: "aborted" };
427
+
428
+ async function readResponseEvent(
429
+ iterator: AsyncIterator<AssistantMessageEvent>,
430
+ signal: AbortSignal | undefined,
431
+ ): Promise<ResponseEventRead> {
432
+ if (!signal) {
433
+ return { type: "event", result: await iterator.next() };
434
+ }
435
+ if (signal.aborted) {
436
+ const returnPromise = iterator.return?.();
437
+ if (returnPromise) void returnPromise.catch(() => {});
438
+ return { type: "aborted" };
439
+ }
440
+
441
+ const { promise: abortPromise, resolve: resolveAbort } = Promise.withResolvers<ResponseEventRead>();
442
+ const onAbort = () => resolveAbort({ type: "aborted" });
443
+ signal.addEventListener("abort", onAbort, { once: true });
444
+
445
+ const eventPromise = iterator.next().then(
446
+ result => ({ type: "event" as const, result }),
447
+ error => ({ type: "error" as const, error }),
448
+ );
449
+
450
+ try {
451
+ const read = await Promise.race([eventPromise, abortPromise]);
452
+ if (read.type === "aborted") {
453
+ const returnPromise = iterator.return?.();
454
+ if (returnPromise) void returnPromise.catch(() => {});
455
+ }
456
+ return read;
457
+ } finally {
458
+ signal.removeEventListener("abort", onAbort);
459
+ }
460
+ }
461
+
462
+ function emitAbortedAssistantMessage(
463
+ partialMessage: AssistantMessage | null,
464
+ addedPartial: boolean,
465
+ context: AgentContext,
466
+ config: AgentLoopConfig,
467
+ stream: EventStream<AgentEvent, AgentMessage[]>,
468
+ ): AssistantMessage {
469
+ const errorMessage = "Request was aborted";
470
+ const abortedMessage: AssistantMessage = partialMessage
471
+ ? { ...partialMessage, stopReason: "aborted", errorMessage }
472
+ : {
473
+ role: "assistant",
474
+ content: [],
475
+ api: config.model.api,
476
+ provider: config.model.provider,
477
+ model: config.model.id,
478
+ usage: {
479
+ input: 0,
480
+ output: 0,
481
+ cacheRead: 0,
482
+ cacheWrite: 0,
483
+ totalTokens: 0,
484
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
485
+ },
486
+ stopReason: "aborted",
487
+ errorMessage,
488
+ timestamp: Date.now(),
489
+ };
490
+ if (addedPartial) {
491
+ context.messages[context.messages.length - 1] = abortedMessage;
492
+ } else {
493
+ context.messages.push(abortedMessage);
494
+ stream.push({ type: "message_start", message: { ...abortedMessage } });
495
+ }
496
+ stream.push({ type: "message_end", message: abortedMessage });
497
+ return abortedMessage;
498
+ }
499
+
437
500
  /**
438
501
  * Execute tool calls from an assistant message.
439
502
  */