@prometheus-ai/agent-core 0.5.4 → 0.5.8

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/src/agent.ts CHANGED
@@ -3,12 +3,13 @@
3
3
  */
4
4
  import { isPromise } from "node:util/types";
5
5
  import {
6
+ type ApiKeyResolveContext,
6
7
  type AssistantMessage,
7
8
  type AssistantMessageEvent,
9
+ type Context,
8
10
  type CursorExecHandlers,
9
11
  type CursorToolResultHandler,
10
12
  type Effort,
11
- getBundledModel,
12
13
  type ImageContent,
13
14
  type Message,
14
15
  type Model,
@@ -21,7 +22,9 @@ import {
21
22
  type ToolChoice,
22
23
  type ToolResultMessage,
23
24
  } from "@prometheus-ai/ai";
24
- import { agentLoop, agentLoopContinue } from "./agent-loop";
25
+ import { getBundledModel } from "@prometheus-ai/catalog/models";
26
+ import { logger } from "@prometheus-ai/utils";
27
+ import { abortReasonText, agentLoop, agentLoopContinue } from "./agent-loop";
25
28
  import type { AppendOnlyContextManager } from "./append-only-context";
26
29
  import type { HarmonyAuditEvent } from "./harmony-leak";
27
30
  import type {
@@ -32,6 +35,7 @@ import type {
32
35
  AgentState,
33
36
  AgentTool,
34
37
  AgentToolContext,
38
+ AsideMessage,
35
39
  StreamFn,
36
40
  ToolCallContext,
37
41
  } from "./types";
@@ -91,6 +95,12 @@ export interface AgentOptions {
91
95
  */
92
96
  transformContext?: (messages: AgentMessage[], signal?: AbortSignal) => Promise<AgentMessage[]>;
93
97
 
98
+ /**
99
+ * Optional transform applied after provider context assembly and before
100
+ * telemetry capture/provider send.
101
+ */
102
+ transformProviderContext?: (context: Context, model: Model) => Context;
103
+
94
104
  /**
95
105
  * Steering mode: "all" = send all steering messages at once, "one-at-a-time" = one per turn
96
106
  */
@@ -108,12 +118,6 @@ export interface AgentOptions {
108
118
  */
109
119
  interruptMode?: "immediate" | "wait";
110
120
 
111
- /**
112
- * Maximum completed tool calls to accept from one streamed assistant turn before
113
- * executing the batch. Undefined disables batching.
114
- */
115
- maxToolCallsPerTurn?: number;
116
-
117
121
  /**
118
122
  * API format for Kimi Code provider: "openai" or "anthropic" (default: "anthropic")
119
123
  */
@@ -132,6 +136,11 @@ export interface AgentOptions {
132
136
  * Used by providers that support session-based caching (e.g., OpenAI Codex).
133
137
  */
134
138
  sessionId?: string;
139
+ /**
140
+ * Optional prompt cache key forwarded to LLM providers.
141
+ * When omitted, providers may fall back to sessionId.
142
+ */
143
+ promptCacheKey?: string;
135
144
  /**
136
145
  * Shared provider state map for session-scoped transport/session caches.
137
146
  */
@@ -141,7 +150,7 @@ export interface AgentOptions {
141
150
  * Resolves an API key dynamically for each LLM call.
142
151
  * Useful for expiring tokens (e.g., GitHub Copilot OAuth).
143
152
  */
144
- getApiKey?: (provider: string) => Promise<string | undefined> | string | undefined;
153
+ getApiKey?: (provider: string, ctx?: ApiKeyResolveContext) => Promise<string | undefined> | string | undefined;
145
154
 
146
155
  /**
147
156
  * Inspect or replace provider payloads before they are sent.
@@ -264,6 +273,7 @@ export class Agent {
264
273
  systemPrompt: [],
265
274
  model: getBundledModel("google", "gemini-2.5-flash-lite-preview-06-17"),
266
275
  thinkingLevel: undefined,
276
+ disableReasoning: false,
267
277
  tools: [],
268
278
  messages: [],
269
279
  isStreaming: false,
@@ -276,13 +286,14 @@ export class Agent {
276
286
  #abortController?: AbortController;
277
287
  #convertToLlm: (messages: AgentMessage[]) => Message[] | Promise<Message[]>;
278
288
  #transformContext?: (messages: AgentMessage[], signal?: AbortSignal) => Promise<AgentMessage[]>;
289
+ #transformProviderContext?: (context: Context, model: Model) => Context;
279
290
  #steeringQueue: AgentMessage[] = [];
280
291
  #followUpQueue: AgentMessage[] = [];
281
292
  #steeringMode: "all" | "one-at-a-time";
282
293
  #followUpMode: "all" | "one-at-a-time";
283
294
  #interruptMode: "immediate" | "wait";
284
- #maxToolCallsPerTurn?: number;
285
295
  #sessionId?: string;
296
+ #promptCacheKey?: string;
286
297
  #metadata?: Record<string, unknown>;
287
298
  #metadataResolver?: (provider: string) => Record<string, unknown> | undefined;
288
299
  #providerSessionState?: Map<string, ProviderSessionState>;
@@ -312,6 +323,7 @@ export class Agent {
312
323
  #onAssistantMessageEvent?: (message: AssistantMessage, event: AssistantMessageEvent) => void;
313
324
  #onHarmonyLeak?: (event: HarmonyAuditEvent) => void | Promise<void>;
314
325
  #onBeforeYield?: () => Promise<void> | void;
326
+ #asideMessageProvider?: () => AsideMessage[] | Promise<AsideMessage[]>;
315
327
  #telemetry?: AgentLoopConfig["telemetry"];
316
328
  #appendOnlyContext?: AppendOnlyContextManager;
317
329
 
@@ -319,7 +331,7 @@ export class Agent {
319
331
  #cursorToolResultBuffer: CursorToolResultEntry[] = [];
320
332
 
321
333
  streamFn: StreamFn;
322
- getApiKey?: (provider: string) => Promise<string | undefined> | string | undefined;
334
+ getApiKey?: (provider: string, ctx?: ApiKeyResolveContext) => Promise<string | undefined> | string | undefined;
323
335
  /**
324
336
  * Hook invoked after tool arguments are validated and before execution.
325
337
  * Reassign at any time to swap the implementation (e.g. on extension reload).
@@ -341,9 +353,9 @@ export class Agent {
341
353
  this.#steeringMode = opts.steeringMode || "one-at-a-time";
342
354
  this.#followUpMode = opts.followUpMode || "one-at-a-time";
343
355
  this.#interruptMode = opts.interruptMode || "immediate";
344
- this.#maxToolCallsPerTurn = opts.maxToolCallsPerTurn;
345
356
  this.streamFn = opts.streamFn || streamSimple;
346
357
  this.#sessionId = opts.sessionId;
358
+ this.#promptCacheKey = opts.promptCacheKey;
347
359
  this.#providerSessionState = opts.providerSessionState;
348
360
  this.#thinkingBudgets = opts.thinkingBudgets;
349
361
  this.#temperature = opts.temperature;
@@ -373,6 +385,7 @@ export class Agent {
373
385
  this.afterToolCall = opts.afterToolCall;
374
386
  this.#telemetry = opts.telemetry;
375
387
  this.#appendOnlyContext = opts.appendOnlyContext;
388
+ this.#transformProviderContext = opts.transformProviderContext;
376
389
  }
377
390
 
378
391
  /**
@@ -390,6 +403,20 @@ export class Agent {
390
403
  this.#sessionId = value;
391
404
  }
392
405
 
406
+ /**
407
+ * Get the prompt cache key forwarded to providers.
408
+ */
409
+ get promptCacheKey(): string | undefined {
410
+ return this.#promptCacheKey;
411
+ }
412
+
413
+ /**
414
+ * Set the prompt cache key forwarded to providers.
415
+ */
416
+ set promptCacheKey(value: string | undefined) {
417
+ this.#promptCacheKey = value;
418
+ }
419
+
393
420
  /**
394
421
  * Static metadata forwarded to every API request when no resolver is installed
395
422
  * (e.g. `metadata.user_id` for Anthropic session attribution). Setting this
@@ -564,14 +591,6 @@ export class Agent {
564
591
  this.#maxRetryDelayMs = value;
565
592
  }
566
593
 
567
- get maxToolCallsPerTurn(): number | undefined {
568
- return this.#maxToolCallsPerTurn;
569
- }
570
-
571
- set maxToolCallsPerTurn(value: number | undefined) {
572
- this.#maxToolCallsPerTurn = value;
573
- }
574
-
575
594
  get state(): AgentState {
576
595
  return this.#state;
577
596
  }
@@ -607,6 +626,15 @@ export class Agent {
607
626
  this.#onBeforeYield = fn;
608
627
  }
609
628
 
629
+ /**
630
+ * Provide a source of non-interrupting "aside" messages (e.g. background-job
631
+ * completions, late LSP diagnostics) drained at each step boundary. Never
632
+ * aborts in-flight tools. See `AgentLoopConfig.getAsideMessages`.
633
+ */
634
+ setAsideMessageProvider(fn: (() => AsideMessage[] | Promise<AsideMessage[]>) | undefined): void {
635
+ this.#asideMessageProvider = fn;
636
+ }
637
+
610
638
  emitExternalEvent(event: AgentEvent) {
611
639
  switch (event.type) {
612
640
  case "message_start":
@@ -629,8 +657,8 @@ export class Agent {
629
657
  }
630
658
 
631
659
  // State mutators
632
- setSystemPrompt(v: string[]) {
633
- this.#state.systemPrompt = v;
660
+ setSystemPrompt(v: string[] | string) {
661
+ this.#state.systemPrompt = typeof v === "string" ? [v] : v;
634
662
  }
635
663
 
636
664
  setModel(m: Model) {
@@ -641,6 +669,10 @@ export class Agent {
641
669
  this.#state.thinkingLevel = l;
642
670
  }
643
671
 
672
+ setDisableReasoning(disabled: boolean) {
673
+ this.#state.disableReasoning = disabled;
674
+ }
675
+
644
676
  setSteeringMode(mode: "all" | "one-at-a-time") {
645
677
  this.#steeringMode = mode;
646
678
  }
@@ -675,6 +707,11 @@ export class Agent {
675
707
  this.#state.messages = ms.slice();
676
708
  }
677
709
 
710
+ replaceQueues(steering: AgentMessage[], followUp: AgentMessage[]) {
711
+ this.#steeringQueue = steering.slice();
712
+ this.#followUpQueue = followUp.slice();
713
+ }
714
+
678
715
  appendMessage(m: AgentMessage) {
679
716
  this.#state.messages.push(m);
680
717
  }
@@ -720,6 +757,24 @@ export class Agent {
720
757
  return this.#steeringQueue.length > 0 || this.#followUpQueue.length > 0;
721
758
  }
722
759
 
760
+ /** Non-consuming view of the pending steering queue (insertion order, newest
761
+ * last). The session layer derives its queued-message display/count from
762
+ * this live view instead of a mirror, so the agent-core queue stays the
763
+ * single source of truth. */
764
+ peekSteeringQueue(): readonly AgentMessage[] {
765
+ return this.#steeringQueue;
766
+ }
767
+
768
+ /** Non-consuming view of the pending follow-up queue. See
769
+ * {@link peekSteeringQueue}. */
770
+ peekFollowUpQueue(): readonly AgentMessage[] {
771
+ return this.#followUpQueue;
772
+ }
773
+
774
+ get isAborting(): boolean {
775
+ return this.#abortController?.signal.aborted === true && this.#state.isStreaming;
776
+ }
777
+
723
778
  #dequeueSteeringMessages(): AgentMessage[] {
724
779
  if (this.#steeringMode === "one-at-a-time") {
725
780
  if (this.#steeringQueue.length > 0) {
@@ -768,8 +823,8 @@ export class Agent {
768
823
  this.#state.messages.length = 0;
769
824
  }
770
825
 
771
- abort() {
772
- this.#abortController?.abort();
826
+ abort(reason?: unknown) {
827
+ this.#abortController?.abort(reason);
773
828
  }
774
829
 
775
830
  waitForIdle(): Promise<void> {
@@ -919,12 +974,18 @@ export class Agent {
919
974
  }
920
975
  : undefined;
921
976
 
922
- const getToolChoice = () =>
923
- this.#getToolChoice?.() ?? refreshToolChoiceForActiveTools(options?.toolChoice, this.#state.tools);
977
+ const getToolChoice = () => {
978
+ const queuedToolChoice = this.#getToolChoice?.();
979
+ if (queuedToolChoice !== undefined) {
980
+ return refreshToolChoiceForActiveTools(queuedToolChoice, this.#state.tools);
981
+ }
982
+ return refreshToolChoiceForActiveTools(options?.toolChoice, this.#state.tools);
983
+ };
924
984
 
925
985
  const config: AgentLoopConfig = {
926
986
  model,
927
987
  reasoning,
988
+ disableReasoning: this.#state.disableReasoning,
928
989
  temperature: this.#temperature,
929
990
  topP: this.#topP,
930
991
  topK: this.#topK,
@@ -934,8 +995,8 @@ export class Agent {
934
995
  serviceTier: this.#serviceTier,
935
996
  hideThinkingSummary: this.#hideThinkingSummary,
936
997
  interruptMode: this.#interruptMode,
937
- maxToolCallsPerTurn: this.#maxToolCallsPerTurn,
938
998
  sessionId: this.#sessionId,
999
+ promptCacheKey: this.#promptCacheKey,
939
1000
  metadata: this.#metadataResolver ? undefined : this.#metadata,
940
1001
  metadataResolver: this.#metadataResolver,
941
1002
  providerSessionState: this.#providerSessionState,
@@ -944,6 +1005,7 @@ export class Agent {
944
1005
  kimiApiFormat: this.#kimiApiFormat,
945
1006
  preferWebsockets: this.#preferWebsockets,
946
1007
  convertToLlm: this.#convertToLlm,
1008
+ transformProviderContext: this.#transformProviderContext,
947
1009
  transformContext: this.#transformContext,
948
1010
  onPayload: this.#onPayload,
949
1011
  onResponse: this.#onResponse,
@@ -968,6 +1030,7 @@ export class Agent {
968
1030
  onHarmonyLeak: this.#onHarmonyLeak,
969
1031
  getToolChoice,
970
1032
  getReasoning: () => this.#state.thinkingLevel,
1033
+ getDisableReasoning: () => this.#state.disableReasoning,
971
1034
  getSteeringMessages: async () => {
972
1035
  if (skipInitialSteeringPoll) {
973
1036
  skipInitialSteeringPoll = false;
@@ -975,7 +1038,9 @@ export class Agent {
975
1038
  }
976
1039
  return this.#dequeueSteeringMessages();
977
1040
  },
1041
+ hasSteeringMessages: () => this.#steeringQueue.length > 0,
978
1042
  getFollowUpMessages: async () => this.#dequeueFollowUpMessages(),
1043
+ getAsideMessages: async () => (await this.#asideMessageProvider?.()) ?? [],
979
1044
  onBeforeYield: () => this.#onBeforeYield?.(),
980
1045
  telemetry: this.#telemetry,
981
1046
  };
@@ -1053,8 +1118,12 @@ export class Agent {
1053
1118
  }
1054
1119
  }
1055
1120
  } catch (err) {
1056
- const errorMessage = err instanceof Error ? err.message : String(err);
1057
1121
  const stoppedForAbort = this.#abortController?.signal.aborted === true;
1122
+ const errorMessage = stoppedForAbort
1123
+ ? abortReasonText(this.#abortController?.signal)
1124
+ : err instanceof Error
1125
+ ? err.message
1126
+ : String(err);
1058
1127
  const shouldEmitVisibleOutputBlockedError = !stoppedForAbort && isAnthropicOutputBlockedError(errorMessage);
1059
1128
  const assistantPartial = partial?.role === "assistant" ? partial : undefined;
1060
1129
  const hadAssistantStart = assistantPartial !== undefined;
@@ -1113,11 +1182,15 @@ export class Agent {
1113
1182
  const result = listener(e) as unknown;
1114
1183
  if (isPromise(result)) {
1115
1184
  result.catch(err => {
1116
- console.error("Agent listener rejected:", err instanceof Error ? err.message : err);
1185
+ logger.warn("Agent listener rejected", {
1186
+ error: err instanceof Error ? err.message : String(err),
1187
+ });
1117
1188
  });
1118
1189
  }
1119
1190
  } catch (err) {
1120
- console.error("Agent listener threw:", err instanceof Error ? err.message : err);
1191
+ logger.warn("Agent listener threw", {
1192
+ error: err instanceof Error ? err.message : String(err),
1193
+ });
1121
1194
  }
1122
1195
  }
1123
1196
  }
@@ -5,7 +5,7 @@
5
5
  * a summary of the branch being left so context isn't lost.
6
6
  */
7
7
 
8
- import type { Model } from "@prometheus-ai/ai";
8
+ import type { ApiKey, Model } from "@prometheus-ai/ai";
9
9
  import { prompt } from "@prometheus-ai/utils";
10
10
  import { type AgentTelemetry, instrumentedCompleteSimple } from "../telemetry";
11
11
  import type { AgentMessage } from "../types";
@@ -13,10 +13,10 @@ import { estimateTokens } from "./compaction";
13
13
  import type { ReadonlySessionManager, SessionEntry } from "./entries";
14
14
  import {
15
15
  type ConvertToLlm,
16
- convertToLlm,
17
16
  createBranchSummaryMessage,
18
17
  createCompactionSummaryMessage,
19
18
  createCustomMessage,
19
+ defaultConvertToLlm,
20
20
  } from "./messages";
21
21
  import branchSummaryPrompt from "./prompts/branch-summary.md" with { type: "text" };
22
22
  import branchSummaryPreamble from "./prompts/branch-summary-preamble.md" with { type: "text" };
@@ -27,6 +27,7 @@ import {
27
27
  type FileOperations,
28
28
  SUMMARIZATION_SYSTEM_PROMPT,
29
29
  serializeConversation,
30
+ stripReadSelector,
30
31
  upsertFileOperations,
31
32
  } from "./utils";
32
33
 
@@ -70,7 +71,7 @@ export interface GenerateBranchSummaryOptions {
70
71
  /** Model to use for summarization */
71
72
  model: Model;
72
73
  /** API key for the model */
73
- apiKey: string;
74
+ apiKey: ApiKey;
74
75
  /** Abort signal for cancellation */
75
76
  signal: AbortSignal;
76
77
  /** Optional custom instructions for summarization */
@@ -83,7 +84,7 @@ export interface GenerateBranchSummaryOptions {
83
84
  convertToLlm?: ConvertToLlm;
84
85
  /**
85
86
  * Optional telemetry handle. When provided, the branch summary LLM call is
86
- * wrapped in an OTEL chat span tagged with `prometheus.gen_ai.oneshot.kind = "branch_summary"`.
87
+ * wrapped in an OTEL chat span tagged with `pi.gen_ai.oneshot.kind = "branch_summary"`.
87
88
  */
88
89
  telemetry?: AgentTelemetry;
89
90
  }
@@ -214,7 +215,7 @@ export function prepareBranchEntries(entries: SessionEntry[], tokenBudget: numbe
214
215
  if (entry.type === "branch_summary" && !entry.fromExtension && entry.details) {
215
216
  const details = entry.details as BranchSummaryDetails;
216
217
  if (Array.isArray(details.readFiles)) {
217
- for (const f of details.readFiles) fileOps.read.add(f);
218
+ for (const f of details.readFiles) fileOps.read.add(stripReadSelector(f));
218
219
  }
219
220
  if (Array.isArray(details.modifiedFiles)) {
220
221
  // Modified files go into both edited and written for proper deduplication
@@ -288,7 +289,7 @@ export async function generateBranchSummary(
288
289
 
289
290
  // Transform to LLM-compatible messages, then serialize to text
290
291
  // Serialization prevents the model from treating it as a conversation to continue
291
- const llmMessages = (options.convertToLlm ?? convertToLlm)(messages);
292
+ const llmMessages = (options.convertToLlm ?? defaultConvertToLlm)(messages);
292
293
  const conversationText = serializeConversation(llmMessages);
293
294
 
294
295
  // Build prompt
@@ -329,7 +330,7 @@ export async function generateBranchSummary(
329
330
 
330
331
  // Compute file lists and append to summary
331
332
  const { readFiles, modifiedFiles } = computeFileLists(fileOps);
332
- summary = upsertFileOperations(summary, readFiles, modifiedFiles);
333
+ summary = upsertFileOperations(summary, readFiles, modifiedFiles, fileOps.read);
333
334
 
334
335
  return {
335
336
  summary: summary || "No summary generated",