@oh-my-pi/pi-agent-core 15.0.1 → 15.0.2
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 +62 -18
- package/src/agent.ts +28 -2
- package/src/types.ts +98 -0
package/package.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@oh-my-pi/pi-agent-core",
|
|
4
|
-
"version": "15.0.
|
|
4
|
+
"version": "15.0.2",
|
|
5
5
|
"description": "General-purpose agent with transport abstraction, state management, and attachment support",
|
|
6
|
-
"homepage": "https://
|
|
6
|
+
"homepage": "https://omp.sh",
|
|
7
7
|
"author": "Can Boluk",
|
|
8
8
|
"contributors": [
|
|
9
9
|
"Mario Zechner"
|
|
@@ -35,9 +35,9 @@
|
|
|
35
35
|
"fmt": "biome format --write ."
|
|
36
36
|
},
|
|
37
37
|
"dependencies": {
|
|
38
|
-
"@oh-my-pi/pi-ai": "15.0.
|
|
39
|
-
"@oh-my-pi/pi-natives": "15.0.
|
|
40
|
-
"@oh-my-pi/pi-utils": "15.0.
|
|
38
|
+
"@oh-my-pi/pi-ai": "15.0.2",
|
|
39
|
+
"@oh-my-pi/pi-natives": "15.0.2",
|
|
40
|
+
"@oh-my-pi/pi-utils": "15.0.2"
|
|
41
41
|
},
|
|
42
42
|
"devDependencies": {
|
|
43
43
|
"@sinclair/typebox": "^0.34.49",
|
package/src/agent-loop.ts
CHANGED
|
@@ -375,17 +375,7 @@ async function runLoop(
|
|
|
375
375
|
|
|
376
376
|
const toolResults: ToolResultMessage[] = [];
|
|
377
377
|
if (hasMoreToolCalls) {
|
|
378
|
-
const executionResult = await executeToolCalls(
|
|
379
|
-
currentContext.tools,
|
|
380
|
-
message,
|
|
381
|
-
signal,
|
|
382
|
-
stream,
|
|
383
|
-
config.getSteeringMessages,
|
|
384
|
-
config.interruptMode,
|
|
385
|
-
config.getToolContext,
|
|
386
|
-
config.transformToolCallArguments,
|
|
387
|
-
config.intentTracing,
|
|
388
|
-
);
|
|
378
|
+
const executionResult = await executeToolCalls(currentContext, message, signal, stream, config);
|
|
389
379
|
|
|
390
380
|
toolResults.push(...executionResult.toolResults);
|
|
391
381
|
steeringMessagesFromExecution = executionResult.steeringMessages;
|
|
@@ -633,16 +623,22 @@ function emitAbortedAssistantMessage(
|
|
|
633
623
|
* Execute tool calls from an assistant message.
|
|
634
624
|
*/
|
|
635
625
|
async function executeToolCalls(
|
|
636
|
-
|
|
626
|
+
currentContext: AgentContext,
|
|
637
627
|
assistantMessage: AssistantMessage,
|
|
638
628
|
signal: AbortSignal | undefined,
|
|
639
629
|
stream: EventStream<AgentEvent, AgentMessage[]>,
|
|
640
|
-
|
|
641
|
-
interruptMode: AgentLoopConfig["interruptMode"] = "immediate",
|
|
642
|
-
getToolContext?: AgentLoopConfig["getToolContext"],
|
|
643
|
-
transformToolCallArguments?: AgentLoopConfig["transformToolCallArguments"],
|
|
644
|
-
intentTracing?: AgentLoopConfig["intentTracing"],
|
|
630
|
+
config: AgentLoopConfig,
|
|
645
631
|
): Promise<{ toolResults: ToolResultMessage[]; steeringMessages?: AgentMessage[] }> {
|
|
632
|
+
const tools = currentContext.tools;
|
|
633
|
+
const {
|
|
634
|
+
getSteeringMessages,
|
|
635
|
+
interruptMode = "immediate",
|
|
636
|
+
getToolContext,
|
|
637
|
+
transformToolCallArguments,
|
|
638
|
+
intentTracing,
|
|
639
|
+
beforeToolCall,
|
|
640
|
+
afterToolCall,
|
|
641
|
+
} = config;
|
|
646
642
|
type ToolCallContent = Extract<AssistantMessage["content"][number], { type: "toolCall" }>;
|
|
647
643
|
const toolCalls = assistantMessage.content.filter((c): c is ToolCallContent => c.type === "toolCall");
|
|
648
644
|
const emittedToolResults: ToolResultMessage[] = [];
|
|
@@ -785,6 +781,24 @@ async function executeToolCalls(
|
|
|
785
781
|
throw validationError;
|
|
786
782
|
}
|
|
787
783
|
}
|
|
784
|
+
|
|
785
|
+
if (beforeToolCall) {
|
|
786
|
+
const beforeResult = await beforeToolCall(
|
|
787
|
+
{
|
|
788
|
+
assistantMessage,
|
|
789
|
+
toolCall,
|
|
790
|
+
args: effectiveArgs,
|
|
791
|
+
context: currentContext,
|
|
792
|
+
},
|
|
793
|
+
toolSignal,
|
|
794
|
+
);
|
|
795
|
+
if (beforeResult?.block) {
|
|
796
|
+
throw new Error(beforeResult.reason || "Tool execution was blocked");
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
// Reflect post-hook args so emitted tool results / afterToolCall see what actually executed.
|
|
800
|
+
record.args = effectiveArgs;
|
|
801
|
+
|
|
788
802
|
const toolContext = getToolContext
|
|
789
803
|
? getToolContext({
|
|
790
804
|
batchId,
|
|
@@ -802,7 +816,7 @@ async function executeToolCalls(
|
|
|
802
816
|
type: "tool_execution_update",
|
|
803
817
|
toolCallId: toolCall.id,
|
|
804
818
|
toolName: toolCall.name,
|
|
805
|
-
args:
|
|
819
|
+
args: effectiveArgs,
|
|
806
820
|
partialResult: coerceToolResult(partialResult).result,
|
|
807
821
|
});
|
|
808
822
|
},
|
|
@@ -819,6 +833,36 @@ async function executeToolCalls(
|
|
|
819
833
|
isError = true;
|
|
820
834
|
}
|
|
821
835
|
|
|
836
|
+
if (afterToolCall) {
|
|
837
|
+
try {
|
|
838
|
+
const after = await afterToolCall(
|
|
839
|
+
{
|
|
840
|
+
assistantMessage,
|
|
841
|
+
toolCall,
|
|
842
|
+
args: record.args,
|
|
843
|
+
result,
|
|
844
|
+
isError,
|
|
845
|
+
context: currentContext,
|
|
846
|
+
},
|
|
847
|
+
toolSignal,
|
|
848
|
+
);
|
|
849
|
+
if (after) {
|
|
850
|
+
result = {
|
|
851
|
+
content: after.content ?? result.content,
|
|
852
|
+
details: after.details ?? result.details,
|
|
853
|
+
isError: after.isError ?? result.isError,
|
|
854
|
+
};
|
|
855
|
+
isError = after.isError ?? isError;
|
|
856
|
+
}
|
|
857
|
+
} catch (e) {
|
|
858
|
+
result = {
|
|
859
|
+
content: [{ type: "text", text: e instanceof Error ? e.message : String(e) }],
|
|
860
|
+
details: {},
|
|
861
|
+
};
|
|
862
|
+
isError = true;
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
|
|
822
866
|
if (interruptState.triggered) {
|
|
823
867
|
record.skipped = true;
|
|
824
868
|
emitToolResult(record, createSkippedToolResult(), true);
|
package/src/agent.ts
CHANGED
|
@@ -38,7 +38,7 @@ import type {
|
|
|
38
38
|
* Default convertToLlm: Keep only LLM-compatible messages, convert attachments.
|
|
39
39
|
*/
|
|
40
40
|
function defaultConvertToLlm(messages: AgentMessage[]): Message[] {
|
|
41
|
-
return messages.filter(m => m.role === "user" || m.role === "assistant" || m.role === "toolResult");
|
|
41
|
+
return messages.filter((m): m is Message => m.role === "user" || m.role === "assistant" || m.role === "toolResult");
|
|
42
42
|
}
|
|
43
43
|
|
|
44
44
|
function refreshToolChoiceForActiveTools(
|
|
@@ -208,6 +208,18 @@ export interface AgentOptions {
|
|
|
208
208
|
* Cursor tool result callback for exec tool responses.
|
|
209
209
|
*/
|
|
210
210
|
cursorOnToolResult?: CursorToolResultHandler;
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Called after a tool call has been validated and is about to execute.
|
|
214
|
+
* See {@link AgentLoopConfig.beforeToolCall} for full semantics.
|
|
215
|
+
*/
|
|
216
|
+
beforeToolCall?: AgentLoopConfig["beforeToolCall"];
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Called after a tool finishes executing, before `tool_execution_end` and the tool-result
|
|
220
|
+
* message are emitted. See {@link AgentLoopConfig.afterToolCall} for full semantics.
|
|
221
|
+
*/
|
|
222
|
+
afterToolCall?: AgentLoopConfig["afterToolCall"];
|
|
211
223
|
}
|
|
212
224
|
|
|
213
225
|
export interface AgentPromptOptions {
|
|
@@ -277,6 +289,16 @@ export class Agent {
|
|
|
277
289
|
|
|
278
290
|
streamFn: StreamFn;
|
|
279
291
|
getApiKey?: (provider: string) => Promise<string | undefined> | string | undefined;
|
|
292
|
+
/**
|
|
293
|
+
* Hook invoked after tool arguments are validated and before execution.
|
|
294
|
+
* Reassign at any time to swap the implementation (e.g. on extension reload).
|
|
295
|
+
*/
|
|
296
|
+
beforeToolCall?: AgentLoopConfig["beforeToolCall"];
|
|
297
|
+
/**
|
|
298
|
+
* Hook invoked after tool execution and before `tool_execution_end` / tool-result
|
|
299
|
+
* message emission. Reassign at any time to swap the implementation.
|
|
300
|
+
*/
|
|
301
|
+
afterToolCall?: AgentLoopConfig["afterToolCall"];
|
|
280
302
|
|
|
281
303
|
constructor(opts: AgentOptions = {}) {
|
|
282
304
|
this.#state = { ...this.#state, ...opts.initialState };
|
|
@@ -312,6 +334,8 @@ export class Agent {
|
|
|
312
334
|
this.#getToolChoice = opts.getToolChoice;
|
|
313
335
|
this.#onAssistantMessageEvent = opts.onAssistantMessageEvent;
|
|
314
336
|
this.#onHarmonyLeak = opts.onHarmonyLeak;
|
|
337
|
+
this.beforeToolCall = opts.beforeToolCall;
|
|
338
|
+
this.afterToolCall = opts.afterToolCall;
|
|
315
339
|
}
|
|
316
340
|
|
|
317
341
|
/**
|
|
@@ -868,6 +892,8 @@ export class Agent {
|
|
|
868
892
|
cursorOnToolResult,
|
|
869
893
|
transformToolCallArguments: this.#transformToolCallArguments,
|
|
870
894
|
intentTracing: this.#intentTracing,
|
|
895
|
+
beforeToolCall: this.beforeToolCall ? (ctx, signal) => this.beforeToolCall?.(ctx, signal) : undefined,
|
|
896
|
+
afterToolCall: this.afterToolCall ? (ctx, signal) => this.afterToolCall?.(ctx, signal) : undefined,
|
|
871
897
|
onAssistantMessageEvent: this.#onAssistantMessageEvent,
|
|
872
898
|
onHarmonyLeak: this.#onHarmonyLeak,
|
|
873
899
|
getToolChoice,
|
|
@@ -945,7 +971,7 @@ export class Agent {
|
|
|
945
971
|
}
|
|
946
972
|
|
|
947
973
|
// Handle any remaining partial message
|
|
948
|
-
if (partial && partial.role === "assistant" && partial.content.length > 0) {
|
|
974
|
+
if (partial && partial.role === "assistant" && Array.isArray(partial.content) && partial.content.length > 0) {
|
|
949
975
|
const onlyEmpty = !partial.content.some(
|
|
950
976
|
c =>
|
|
951
977
|
(c.type === "thinking" && c.thinking.trim().length > 0) ||
|
package/src/types.ts
CHANGED
|
@@ -169,8 +169,43 @@ export interface AgentLoopConfig extends SimpleStreamOptions {
|
|
|
169
169
|
* the next model call instead of waiting for the next prompt.
|
|
170
170
|
*/
|
|
171
171
|
getReasoning?: () => Effort | undefined;
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Called after a tool call has been validated and is about to execute.
|
|
175
|
+
*
|
|
176
|
+
* Return `{ block: true }` to prevent execution. The loop emits an error tool
|
|
177
|
+
* result instead (using `reason` as the error text, or a default if omitted).
|
|
178
|
+
*
|
|
179
|
+
* Mutating `context.args` in place changes the arguments passed to `tool.execute`
|
|
180
|
+
* — the loop does **not** re-validate after this hook runs.
|
|
181
|
+
*
|
|
182
|
+
* The hook receives the tool abort signal (`signal`) and is responsible for
|
|
183
|
+
* honoring it. Throwing surfaces as a tool-error result and does not abort the
|
|
184
|
+
* rest of the batch.
|
|
185
|
+
*/
|
|
186
|
+
beforeToolCall?: (
|
|
187
|
+
context: BeforeToolCallContext,
|
|
188
|
+
signal?: AbortSignal,
|
|
189
|
+
) => Promise<BeforeToolCallResult | undefined> | BeforeToolCallResult | undefined;
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Called after a tool finishes executing, before `tool_execution_end` and the
|
|
193
|
+
* tool-result message are emitted.
|
|
194
|
+
*
|
|
195
|
+
* Return an `AfterToolCallResult` to override individual fields of the executed
|
|
196
|
+
* tool result. Omitted fields keep their original values; there is no deep merge.
|
|
197
|
+
*
|
|
198
|
+
* Throwing surfaces as a tool-error result and does not abort the rest of the batch.
|
|
199
|
+
*/
|
|
200
|
+
afterToolCall?: (
|
|
201
|
+
context: AfterToolCallContext,
|
|
202
|
+
signal?: AbortSignal,
|
|
203
|
+
) => Promise<AfterToolCallResult | undefined> | AfterToolCallResult | undefined;
|
|
172
204
|
}
|
|
173
205
|
|
|
206
|
+
/**
|
|
207
|
+
* Batch/sequencing metadata for the tool call currently being processed.
|
|
208
|
+
*/
|
|
174
209
|
export interface ToolCallContext {
|
|
175
210
|
batchId: string;
|
|
176
211
|
index: number;
|
|
@@ -178,6 +213,69 @@ export interface ToolCallContext {
|
|
|
178
213
|
toolCalls: Array<{ id: string; name: string }>;
|
|
179
214
|
}
|
|
180
215
|
|
|
216
|
+
/** A single tool-call content block emitted by an assistant message. */
|
|
217
|
+
export type AgentToolCall = Extract<AssistantMessage["content"][number], { type: "toolCall" }>;
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Result returned from `beforeToolCall`.
|
|
221
|
+
*
|
|
222
|
+
* Set `block: true` to prevent the tool from executing. The loop emits an error tool
|
|
223
|
+
* result instead, using `reason` as the error text (or a default if omitted).
|
|
224
|
+
*
|
|
225
|
+
* Mutating the `args` reference passed in `BeforeToolCallContext` is supported and
|
|
226
|
+
* survives into execution — the loop does **not** re-validate after this hook runs.
|
|
227
|
+
*/
|
|
228
|
+
export interface BeforeToolCallResult {
|
|
229
|
+
block?: boolean;
|
|
230
|
+
reason?: string;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Partial override returned from `afterToolCall`.
|
|
235
|
+
*
|
|
236
|
+
* Merge semantics are field-by-field; omitted fields keep the executed values.
|
|
237
|
+
* No deep merge is performed.
|
|
238
|
+
*/
|
|
239
|
+
export interface AfterToolCallResult {
|
|
240
|
+
/** If provided, replaces the tool result content array in full. */
|
|
241
|
+
content?: (TextContent | ImageContent)[];
|
|
242
|
+
/** If provided, replaces the tool result details payload in full. */
|
|
243
|
+
details?: unknown;
|
|
244
|
+
/** If provided, replaces the error flag carried with the tool result. */
|
|
245
|
+
isError?: boolean;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/** Context passed to `beforeToolCall`. */
|
|
249
|
+
export interface BeforeToolCallContext {
|
|
250
|
+
/** The assistant message that requested the tool call. */
|
|
251
|
+
assistantMessage: AssistantMessage;
|
|
252
|
+
/** The raw tool call block from `assistantMessage.content`. */
|
|
253
|
+
toolCall: AgentToolCall;
|
|
254
|
+
/**
|
|
255
|
+
* Validated tool arguments. The same reference is forwarded to `tool.execute`
|
|
256
|
+
* (after any `transformToolCallArguments` pass), so in-place mutations stick.
|
|
257
|
+
*/
|
|
258
|
+
args: Record<string, unknown>;
|
|
259
|
+
/** Current agent context at the time the tool call is prepared. */
|
|
260
|
+
context: AgentContext;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/** Context passed to `afterToolCall`. */
|
|
264
|
+
export interface AfterToolCallContext {
|
|
265
|
+
/** The assistant message that requested the tool call. */
|
|
266
|
+
assistantMessage: AssistantMessage;
|
|
267
|
+
/** The raw tool call block from `assistantMessage.content`. */
|
|
268
|
+
toolCall: AgentToolCall;
|
|
269
|
+
/** Validated tool arguments used for execution (post `beforeToolCall` mutations). */
|
|
270
|
+
args: Record<string, unknown>;
|
|
271
|
+
/** The executed tool result before any `afterToolCall` overrides are applied. */
|
|
272
|
+
result: AgentToolResult<any>;
|
|
273
|
+
/** Whether the executed tool result is currently treated as an error. */
|
|
274
|
+
isError: boolean;
|
|
275
|
+
/** Current agent context at the time the tool call is finalized. */
|
|
276
|
+
context: AgentContext;
|
|
277
|
+
}
|
|
278
|
+
|
|
181
279
|
/**
|
|
182
280
|
* Extensible interface for custom app messages.
|
|
183
281
|
* Apps can extend via declaration merging:
|