@oh-my-pi/pi-agent-core 14.7.2 → 14.7.3

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 (2) hide show
  1. package/package.json +4 -4
  2. package/src/agent-loop.ts +80 -95
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.7.2",
4
+ "version": "14.7.3",
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.7.2",
39
- "@oh-my-pi/pi-natives": "14.7.2",
40
- "@oh-my-pi/pi-utils": "14.7.2"
38
+ "@oh-my-pi/pi-ai": "14.7.3",
39
+ "@oh-my-pi/pi-natives": "14.7.3",
40
+ "@oh-my-pi/pi-utils": "14.7.3"
41
41
  },
42
42
  "devDependencies": {
43
43
  "@sinclair/typebox": "^0.34.49",
package/src/agent-loop.ts CHANGED
@@ -22,6 +22,9 @@ import type {
22
22
  StreamFn,
23
23
  } from "./types";
24
24
 
25
+ /** Sentinel returned by the abort race in `streamAssistantResponse`. */
26
+ const ABORTED: unique symbol = Symbol("agent-loop-aborted");
27
+
25
28
  /**
26
29
  * Start an agent loop with a new prompt message.
27
30
  * The prompt is added to the context and events are emitted for it.
@@ -360,115 +363,97 @@ async function streamAssistantResponse(
360
363
  let addedPartial = false;
361
364
 
362
365
  const responseIterator = response[Symbol.asyncIterator]();
363
- while (true) {
364
- const read = await readResponseEvent(responseIterator, signal);
365
- if (read.type === "aborted") {
366
+ // Set up a single abort race: register the abort listener once for the whole
367
+ // stream and reuse the same race promise for every iterator.next() instead of
368
+ // allocating Promise.withResolvers and add/removeEventListener per event.
369
+ let abortRacePromise: Promise<typeof ABORTED> | undefined;
370
+ let detachAbortListener: (() => void) | undefined;
371
+ if (signal) {
372
+ if (signal.aborted) {
366
373
  return emitAbortedAssistantMessage(partialMessage, addedPartial, context, config, stream);
367
374
  }
368
- if (read.type === "error") {
369
- throw read.error;
370
- }
371
- if (read.result.done) {
372
- break;
373
- }
375
+ const { promise, resolve } = Promise.withResolvers<typeof ABORTED>();
376
+ const onAbort = () => resolve(ABORTED);
377
+ signal.addEventListener("abort", onAbort, { once: true });
378
+ abortRacePromise = promise;
379
+ detachAbortListener = () => signal.removeEventListener("abort", onAbort);
380
+ }
374
381
 
375
- const event = read.result.value;
376
- // Check for abort signal before processing each event
377
- if (signal?.aborted) {
378
- return emitAbortedAssistantMessage(partialMessage, addedPartial, context, config, stream);
379
- }
382
+ try {
383
+ while (true) {
384
+ let next: IteratorResult<AssistantMessageEvent>;
385
+ if (abortRacePromise) {
386
+ const result = await Promise.race([responseIterator.next(), abortRacePromise]);
387
+ if (result === ABORTED) {
388
+ responseIterator.return?.()?.catch(() => {});
389
+ return emitAbortedAssistantMessage(partialMessage, addedPartial, context, config, stream);
390
+ }
391
+ next = result;
392
+ } else {
393
+ next = await responseIterator.next();
394
+ }
395
+ if (signal?.aborted) {
396
+ return emitAbortedAssistantMessage(partialMessage, addedPartial, context, config, stream);
397
+ }
398
+ if (next.done) break;
380
399
 
381
- switch (event.type) {
382
- case "start":
383
- partialMessage = event.partial;
384
- context.messages.push(partialMessage);
385
- addedPartial = true;
386
- stream.push({ type: "message_start", message: { ...partialMessage } });
387
- break;
388
-
389
- case "text_start":
390
- case "text_delta":
391
- case "text_end":
392
- case "thinking_start":
393
- case "thinking_delta":
394
- case "thinking_end":
395
- case "toolcall_start":
396
- case "toolcall_delta":
397
- case "toolcall_end":
398
- if (partialMessage) {
400
+ const event = next.value;
401
+
402
+ switch (event.type) {
403
+ case "start":
399
404
  partialMessage = event.partial;
400
- context.messages[context.messages.length - 1] = partialMessage;
401
- config.onAssistantMessageEvent?.(partialMessage, event);
402
- if (signal?.aborted) {
403
- continue;
405
+ context.messages.push(partialMessage);
406
+ addedPartial = true;
407
+ stream.push({ type: "message_start", message: { ...partialMessage } });
408
+ break;
409
+
410
+ case "text_start":
411
+ case "text_delta":
412
+ case "text_end":
413
+ case "thinking_start":
414
+ case "thinking_delta":
415
+ case "thinking_end":
416
+ case "toolcall_start":
417
+ case "toolcall_delta":
418
+ case "toolcall_end":
419
+ if (partialMessage) {
420
+ partialMessage = event.partial;
421
+ context.messages[context.messages.length - 1] = partialMessage;
422
+ config.onAssistantMessageEvent?.(partialMessage, event);
423
+ if (signal?.aborted) {
424
+ continue;
425
+ }
426
+ stream.push({
427
+ type: "message_update",
428
+ assistantMessageEvent: event,
429
+ message: { ...partialMessage },
430
+ });
404
431
  }
405
- stream.push({
406
- type: "message_update",
407
- assistantMessageEvent: event,
408
- message: { ...partialMessage },
409
- });
410
- }
411
- break;
412
-
413
- case "done":
414
- case "error": {
415
- const finalMessage = await response.result();
416
- if (addedPartial) {
417
- context.messages[context.messages.length - 1] = finalMessage;
418
- } else {
419
- context.messages.push(finalMessage);
420
- }
421
- if (!addedPartial) {
422
- stream.push({ type: "message_start", message: { ...finalMessage } });
432
+ break;
433
+
434
+ case "done":
435
+ case "error": {
436
+ const finalMessage = await response.result();
437
+ if (addedPartial) {
438
+ context.messages[context.messages.length - 1] = finalMessage;
439
+ } else {
440
+ context.messages.push(finalMessage);
441
+ }
442
+ if (!addedPartial) {
443
+ stream.push({ type: "message_start", message: { ...finalMessage } });
444
+ }
445
+ stream.push({ type: "message_end", message: finalMessage });
446
+ return finalMessage;
423
447
  }
424
- stream.push({ type: "message_end", message: finalMessage });
425
- return finalMessage;
426
448
  }
427
449
  }
450
+ } finally {
451
+ detachAbortListener?.();
428
452
  }
429
453
 
430
454
  return await response.result();
431
455
  }
432
456
 
433
- type ResponseEventRead =
434
- | { type: "event"; result: IteratorResult<AssistantMessageEvent> }
435
- | { type: "error"; error: unknown }
436
- | { type: "aborted" };
437
-
438
- async function readResponseEvent(
439
- iterator: AsyncIterator<AssistantMessageEvent>,
440
- signal: AbortSignal | undefined,
441
- ): Promise<ResponseEventRead> {
442
- if (!signal) {
443
- return { type: "event", result: await iterator.next() };
444
- }
445
- if (signal.aborted) {
446
- const returnPromise = iterator.return?.();
447
- if (returnPromise) void returnPromise.catch(() => {});
448
- return { type: "aborted" };
449
- }
450
-
451
- const { promise: abortPromise, resolve: resolveAbort } = Promise.withResolvers<ResponseEventRead>();
452
- const onAbort = () => resolveAbort({ type: "aborted" });
453
- signal.addEventListener("abort", onAbort, { once: true });
454
-
455
- const eventPromise = iterator.next().then(
456
- result => ({ type: "event" as const, result }),
457
- error => ({ type: "error" as const, error }),
458
- );
459
-
460
- try {
461
- const read = await Promise.race([eventPromise, abortPromise]);
462
- if (read.type === "aborted") {
463
- const returnPromise = iterator.return?.();
464
- if (returnPromise) void returnPromise.catch(() => {});
465
- }
466
- return read;
467
- } finally {
468
- signal.removeEventListener("abort", onAbort);
469
- }
470
- }
471
-
472
457
  function emitAbortedAssistantMessage(
473
458
  partialMessage: AssistantMessage | null,
474
459
  addedPartial: boolean,