@oh-my-pi/pi-agent-core 10.3.2 → 10.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/package.json +5 -5
- package/src/agent-loop.ts +92 -53
- package/src/types.ts +6 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oh-my-pi/pi-agent-core",
|
|
3
|
-
"version": "10.
|
|
3
|
+
"version": "10.6.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.
|
|
28
|
-
"@oh-my-pi/pi-tui": "10.
|
|
29
|
-
"@oh-my-pi/pi-utils": "10.
|
|
27
|
+
"@oh-my-pi/pi-ai": "10.6.0",
|
|
28
|
+
"@oh-my-pi/pi-tui": "10.6.0",
|
|
29
|
+
"@oh-my-pi/pi-utils": "10.6.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
|
|
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
|
-
|
|
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
|
-
|
|
383
|
-
|
|
384
|
-
|
|
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 :
|
|
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
|
|
472
|
-
|
|
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>,
|