@oh-my-pi/pi-agent-core 14.7.1 → 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.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,12 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [14.7.2] - 2026-05-06
6
+ ### Added
7
+
8
+ - Added `loadMode` option to `AgentTool` to mark built-in tools as `essential` for initial loading or `discoverable` for search activation
9
+ - Added optional `summary` field to `AgentTool` definitions for one-line text used in tool discovery indexes
10
+
5
11
  ## [14.7.0] - 2026-05-04
6
12
  ### Breaking Changes
7
13
 
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.1",
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.1",
39
- "@oh-my-pi/pi-natives": "14.7.1",
40
- "@oh-my-pi/pi-utils": "14.7.1"
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,
package/src/types.ts CHANGED
@@ -248,6 +248,10 @@ export interface AgentTool<TParameters extends TSchema = TSchema, TDetails = any
248
248
  hidden?: boolean;
249
249
  /** If true, tool can stage a pending action that requires explicit resolution via the resolve tool. */
250
250
  deferrable?: boolean;
251
+ /** Built-in tool loading behavior. "essential" loads initially; "discoverable" can be activated by tool search. */
252
+ loadMode?: "essential" | "discoverable";
253
+ /** Short one-line summary used for tool discovery indexes. */
254
+ summary?: string;
251
255
  /** If true, tool execution ignores abort signals (runs to completion) */
252
256
  nonAbortable?: boolean;
253
257
  /**