@oh-my-pi/pi-agent-core 10.3.2 → 10.5.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oh-my-pi/pi-agent-core",
3
- "version": "10.3.2",
3
+ "version": "10.5.0",
4
4
  "description": "General-purpose agent with transport abstraction, state management, and attachment support",
5
5
  "type": "module",
6
6
  "main": "./src/index.ts",
@@ -24,9 +24,9 @@
24
24
  "test": "bun test"
25
25
  },
26
26
  "dependencies": {
27
- "@oh-my-pi/pi-ai": "10.3.2",
28
- "@oh-my-pi/pi-tui": "10.3.2",
29
- "@oh-my-pi/pi-utils": "10.3.2"
27
+ "@oh-my-pi/pi-ai": "10.5.0",
28
+ "@oh-my-pi/pi-tui": "10.5.0",
29
+ "@oh-my-pi/pi-utils": "10.5.0"
30
30
  },
31
31
  "keywords": [
32
32
  "ai",
@@ -47,6 +47,6 @@
47
47
  },
48
48
  "devDependencies": {
49
49
  "@sinclair/typebox": "^0.34.48",
50
- "@types/node": "^25.0.10"
50
+ "@types/node": "^25.2.0"
51
51
  }
52
52
  }
package/src/agent-loop.ts CHANGED
@@ -372,17 +372,58 @@ async function executeToolCalls(
372
372
  getToolContext?: AgentLoopConfig["getToolContext"],
373
373
  interruptMode: AgentLoopConfig["interruptMode"] = "immediate",
374
374
  ): Promise<{ toolResults: ToolResultMessage[]; steeringMessages?: AgentMessage[] }> {
375
- const toolCalls = assistantMessage.content.filter(c => c.type === "toolCall");
375
+ type ToolCallContent = Extract<AssistantMessage["content"][number], { type: "toolCall" }>;
376
+ const toolCalls = assistantMessage.content.filter((c): c is ToolCallContent => c.type === "toolCall");
376
377
  const results: ToolResultMessage[] = [];
377
378
  let steeringMessages: AgentMessage[] | undefined;
378
379
  const shouldInterruptImmediately = interruptMode !== "wait";
379
380
  const toolCallInfos = toolCalls.map(call => ({ id: call.id, name: call.name }));
380
381
  const batchId = `${assistantMessage.timestamp ?? Date.now()}_${toolCalls[0]?.id ?? "batch"}`;
382
+ const steeringAbortController = new AbortController();
383
+ const toolSignal = signal
384
+ ? AbortSignal.any([signal, steeringAbortController.signal])
385
+ : steeringAbortController.signal;
386
+ const interruptState = { triggered: false };
387
+ let steeringCheck: Promise<void> | null = null;
388
+
389
+ const checkSteering = async (): Promise<void> => {
390
+ if (!shouldInterruptImmediately || !getSteeringMessages || interruptState.triggered) {
391
+ return;
392
+ }
393
+ if (steeringCheck) {
394
+ await steeringCheck;
395
+ return;
396
+ }
397
+ steeringCheck = (async () => {
398
+ const steering = await getSteeringMessages();
399
+ if (steering.length > 0) {
400
+ steeringMessages = steering;
401
+ interruptState.triggered = true;
402
+ steeringAbortController.abort();
403
+ }
404
+ })().finally(() => {
405
+ steeringCheck = null;
406
+ });
407
+ await steeringCheck;
408
+ };
381
409
 
382
- for (let index = 0; index < toolCalls.length; index++) {
383
- const toolCall = toolCalls[index];
384
- const tool = tools?.find(t => t.name === toolCall.name);
410
+ const records = toolCalls.map(toolCall => ({
411
+ toolCall,
412
+ tool: tools?.find(t => t.name === toolCall.name),
413
+ started: false,
414
+ result: undefined as AgentToolResult<any> | undefined,
415
+ isError: false,
416
+ skipped: false,
417
+ }));
418
+
419
+ const runTool = async (record: (typeof records)[number], index: number): Promise<void> => {
420
+ if (interruptState.triggered) {
421
+ record.skipped = true;
422
+ return;
423
+ }
385
424
 
425
+ const { toolCall, tool } = record;
426
+ record.started = true;
386
427
  stream.push({
387
428
  type: "tool_execution_start",
388
429
  toolCallId: toolCall.id,
@@ -397,7 +438,6 @@ async function executeToolCalls(
397
438
  if (!tool) throw new Error(`Tool ${toolCall.name} not found`);
398
439
 
399
440
  const validatedArgs = validateToolArguments(tool, toolCall);
400
-
401
441
  const toolContext = getToolContext
402
442
  ? getToolContext({
403
443
  batchId,
@@ -409,8 +449,9 @@ async function executeToolCalls(
409
449
  result = await tool.execute(
410
450
  toolCall.id,
411
451
  validatedArgs,
412
- tool.nonAbortable ? undefined : signal,
452
+ tool.nonAbortable ? undefined : toolSignal,
413
453
  partialResult => {
454
+ if (interruptState.triggered) return;
414
455
  stream.push({
415
456
  type: "tool_execution_update",
416
457
  toolCallId: toolCall.id,
@@ -429,6 +470,49 @@ async function executeToolCalls(
429
470
  isError = true;
430
471
  }
431
472
 
473
+ if (!interruptState.triggered) {
474
+ record.result = result;
475
+ record.isError = isError;
476
+ } else {
477
+ record.skipped = true;
478
+ }
479
+
480
+ await checkSteering();
481
+ };
482
+
483
+ let lastExclusive: Promise<void> = Promise.resolve();
484
+ let sharedTasks: Promise<void>[] = [];
485
+ const tasks: Promise<void>[] = [];
486
+
487
+ for (let index = 0; index < records.length; index++) {
488
+ const record = records[index];
489
+ const concurrency = record.tool?.concurrency ?? "shared";
490
+ const start = concurrency === "exclusive" ? Promise.all([lastExclusive, ...sharedTasks]) : lastExclusive;
491
+ const task = start.then(() => runTool(record, index));
492
+ tasks.push(task);
493
+ if (concurrency === "exclusive") {
494
+ lastExclusive = task;
495
+ sharedTasks = [];
496
+ } else {
497
+ sharedTasks.push(task);
498
+ }
499
+ }
500
+
501
+ await Promise.allSettled(tasks);
502
+
503
+ for (const record of records) {
504
+ const toolCall = record.toolCall;
505
+ const shouldSkip = record.skipped || record.result === undefined;
506
+ const result = shouldSkip ? createSkippedToolResult() : (record.result ?? createSkippedToolResult());
507
+ const isError = shouldSkip ? true : record.isError;
508
+ if (!record.started) {
509
+ stream.push({
510
+ type: "tool_execution_start",
511
+ toolCallId: toolCall.id,
512
+ toolName: toolCall.name,
513
+ args: toolCall.arguments,
514
+ });
515
+ }
432
516
  stream.push({
433
517
  type: "tool_execution_end",
434
518
  toolCallId: toolCall.id,
@@ -450,61 +534,16 @@ async function executeToolCalls(
450
534
  results.push(toolResultMessage);
451
535
  stream.push({ type: "message_start", message: toolResultMessage });
452
536
  stream.push({ type: "message_end", message: toolResultMessage });
453
-
454
- // Check for steering messages - skip remaining tools if user interrupted
455
- if (shouldInterruptImmediately && getSteeringMessages) {
456
- const steering = await getSteeringMessages();
457
- if (steering.length > 0) {
458
- steeringMessages = steering;
459
- const remainingCalls = toolCalls.slice(index + 1);
460
- for (const skipped of remainingCalls) {
461
- results.push(skipToolCall(skipped, stream));
462
- }
463
- break;
464
- }
465
- }
466
537
  }
467
538
 
468
539
  return { toolResults: results, steeringMessages };
469
540
  }
470
541
 
471
- function skipToolCall(
472
- toolCall: Extract<AssistantMessage["content"][number], { type: "toolCall" }>,
473
- stream: EventStream<AgentEvent, AgentMessage[]>,
474
- ): ToolResultMessage {
475
- const result: AgentToolResult<any> = {
542
+ function createSkippedToolResult(): AgentToolResult<any> {
543
+ return {
476
544
  content: [{ type: "text", text: "Skipped due to queued user message." }],
477
545
  details: {},
478
546
  };
479
-
480
- stream.push({
481
- type: "tool_execution_start",
482
- toolCallId: toolCall.id,
483
- toolName: toolCall.name,
484
- args: toolCall.arguments,
485
- });
486
- stream.push({
487
- type: "tool_execution_end",
488
- toolCallId: toolCall.id,
489
- toolName: toolCall.name,
490
- result,
491
- isError: true,
492
- });
493
-
494
- const toolResultMessage: ToolResultMessage = {
495
- role: "toolResult",
496
- toolCallId: toolCall.id,
497
- toolName: toolCall.name,
498
- content: result.content,
499
- details: {},
500
- isError: true,
501
- timestamp: Date.now(),
502
- };
503
-
504
- stream.push({ type: "message_start", message: toolResultMessage });
505
- stream.push({ type: "message_end", message: toolResultMessage });
506
-
507
- return toolResultMessage;
508
547
  }
509
548
 
510
549
  /**
package/src/types.ts CHANGED
@@ -206,6 +206,12 @@ export interface AgentTool<TParameters extends TSchema = TSchema, TDetails = any
206
206
  hidden?: boolean;
207
207
  /** If true, tool execution ignores abort signals (runs to completion) */
208
208
  nonAbortable?: boolean;
209
+ /**
210
+ * Concurrency mode for tool scheduling when multiple calls are in one turn.
211
+ * - "shared": can run alongside other shared tools (default)
212
+ * - "exclusive": runs alone; other tools wait until it finishes
213
+ */
214
+ concurrency?: "shared" | "exclusive";
209
215
  execute: (
210
216
  toolCallId: string,
211
217
  params: Static<TParameters>,