@oh-my-pi/pi-agent-core 17.1.5 → 17.1.7
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 +16 -0
- package/dist/types/agent.d.ts +21 -1
- package/dist/types/types.d.ts +63 -7
- package/package.json +7 -7
- package/src/agent-loop.ts +393 -204
- package/src/agent.ts +82 -1
- package/src/types.ts +73 -7
package/src/agent.ts
CHANGED
|
@@ -39,6 +39,7 @@ import {
|
|
|
39
39
|
import type { AppendOnlyContextManager } from "./append-only-context";
|
|
40
40
|
import { isProviderRefusalMessage } from "./replay-policy";
|
|
41
41
|
import type {
|
|
42
|
+
AgentBeforeModelCall,
|
|
42
43
|
AgentContext,
|
|
43
44
|
AgentEvent,
|
|
44
45
|
AgentLoopConfig,
|
|
@@ -267,6 +268,8 @@ export interface AgentOptions {
|
|
|
267
268
|
abortOnFabricatedToolResult?: boolean;
|
|
268
269
|
/** Dynamic tool-choice directive (hard {@link ToolChoice} or {@link SoftToolRequirement}), resolved once per turn. */
|
|
269
270
|
getToolChoice?: () => ToolChoiceDirective | undefined;
|
|
271
|
+
/** Reject a deferred hard choice when its named tool is no longer active. */
|
|
272
|
+
onToolChoiceUnavailable?: () => void;
|
|
270
273
|
|
|
271
274
|
/**
|
|
272
275
|
* Cursor exec handlers for local tool execution.
|
|
@@ -409,6 +412,9 @@ export class Agent {
|
|
|
409
412
|
#dialect?: Dialect;
|
|
410
413
|
#abortOnFabricatedToolResult?: boolean;
|
|
411
414
|
#getToolChoice?: () => ToolChoiceDirective | undefined;
|
|
415
|
+
#onToolChoiceUnavailable?: () => void;
|
|
416
|
+
#softToolRequirementState: NonNullable<AgentLoopConfig["softToolRequirementState"]> = { escalations: 0 };
|
|
417
|
+
#deferredToolChoice?: ToolChoice;
|
|
412
418
|
#onPayload?: SimpleStreamOptions["onPayload"];
|
|
413
419
|
#onResponse?: SimpleStreamOptions["onResponse"];
|
|
414
420
|
#onSseEvent?: SimpleStreamOptions["onSseEvent"];
|
|
@@ -416,6 +422,8 @@ export class Agent {
|
|
|
416
422
|
#onHarmonyLeak?: (event: HarmonyAuditEvent) => void | Promise<void>;
|
|
417
423
|
#onBeforeYield?: () => Promise<void> | void;
|
|
418
424
|
#onTurnEnd?: (messages: AgentMessage[], signal?: AbortSignal, context?: AgentTurnEndContext) => Promise<void> | void;
|
|
425
|
+
#beforeModelCall?: AgentBeforeModelCall;
|
|
426
|
+
#additionalBeforeModelCalls = new Set<AgentBeforeModelCall>();
|
|
419
427
|
#asideMessageProvider?: () => AsideMessage[] | Promise<AsideMessage[]>;
|
|
420
428
|
#telemetry?: AgentLoopConfig["telemetry"];
|
|
421
429
|
#appendOnlyContext?: AppendOnlyContextManager;
|
|
@@ -490,6 +498,7 @@ export class Agent {
|
|
|
490
498
|
this.#dialect = opts.dialect;
|
|
491
499
|
this.#abortOnFabricatedToolResult = opts.abortOnFabricatedToolResult;
|
|
492
500
|
this.#getToolChoice = opts.getToolChoice;
|
|
501
|
+
this.#onToolChoiceUnavailable = opts.onToolChoiceUnavailable;
|
|
493
502
|
this.#onAssistantMessageEvent = opts.onAssistantMessageEvent;
|
|
494
503
|
this.#onHarmonyLeak = opts.onHarmonyLeak;
|
|
495
504
|
this.beforeToolCall = opts.beforeToolCall;
|
|
@@ -803,6 +812,28 @@ export class Agent {
|
|
|
803
812
|
this.#onTurnEnd = fn;
|
|
804
813
|
}
|
|
805
814
|
|
|
815
|
+
/**
|
|
816
|
+
* Install or replace the host pre-model-call gate; pass `undefined` to
|
|
817
|
+
* remove it. Gates are sampled when a run starts: installing the first
|
|
818
|
+
* gate while a run is in flight takes effect on the next run.
|
|
819
|
+
*/
|
|
820
|
+
setBeforeModelCall(fn: AgentBeforeModelCall | undefined): void {
|
|
821
|
+
this.#beforeModelCall = fn;
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
/**
|
|
825
|
+
* Add a pre-model callback without replacing callbacks owned by the host.
|
|
826
|
+
* Returns a disposer that removes only this callback. Like
|
|
827
|
+
* {@link setBeforeModelCall}, the first gate installed while a run is in
|
|
828
|
+
* flight takes effect on the next run.
|
|
829
|
+
*/
|
|
830
|
+
addBeforeModelCall(fn: AgentBeforeModelCall): () => void {
|
|
831
|
+
this.#additionalBeforeModelCalls.add(fn);
|
|
832
|
+
return () => {
|
|
833
|
+
this.#additionalBeforeModelCalls.delete(fn);
|
|
834
|
+
};
|
|
835
|
+
}
|
|
836
|
+
|
|
806
837
|
/**
|
|
807
838
|
* Provide a source of non-interrupting "aside" messages (e.g. background-job
|
|
808
839
|
* completions, late LSP diagnostics) drained at each step boundary. Never
|
|
@@ -928,10 +959,20 @@ export class Agent {
|
|
|
928
959
|
this.#followUpQueue = [];
|
|
929
960
|
}
|
|
930
961
|
|
|
962
|
+
/**
|
|
963
|
+
* Drop tool-directive state retained across a gate-stopped run: the
|
|
964
|
+
* deferred hard choice and the soft-requirement lifecycle.
|
|
965
|
+
*/
|
|
966
|
+
clearDeferredToolDirectives() {
|
|
967
|
+
this.#deferredToolChoice = undefined;
|
|
968
|
+
this.#softToolRequirementState = { escalations: 0 };
|
|
969
|
+
}
|
|
970
|
+
|
|
931
971
|
clearAllQueues() {
|
|
932
972
|
this.#steeringQueue = [];
|
|
933
973
|
this.#followUpQueue = [];
|
|
934
974
|
this.#notifySteeringWaiters();
|
|
975
|
+
this.clearDeferredToolDirectives();
|
|
935
976
|
}
|
|
936
977
|
|
|
937
978
|
hasQueuedMessages(): boolean {
|
|
@@ -1044,6 +1085,7 @@ export class Agent {
|
|
|
1044
1085
|
this.#steeringQueue = [];
|
|
1045
1086
|
this.#followUpQueue = [];
|
|
1046
1087
|
this.#notifySteeringWaiters();
|
|
1088
|
+
this.clearDeferredToolDirectives();
|
|
1047
1089
|
}
|
|
1048
1090
|
|
|
1049
1091
|
/** Send a prompt with an AgentMessage */
|
|
@@ -1219,13 +1261,28 @@ export class Agent {
|
|
|
1219
1261
|
return entry.toolResult;
|
|
1220
1262
|
};
|
|
1221
1263
|
|
|
1264
|
+
let claimedToolChoice: ToolChoice | undefined;
|
|
1222
1265
|
const getToolChoice = (): ToolChoiceDirective | undefined => {
|
|
1266
|
+
claimedToolChoice = undefined;
|
|
1267
|
+
const deferred = this.#deferredToolChoice;
|
|
1268
|
+
if (deferred !== undefined) {
|
|
1269
|
+
this.#deferredToolChoice = undefined;
|
|
1270
|
+
const active = refreshToolChoiceForActiveTools(deferred, this.#state.tools);
|
|
1271
|
+
if (active !== undefined) {
|
|
1272
|
+
claimedToolChoice = deferred;
|
|
1273
|
+
return active;
|
|
1274
|
+
}
|
|
1275
|
+
this.#onToolChoiceUnavailable?.();
|
|
1276
|
+
}
|
|
1277
|
+
|
|
1223
1278
|
const queued = this.#getToolChoice?.();
|
|
1224
1279
|
if (queued !== undefined) {
|
|
1225
1280
|
if (isSoftToolRequirement(queued)) {
|
|
1226
1281
|
return (this.#state.tools ?? []).some(tool => tool.name === queued.toolName) ? queued : undefined;
|
|
1227
1282
|
}
|
|
1228
|
-
|
|
1283
|
+
const active = refreshToolChoiceForActiveTools(queued, this.#state.tools);
|
|
1284
|
+
if (active !== undefined) claimedToolChoice = queued;
|
|
1285
|
+
return active;
|
|
1229
1286
|
}
|
|
1230
1287
|
return refreshToolChoiceForActiveTools(options?.toolChoice, this.#state.tools);
|
|
1231
1288
|
};
|
|
@@ -1268,6 +1325,18 @@ export class Agent {
|
|
|
1268
1325
|
context.systemPrompt = this.#state.systemPrompt;
|
|
1269
1326
|
context.tools = this.#toolsForModel(this.#state.model ?? model);
|
|
1270
1327
|
},
|
|
1328
|
+
beforeModelCall:
|
|
1329
|
+
this.#beforeModelCall || this.#additionalBeforeModelCalls.size > 0
|
|
1330
|
+
? async (context, signal) => {
|
|
1331
|
+
const result = (await this.#beforeModelCall?.(context, signal)) || undefined;
|
|
1332
|
+
if (result?.stop) return result;
|
|
1333
|
+
for (const callback of this.#additionalBeforeModelCalls) {
|
|
1334
|
+
const callbackResult = (await callback(context, signal)) || undefined;
|
|
1335
|
+
if (callbackResult?.stop) return callbackResult;
|
|
1336
|
+
}
|
|
1337
|
+
return undefined;
|
|
1338
|
+
}
|
|
1339
|
+
: undefined,
|
|
1271
1340
|
cursorExecHandlers: this.#cursorExecHandlers,
|
|
1272
1341
|
cursorOnToolResult,
|
|
1273
1342
|
cwd: this.#cwd,
|
|
@@ -1288,6 +1357,10 @@ export class Agent {
|
|
|
1288
1357
|
onHarmonyLeak: this.#onHarmonyLeak,
|
|
1289
1358
|
onTurnEnd: (messages, signal, context) => this.#onTurnEnd?.(messages, signal, context),
|
|
1290
1359
|
getToolChoice,
|
|
1360
|
+
softToolRequirementState: this.#softToolRequirementState,
|
|
1361
|
+
onToolChoiceRejected: () => {
|
|
1362
|
+
if (claimedToolChoice !== undefined) this.#deferredToolChoice = claimedToolChoice;
|
|
1363
|
+
},
|
|
1291
1364
|
getModel: () => this.#state.model ?? model,
|
|
1292
1365
|
getReasoning: () => this.#state.thinkingLevel,
|
|
1293
1366
|
getDisableReasoning: () => this.#state.disableReasoning,
|
|
@@ -1322,6 +1395,7 @@ export class Agent {
|
|
|
1322
1395
|
|
|
1323
1396
|
let partial: AgentMessage | null = null;
|
|
1324
1397
|
const completedToolCallIds = new Set<string>();
|
|
1398
|
+
let turnOpen = false;
|
|
1325
1399
|
|
|
1326
1400
|
try {
|
|
1327
1401
|
const stream = messages
|
|
@@ -1329,6 +1403,8 @@ export class Agent {
|
|
|
1329
1403
|
: agentLoopContinue(context, config, this.#abortController.signal, this.streamFn);
|
|
1330
1404
|
|
|
1331
1405
|
for await (const event of stream) {
|
|
1406
|
+
if (event.type === "turn_start") turnOpen = true;
|
|
1407
|
+
if (event.type === "turn_end") turnOpen = false;
|
|
1332
1408
|
// Update internal state based on events
|
|
1333
1409
|
switch (event.type) {
|
|
1334
1410
|
case "message_start":
|
|
@@ -1448,6 +1524,10 @@ export class Agent {
|
|
|
1448
1524
|
};
|
|
1449
1525
|
|
|
1450
1526
|
if (shouldEmitVisibleError) {
|
|
1527
|
+
if (!turnOpen) {
|
|
1528
|
+
this.#emit({ type: "turn_start" });
|
|
1529
|
+
turnOpen = true;
|
|
1530
|
+
}
|
|
1451
1531
|
if (!hadAssistantStart) {
|
|
1452
1532
|
this.#state.streamMessage = errorMsg;
|
|
1453
1533
|
this.#emit({ type: "message_start", message: errorMsg });
|
|
@@ -1489,6 +1569,7 @@ export class Agent {
|
|
|
1489
1569
|
toolResults.push(toolResult);
|
|
1490
1570
|
}
|
|
1491
1571
|
this.#emit({ type: "turn_end", message: errorMsg, toolResults });
|
|
1572
|
+
turnOpen = false;
|
|
1492
1573
|
this.#emit({ type: "agent_end", messages: [errorMsg, ...toolResults] });
|
|
1493
1574
|
} else {
|
|
1494
1575
|
this.appendMessage(errorMsg);
|
package/src/types.ts
CHANGED
|
@@ -48,6 +48,24 @@ export interface AgentTurnEndContext {
|
|
|
48
48
|
willContinue: boolean;
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
+
export interface AgentPreModelCallStop {
|
|
52
|
+
/** Stop the agent loop before sending the next provider request. */
|
|
53
|
+
stop: true;
|
|
54
|
+
/** Optional owner-facing reason, logged by the loop when it stops. */
|
|
55
|
+
reason?: string;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export type AgentPreModelCallResult = AgentPreModelCallStop | undefined;
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* A pre-model-call gate. Return {@link AgentPreModelCallStop} to refuse the
|
|
62
|
+
* request, or nothing to proceed; the signal aborts with the run.
|
|
63
|
+
*/
|
|
64
|
+
export type AgentBeforeModelCall = (
|
|
65
|
+
context: Context,
|
|
66
|
+
signal?: AbortSignal,
|
|
67
|
+
) => AgentPreModelCallResult | void | Promise<AgentPreModelCallResult | void>;
|
|
68
|
+
|
|
51
69
|
/**
|
|
52
70
|
* A soft tool requirement: the host wants `toolName` called before the loop
|
|
53
71
|
* runs other tools or yields, but WITHOUT paying the forced-`toolChoice` cost
|
|
@@ -87,6 +105,13 @@ export interface SoftToolRequirement {
|
|
|
87
105
|
*/
|
|
88
106
|
export type ToolChoiceDirective = ToolChoice | SoftToolRequirement;
|
|
89
107
|
|
|
108
|
+
/** Mutable soft-requirement lifecycle retained across stopped agent runs. */
|
|
109
|
+
export interface SoftToolRequirementState {
|
|
110
|
+
id?: string;
|
|
111
|
+
forcedToolChoice?: ToolChoice;
|
|
112
|
+
escalations: number;
|
|
113
|
+
}
|
|
114
|
+
|
|
90
115
|
/** True when a {@link ToolChoiceDirective} is a soft requirement, not a hard choice. */
|
|
91
116
|
export function isSoftToolRequirement(directive: ToolChoiceDirective | undefined): directive is SoftToolRequirement {
|
|
92
117
|
return typeof directive === "object" && directive !== null && (directive as SoftToolRequirement).soft === true;
|
|
@@ -276,9 +301,23 @@ export interface AgentLoopConfig extends SimpleStreamOptions {
|
|
|
276
301
|
/**
|
|
277
302
|
* Refreshes prompt/tool context from live session state before each model call.
|
|
278
303
|
* Use this when tool availability or the system prompt can change mid-turn.
|
|
304
|
+
*
|
|
305
|
+
* Runs after pending messages are folded in and before provider conversion.
|
|
306
|
+
* Mutate the agent context here; use `beforeModelCall` to inspect the
|
|
307
|
+
* provider-bound context.
|
|
279
308
|
*/
|
|
280
309
|
syncContextBeforeModelCall?: (context: AgentContext) => void | Promise<void>;
|
|
281
310
|
|
|
311
|
+
/**
|
|
312
|
+
* Asked after the complete provider context has been built, including
|
|
313
|
+
* message conversion, provider transforms, normalized tools, and owned
|
|
314
|
+
* dialect prompt injection. Returning {@link AgentPreModelCallStop} ends
|
|
315
|
+
* the stream without emitting `turn_start`, so no turn is left open and no
|
|
316
|
+
* request is billed. Return nothing to proceed. The signal aborts when
|
|
317
|
+
* the run is canceled or its deadline expires.
|
|
318
|
+
*/
|
|
319
|
+
beforeModelCall?: AgentBeforeModelCall;
|
|
320
|
+
|
|
282
321
|
/**
|
|
283
322
|
* Optional transform applied to tool call arguments before execution.
|
|
284
323
|
* Use for deobfuscating secrets or rewriting arguments.
|
|
@@ -356,6 +395,19 @@ export interface AgentLoopConfig extends SimpleStreamOptions {
|
|
|
356
395
|
*/
|
|
357
396
|
getToolChoice?: () => ToolChoiceDirective | undefined;
|
|
358
397
|
|
|
398
|
+
/**
|
|
399
|
+
* Soft-requirement lifecycle retained by the host when a pre-model-call
|
|
400
|
+
* gate stops a run before its pending reminder or escalation is served.
|
|
401
|
+
*/
|
|
402
|
+
softToolRequirementState?: SoftToolRequirementState;
|
|
403
|
+
|
|
404
|
+
/**
|
|
405
|
+
* Notifies the host that the pre-model-call gate stopped the run after a
|
|
406
|
+
* hard tool choice was obtained from {@link getToolChoice} but before it
|
|
407
|
+
* was served, so the host can retain it for the next admitted request.
|
|
408
|
+
*/
|
|
409
|
+
onToolChoiceRejected?: () => void;
|
|
410
|
+
|
|
359
411
|
/**
|
|
360
412
|
* Dynamic reasoning effort override, resolved per LLM call.
|
|
361
413
|
* When set and returns a value, overrides the static `reasoning` captured
|
|
@@ -404,15 +456,22 @@ export interface AgentLoopConfig extends SimpleStreamOptions {
|
|
|
404
456
|
getCwd?: () => string | undefined;
|
|
405
457
|
|
|
406
458
|
/**
|
|
407
|
-
* Called
|
|
459
|
+
* Called once per tool call after argument validation, in call order, before
|
|
460
|
+
* the call is scheduled — ahead of concurrency resolution,
|
|
461
|
+
* `tool_execution_start`, and `tool.execute`. On the streamed path it runs
|
|
462
|
+
* before the assistant message's `message_start`/`message_end` are emitted.
|
|
408
463
|
*
|
|
409
464
|
* Return `{ block: true }` to prevent execution. The loop emits an error tool
|
|
410
465
|
* result instead (using `reason` as the error text, or a default if omitted).
|
|
411
466
|
*
|
|
412
|
-
*
|
|
413
|
-
*
|
|
467
|
+
* Return `{ args }` to replace the arguments the call runs with. The
|
|
468
|
+
* replacement is revalidated against the tool schema and written back to the
|
|
469
|
+
* tool-call block, making it the single source of truth: history, execution
|
|
470
|
+
* events, persistence, provider replay, concurrency scheduling, and
|
|
471
|
+
* `tool.execute` all see the revised arguments. Mutating `context.args` in
|
|
472
|
+
* place also survives into execution, but a returned `args` object wins.
|
|
414
473
|
*
|
|
415
|
-
* The hook receives the
|
|
474
|
+
* The hook receives the run's request abort signal and is responsible for
|
|
416
475
|
* honoring it. Throwing surfaces as a tool-error result and does not abort the
|
|
417
476
|
* rest of the batch.
|
|
418
477
|
*/
|
|
@@ -497,12 +556,16 @@ export type AgentToolCall = Extract<AssistantMessage["content"][number], { type:
|
|
|
497
556
|
* Set `block: true` to prevent the tool from executing. The loop emits an error tool
|
|
498
557
|
* result instead, using `reason` as the error text (or a default if omitted).
|
|
499
558
|
*
|
|
500
|
-
*
|
|
501
|
-
*
|
|
559
|
+
* Set `args` to replace the tool-call arguments. The replacement is revalidated
|
|
560
|
+
* against the tool schema (a failure surfaces as a validation-error tool result),
|
|
561
|
+
* written back to the tool-call block on the assistant message, and seen by
|
|
562
|
+
* history, scheduling, execution events, and `tool.execute` alike. It is
|
|
563
|
+
* ignored when `block` is true.
|
|
502
564
|
*/
|
|
503
565
|
export interface BeforeToolCallResult {
|
|
504
566
|
block?: boolean;
|
|
505
567
|
reason?: string;
|
|
568
|
+
args?: Record<string, unknown>;
|
|
506
569
|
}
|
|
507
570
|
|
|
508
571
|
/**
|
|
@@ -530,9 +593,12 @@ export interface BeforeToolCallContext {
|
|
|
530
593
|
assistantMessage: AssistantMessage;
|
|
531
594
|
/** The raw tool call block from `assistantMessage.content`. */
|
|
532
595
|
toolCall: AgentToolCall;
|
|
596
|
+
/** The resolved tool the call dispatches to. */
|
|
597
|
+
tool: AgentTool<any>;
|
|
533
598
|
/**
|
|
534
599
|
* Validated tool arguments. The same reference is forwarded to `tool.execute`
|
|
535
|
-
* (after any `transformToolCallArguments` pass), so in-place mutations stick
|
|
600
|
+
* (after any `transformToolCallArguments` pass), so in-place mutations stick;
|
|
601
|
+
* a returned `BeforeToolCallResult.args` replaces them entirely.
|
|
536
602
|
*/
|
|
537
603
|
args: Record<string, unknown>;
|
|
538
604
|
/** Current agent context at the time the tool call is prepared. */
|