@f5-sales-demo/pi-agent-core 21.25.0 → 21.27.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,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@f5-sales-demo/pi-agent-core",
4
- "version": "21.25.0",
4
+ "version": "21.27.0",
5
5
  "description": "General-purpose agent with transport abstraction, state management, and attachment support",
6
6
  "homepage": "https://github.com/f5-sales-demo/xcsh",
7
7
  "author": "Can Boluk",
@@ -35,8 +35,8 @@
35
35
  "fmt": "biome format --write ."
36
36
  },
37
37
  "dependencies": {
38
- "@f5-sales-demo/pi-ai": "21.25.0",
39
- "@f5-sales-demo/pi-utils": "21.25.0"
38
+ "@f5-sales-demo/pi-ai": "21.27.0",
39
+ "@f5-sales-demo/pi-utils": "21.27.0"
40
40
  },
41
41
  "devDependencies": {
42
42
  "@sinclair/typebox": "0.34.52",
package/src/agent-loop.ts CHANGED
@@ -12,6 +12,7 @@ import {
12
12
  } from "@f5-sales-demo/pi-ai";
13
13
  import { logger } from "@f5-sales-demo/pi-utils";
14
14
  import emptyToolSuccess from "./prompts/empty-tool-success.md" with { type: "text" };
15
+ import { AgentToolError } from "./tool-error";
15
16
  import type {
16
17
  AgentContext,
17
18
  AgentEvent,
@@ -21,6 +22,7 @@ import type {
21
22
  AgentToolResult,
22
23
  StreamFn,
23
24
  } from "./types";
25
+ import { getToolExecutionKind } from "./types";
24
26
 
25
27
  /**
26
28
  * Start an agent loop with a new prompt message.
@@ -224,7 +226,7 @@ async function runLoop(
224
226
  }
225
227
 
226
228
  // Stream assistant response
227
- const message = await streamAssistantResponse(currentContext, config, signal, stream, streamFn);
229
+ const message = await streamAssistantResponse(currentContext, newMessages, config, signal, stream, streamFn);
228
230
  newMessages.push(message);
229
231
  let steeringMessagesFromExecution: AgentMessage[] | undefined;
230
232
 
@@ -300,6 +302,7 @@ async function runLoop(
300
302
  */
301
303
  async function streamAssistantResponse(
302
304
  context: AgentContext,
305
+ newMessages: AgentMessage[],
303
306
  config: AgentLoopConfig,
304
307
  signal: AbortSignal | undefined,
305
308
  stream: EventStream<AgentEvent, AgentMessage[]>,
@@ -310,6 +313,14 @@ async function streamAssistantResponse(
310
313
  if (config.transformContext) {
311
314
  messages = await logger.ttftAttr("ttft.transform-context", () => config.transformContext!(messages, signal));
312
315
  }
316
+ const injected = signal?.aborted ? [] : (config.getContextMessages?.(messages) ?? []);
317
+ for (const message of injected) {
318
+ context.messages.push(message);
319
+ newMessages.push(message);
320
+ stream.push({ type: "message_start", message });
321
+ stream.push({ type: "message_end", message });
322
+ }
323
+ if (injected.length > 0 && messages !== context.messages) messages = [...messages, ...injected];
313
324
 
314
325
  // Convert to LLM-compatible messages (AgentMessage[] → Message[])
315
326
  const llmMessages = await logger.ttftAttr("ttft.convert-to-llm", () => config.convertToLlm(messages));
@@ -504,12 +515,14 @@ async function executeToolCalls(
504
515
  const normalizedResult = normalizeToolResult(result, isError);
505
516
  const { toolCall } = record;
506
517
  if (!record.started) {
518
+ const executionKind = getToolExecutionKind(record.tool, record.args);
507
519
  stream.push({
508
520
  type: "tool_execution_start",
509
521
  toolCallId: toolCall.id,
510
522
  toolName: toolCall.name,
511
523
  args: record.args,
512
524
  intent: toolCall.intent,
525
+ ...(executionKind ? { executionKind } : {}),
513
526
  });
514
527
  }
515
528
  const isWarning = Boolean(normalizedResult.isWarning);
@@ -559,12 +572,14 @@ async function executeToolCalls(
559
572
  }
560
573
  record.args = argsForExecution;
561
574
  record.started = true;
575
+ const executionKind = getToolExecutionKind(tool, argsForExecution);
562
576
  stream.push({
563
577
  type: "tool_execution_start",
564
578
  toolCallId: toolCall.id,
565
579
  toolName: toolCall.name,
566
580
  args: argsForExecution,
567
581
  intent: toolCall.intent,
582
+ ...(executionKind ? { executionKind } : {}),
568
583
  });
569
584
 
570
585
  let result: AgentToolResult<any>;
@@ -607,10 +622,13 @@ async function executeToolCalls(
607
622
  toolContext,
608
623
  );
609
624
  } catch (e) {
610
- result = {
611
- content: [{ type: "text", text: e instanceof Error ? e.message : String(e) }],
612
- details: {},
613
- };
625
+ result =
626
+ e instanceof AgentToolError
627
+ ? e.result
628
+ : {
629
+ content: [{ type: "text", text: e instanceof Error ? e.message : String(e) }],
630
+ details: {},
631
+ };
614
632
  isError = true;
615
633
  }
616
634
 
package/src/agent.ts CHANGED
@@ -257,6 +257,7 @@ export class Agent {
257
257
  #listeners = new Set<(e: AgentEvent) => void>();
258
258
  #abortController?: AbortController;
259
259
  #convertToLlm: (messages: AgentMessage[]) => Message[] | Promise<Message[]>;
260
+ #contextMessages?: AgentLoopConfig["getContextMessages"];
260
261
  #transformContext?: (messages: AgentMessage[], signal?: AbortSignal) => Promise<AgentMessage[]>;
261
262
  #steeringQueue: AgentMessage[] = [];
262
263
  #followUpQueue: AgentMessage[] = [];
@@ -534,6 +535,14 @@ export class Agent {
534
535
  this.#state.messages = ms.slice();
535
536
  }
536
537
 
538
+ /** Install the owning session's model-context provider. It must not start work. */
539
+ setContextMessagesProvider(provider?: AgentLoopConfig["getContextMessages"]): () => void {
540
+ this.#contextMessages = provider;
541
+ return () => {
542
+ if (this.#contextMessages === provider) this.#contextMessages = undefined;
543
+ };
544
+ }
545
+
537
546
  appendMessage(m: AgentMessage) {
538
547
  this.#state.messages = [...this.#state.messages, m];
539
548
  }
@@ -804,6 +813,7 @@ export class Agent {
804
813
  kimiApiFormat: this.#kimiApiFormat,
805
814
  preferWebsockets: this.#preferWebsockets,
806
815
  convertToLlm: this.#convertToLlm,
816
+ getContextMessages: messages => this.#contextMessages?.(messages) ?? [],
807
817
  transformContext: this.#transformContext,
808
818
  // Per-turn: compose the extension hook with any server-tool injection for
809
819
  // THIS prompt (e.g. Office "Search the web"). No-op when neither is present.
package/src/index.ts CHANGED
@@ -6,5 +6,7 @@ export * from "./agent-loop";
6
6
  export * from "./proxy";
7
7
  // Thinking selectors
8
8
  export * from "./thinking";
9
+ // Structured execution failures
10
+ export * from "./tool-error";
9
11
  // Types
10
12
  export * from "./types";
@@ -0,0 +1,12 @@
1
+ import type { AgentToolResult } from "./types";
2
+
3
+ /** A failed execution whose structured result must survive the tool/agent boundary. */
4
+ export class AgentToolError<T = unknown> extends Error {
5
+ constructor(
6
+ message: string,
7
+ readonly result: AgentToolResult<T>,
8
+ ) {
9
+ super(message);
10
+ this.name = "AgentToolError";
11
+ }
12
+ }
package/src/types.ts CHANGED
@@ -64,6 +64,9 @@ export interface AgentLoopConfig extends SimpleStreamOptions {
64
64
  */
65
65
  convertToLlm: (messages: AgentMessage[]) => Message[] | Promise<Message[]>;
66
66
 
67
+ /** Session-owned context updates after pruning; emitted and retained like other messages. */
68
+ getContextMessages?: (messages: readonly AgentMessage[]) => AgentMessage[];
69
+
67
70
  /**
68
71
  * Optional transform applied to the context before `convertToLlm`.
69
72
  *
@@ -242,6 +245,10 @@ export interface AgentTool<TParameters extends TSchema = TSchema, TDetails = any
242
245
  extends Tool<TParameters> {
243
246
  // A human-readable label for the tool to be displayed in UI
244
247
  label: string;
248
+ /** Host presentation of the actual executor; absent for ordinary dynamic tools. */
249
+ executionKind?: "command" | "fileChange";
250
+ /** Pure per-call classification; returning undefined preserves ordinary dynamic-tool presentation. */
251
+ getExecutionKind?: (params: unknown) => "command" | "fileChange" | undefined;
245
252
  /** If true, tool is excluded unless explicitly listed in --tools or agent's tools field */
246
253
  hidden?: boolean;
247
254
  /** If true, tool can stage a pending action that requires explicit resolution via the resolve tool. */
@@ -293,7 +300,14 @@ export type AgentEvent =
293
300
  | { type: "message_update"; message: AgentMessage; assistantMessageEvent: AssistantMessageEvent }
294
301
  | { type: "message_end"; message: AgentMessage }
295
302
  // Tool execution lifecycle
296
- | { type: "tool_execution_start"; toolCallId: string; toolName: string; args: any; intent?: string }
303
+ | {
304
+ type: "tool_execution_start";
305
+ toolCallId: string;
306
+ toolName: string;
307
+ args: any;
308
+ intent?: string;
309
+ executionKind?: "command" | "fileChange";
310
+ }
297
311
  | { type: "tool_execution_update"; toolCallId: string; toolName: string; args: any; partialResult: any }
298
312
  | {
299
313
  type: "tool_execution_end";
@@ -303,3 +317,10 @@ export type AgentEvent =
303
317
  isError?: boolean;
304
318
  isWarning?: boolean;
305
319
  };
320
+
321
+ export function getToolExecutionKind(
322
+ tool: AgentTool | undefined,
323
+ params: unknown,
324
+ ): "command" | "fileChange" | undefined {
325
+ return tool?.getExecutionKind ? tool.getExecutionKind(params) : tool?.executionKind;
326
+ }