@prometheus-ai/agent-core 0.5.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.
Files changed (61) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/README.md +473 -0
  3. package/dist/types/agent-loop.d.ts +55 -0
  4. package/dist/types/agent.d.ts +331 -0
  5. package/dist/types/append-only-context.d.ts +113 -0
  6. package/dist/types/compaction/branch-summarization.d.ts +94 -0
  7. package/dist/types/compaction/compaction.d.ts +183 -0
  8. package/dist/types/compaction/entries.d.ts +103 -0
  9. package/dist/types/compaction/errors.d.ts +26 -0
  10. package/dist/types/compaction/index.d.ts +12 -0
  11. package/dist/types/compaction/messages.d.ts +61 -0
  12. package/dist/types/compaction/openai.d.ts +58 -0
  13. package/dist/types/compaction/pruning.d.ts +19 -0
  14. package/dist/types/compaction/shake.d.ts +82 -0
  15. package/dist/types/compaction/tool-protection.d.ts +17 -0
  16. package/dist/types/compaction/utils.d.ts +32 -0
  17. package/dist/types/compaction.d.ts +1 -0
  18. package/dist/types/harmony-leak.d.ts +118 -0
  19. package/dist/types/index.d.ts +11 -0
  20. package/dist/types/proxy.d.ts +84 -0
  21. package/dist/types/run-collector.d.ts +196 -0
  22. package/dist/types/telemetry.d.ts +588 -0
  23. package/dist/types/thinking.d.ts +17 -0
  24. package/dist/types/types.d.ts +443 -0
  25. package/dist/types/utils/yield.d.ts +52 -0
  26. package/package.json +75 -0
  27. package/src/agent-loop.ts +1418 -0
  28. package/src/agent.ts +1236 -0
  29. package/src/append-only-context.ts +297 -0
  30. package/src/compaction/branch-summarization.ts +339 -0
  31. package/src/compaction/compaction.ts +1155 -0
  32. package/src/compaction/entries.ts +133 -0
  33. package/src/compaction/errors.ts +31 -0
  34. package/src/compaction/index.ts +13 -0
  35. package/src/compaction/messages.ts +212 -0
  36. package/src/compaction/openai.ts +552 -0
  37. package/src/compaction/prompts/auto-handoff-threshold-focus.md +1 -0
  38. package/src/compaction/prompts/branch-summary-context.md +5 -0
  39. package/src/compaction/prompts/branch-summary-preamble.md +2 -0
  40. package/src/compaction/prompts/branch-summary.md +30 -0
  41. package/src/compaction/prompts/compaction-short-summary.md +9 -0
  42. package/src/compaction/prompts/compaction-summary-context.md +5 -0
  43. package/src/compaction/prompts/compaction-summary.md +38 -0
  44. package/src/compaction/prompts/compaction-turn-prefix.md +17 -0
  45. package/src/compaction/prompts/compaction-update-summary.md +45 -0
  46. package/src/compaction/prompts/file-operations.md +10 -0
  47. package/src/compaction/prompts/handoff-document.md +49 -0
  48. package/src/compaction/prompts/summarization-system.md +3 -0
  49. package/src/compaction/pruning.ts +99 -0
  50. package/src/compaction/shake.ts +406 -0
  51. package/src/compaction/tool-protection.ts +55 -0
  52. package/src/compaction/utils.ts +185 -0
  53. package/src/compaction.ts +1 -0
  54. package/src/harmony-leak.ts +456 -0
  55. package/src/index.ts +21 -0
  56. package/src/proxy.ts +326 -0
  57. package/src/run-collector.ts +631 -0
  58. package/src/telemetry.ts +2020 -0
  59. package/src/thinking.ts +19 -0
  60. package/src/types.ts +505 -0
  61. package/src/utils/yield.ts +146 -0
@@ -0,0 +1,19 @@
1
+ import { Effort } from "@prometheus-ai/ai";
2
+
3
+ /**
4
+ * Agent-local thinking selector.
5
+ *
6
+ * `off` disables reasoning, while `inherit` defers to a higher-level selector.
7
+ */
8
+ export const ThinkingLevel = {
9
+ Inherit: "inherit",
10
+ Off: "off",
11
+ Minimal: Effort.Minimal,
12
+ Low: Effort.Low,
13
+ Medium: Effort.Medium,
14
+ High: Effort.High,
15
+ XHigh: Effort.XHigh,
16
+ } as const;
17
+
18
+ export type ThinkingLevel = (typeof ThinkingLevel)[keyof typeof ThinkingLevel];
19
+ export type ResolvedThinkingLevel = Exclude<ThinkingLevel, "inherit">;
package/src/types.ts ADDED
@@ -0,0 +1,505 @@
1
+ import type {
2
+ AssistantMessage,
3
+ AssistantMessageEvent,
4
+ AssistantMessageEventStream,
5
+ Effort,
6
+ ImageContent,
7
+ Message,
8
+ Model,
9
+ SimpleStreamOptions,
10
+ Static,
11
+ streamSimple,
12
+ TextContent,
13
+ Tool,
14
+ ToolChoice,
15
+ ToolResultMessage,
16
+ TSchema,
17
+ } from "@prometheus-ai/ai";
18
+ import type { AppendOnlyContextManager } from "./append-only-context";
19
+ import type { HarmonyAuditEvent } from "./harmony-leak";
20
+ import type { AgentRunCoverage, AgentRunSummary } from "./run-collector";
21
+ import type { AgentTelemetryConfig } from "./telemetry";
22
+
23
+ /** Stream function - can return sync or Promise for async config lookup */
24
+ export type StreamFn = (
25
+ ...args: Parameters<typeof streamSimple>
26
+ ) => AssistantMessageEventStream | Promise<AssistantMessageEventStream>;
27
+
28
+ /**
29
+ * Configuration for the agent loop.
30
+ */
31
+ export interface AgentLoopConfig extends SimpleStreamOptions {
32
+ model: Model;
33
+
34
+ /**
35
+ * When to interrupt tool execution for steering messages.
36
+ * - "immediate" = check after each tool call (default)
37
+ * - "wait" = defer steering until the current turn completes
38
+ */
39
+ interruptMode?: "immediate" | "wait";
40
+
41
+ /**
42
+ * Maximum completed tool calls to accept from one streamed assistant turn before
43
+ * cutting the provider stream and executing that batch. The cap is enforced on
44
+ * `toolcall_end` so every executed call has complete arguments. Undefined disables
45
+ * batching.
46
+ */
47
+ maxToolCallsPerTurn?: number;
48
+
49
+ /**
50
+ * Optional session identifier forwarded to LLM providers.
51
+ * Used by providers that support session-based caching (e.g., OpenAI Codex).
52
+ */
53
+ sessionId?: string;
54
+
55
+ /**
56
+ * Optional resolver called per LLM request to produce request metadata.
57
+ * When set, the agent loop evaluates it **after** `getApiKey` resolves the
58
+ * session-sticky credential, ensuring the metadata's `account_uuid` reflects
59
+ * the credential actually used for the request (not the credential that was
60
+ * current when `AgentLoopConfig` was first constructed). Overrides the static
61
+ * `metadata` field when present.
62
+ */
63
+ metadataResolver?: (provider: string) => Record<string, unknown> | undefined;
64
+
65
+ /**
66
+ * Converts AgentMessage[] to LLM-compatible Message[] before each LLM call.
67
+ *
68
+ * Each AgentMessage must be converted to a UserMessage, AssistantMessage, or ToolResultMessage
69
+ * that the LLM can understand. AgentMessages that cannot be converted (e.g., UI-only notifications,
70
+ * status messages) should be filtered out.
71
+ *
72
+ * @example
73
+ * ```typescript
74
+ * convertToLlm: (messages) => messages.flatMap(m => {
75
+ * if (m.role === "custom") {
76
+ * // Convert custom message to user message
77
+ * return [{ role: "user", content: m.content, timestamp: m.timestamp }];
78
+ * }
79
+ * if (m.role === "notification") {
80
+ * // Filter out UI-only messages
81
+ * return [];
82
+ * }
83
+ * // Pass through standard LLM messages
84
+ * return [m];
85
+ * })
86
+ * ```
87
+ */
88
+ convertToLlm: (messages: AgentMessage[]) => Message[] | Promise<Message[]>;
89
+
90
+ /**
91
+ * Optional transform applied to the context before `convertToLlm`.
92
+ *
93
+ * Use this for operations that work at the AgentMessage level:
94
+ * - Context window management (pruning old messages)
95
+ * - Injecting context from external sources
96
+ *
97
+ * @example
98
+ * ```typescript
99
+ * transformContext: async (messages) => {
100
+ * if (estimateTokens(messages) > MAX_TOKENS) {
101
+ * return pruneOldMessages(messages);
102
+ * }
103
+ * return messages;
104
+ * }
105
+ * ```
106
+ */
107
+ transformContext?: (messages: AgentMessage[], signal?: AbortSignal) => Promise<AgentMessage[]>;
108
+
109
+ /**
110
+ * Resolves an API key dynamically for each LLM call.
111
+ *
112
+ * Useful for short-lived OAuth tokens (e.g., GitHub Copilot) that may expire
113
+ * during long-running tool execution phases.
114
+ */
115
+ getApiKey?: (provider: string) => Promise<string | undefined> | string | undefined;
116
+
117
+ /**
118
+ * Returns steering messages to inject into the conversation mid-run.
119
+ *
120
+ * Called after each tool execution to check for user interruptions unless interruptMode is "wait".
121
+ * If messages are returned, remaining tool calls are skipped and
122
+ * these messages are added to the context before the next LLM call.
123
+ */
124
+ getSteeringMessages?: () => Promise<AgentMessage[]>;
125
+
126
+ /**
127
+ * Returns follow-up messages to process after the agent would otherwise stop.
128
+ *
129
+ * Called when the agent has no more tool calls and no steering messages.
130
+ * If messages are returned, they're added to the context and the agent
131
+ * continues with another turn.
132
+ */
133
+ getFollowUpMessages?: () => Promise<AgentMessage[]>;
134
+ /**
135
+ * Hook fired right before the loop would exit.
136
+ *
137
+ * Called when the agent has no more tool calls and no steering messages,
138
+ * immediately before polling follow-up messages.
139
+ */
140
+ onBeforeYield?: () => Promise<void> | void;
141
+
142
+ /**
143
+ * Provides tool execution context, resolved per tool call.
144
+ * Use for late-bound UI or session state access.
145
+ */
146
+ getToolContext?: (toolCall?: ToolCallContext) => AgentToolContext | undefined;
147
+
148
+ /**
149
+ * Refreshes prompt/tool context from live session state before each model call.
150
+ * Use this when tool availability or the system prompt can change mid-turn.
151
+ */
152
+ syncContextBeforeModelCall?: (context: AgentContext) => void | Promise<void>;
153
+
154
+ /**
155
+ * Optional transform applied to tool call arguments before execution.
156
+ * Use for deobfuscating secrets or rewriting arguments.
157
+ */
158
+ transformToolCallArguments?: (args: Record<string, unknown>, toolName: string) => Record<string, unknown>;
159
+
160
+ /**
161
+ * Enable intent tracing for tool calls.
162
+ * When enabled, the harness injects a `string` field into tool schemas sent to the model,
163
+ * then strips from arguments before executing tools.
164
+ */
165
+ intentTracing?: boolean;
166
+ /**
167
+ * Append-only context mode — stabilizes system prompt + tool spec bytes
168
+ * across turns so provider prefix caches hit at maximum rate.
169
+ *
170
+ * When set, the loop reads messages from the append-only log (stable
171
+ * byte prefix) and caches system prompt + tools. Tools exclude per-turn
172
+ * `_i` intent fields.
173
+ */
174
+ appendOnlyContext?: AppendOnlyContextManager;
175
+
176
+ /**
177
+ * Inspect assistant streaming events before they are published to the outer agent event stream.
178
+ * Callers may abort synchronously to stop consuming buffered provider events.
179
+ */
180
+ onAssistantMessageEvent?: (message: AssistantMessage, event: AssistantMessageEvent) => void;
181
+
182
+ /**
183
+ * Called when GPT-5 Harmony protocol leakage is detected and mitigated.
184
+ */
185
+ onHarmonyLeak?: (event: HarmonyAuditEvent) => void | Promise<void>;
186
+
187
+ /**
188
+ * Dynamic tool choice override, resolved per LLM call.
189
+ * When set and returns a value, overrides the static `toolChoice`.
190
+ */
191
+ getToolChoice?: () => ToolChoice | undefined;
192
+
193
+ /**
194
+ * Dynamic reasoning effort override, resolved per LLM call.
195
+ * When set and returns a value, overrides the static `reasoning` captured
196
+ * at run-loop start. Use this so mid-run thinking-level changes apply on
197
+ * the next model call instead of waiting for the next prompt.
198
+ */
199
+ getReasoning?: () => Effort | undefined;
200
+
201
+ /**
202
+ * Called after a tool call has been validated and is about to execute.
203
+ *
204
+ * Return `{ block: true }` to prevent execution. The loop emits an error tool
205
+ * result instead (using `reason` as the error text, or a default if omitted).
206
+ *
207
+ * Mutating `context.args` in place changes the arguments passed to `tool.execute`
208
+ * — the loop does **not** re-validate after this hook runs.
209
+ *
210
+ * The hook receives the tool abort signal (`signal`) and is responsible for
211
+ * honoring it. Throwing surfaces as a tool-error result and does not abort the
212
+ * rest of the batch.
213
+ */
214
+ beforeToolCall?: (
215
+ context: BeforeToolCallContext,
216
+ signal?: AbortSignal,
217
+ ) => Promise<BeforeToolCallResult | undefined> | BeforeToolCallResult | undefined;
218
+
219
+ /**
220
+ * Called after a tool finishes executing, before `tool_execution_end` and the
221
+ * tool-result message are emitted.
222
+ *
223
+ * Return an `AfterToolCallResult` to override individual fields of the executed
224
+ * tool result. Omitted fields keep their original values; there is no deep merge.
225
+ *
226
+ * Throwing surfaces as a tool-error result and does not abort the rest of the batch.
227
+ */
228
+ afterToolCall?: (
229
+ context: AfterToolCallContext,
230
+ signal?: AbortSignal,
231
+ ) => Promise<AfterToolCallResult | undefined> | AfterToolCallResult | undefined;
232
+ /**
233
+ * Opt-in OpenTelemetry instrumentation. Passing `{}` enables the loop's
234
+ * GenAI-semantic-convention spans (`invoke_agent`, `chat`, `execute_tool`)
235
+ * using the global tracer provider. Leaving this field undefined disables
236
+ * the instrumentation entirely — the loop performs zero tracer lookups.
237
+ *
238
+ * See {@link AgentTelemetryConfig} for the full surface (hooks, content
239
+ * capture, cost estimator, agent identity).
240
+ */
241
+ telemetry?: AgentTelemetryConfig;
242
+ }
243
+
244
+ /**
245
+ * Batch/sequencing metadata for the tool call currently being processed.
246
+ */
247
+ export interface ToolCallContext {
248
+ batchId: string;
249
+ index: number;
250
+ total: number;
251
+ toolCalls: Array<{ id: string; name: string }>;
252
+ }
253
+
254
+ /** A single tool-call content block emitted by an assistant message. */
255
+ export type AgentToolCall = Extract<AssistantMessage["content"][number], { type: "toolCall" }>;
256
+
257
+ /**
258
+ * Result returned from `beforeToolCall`.
259
+ *
260
+ * Set `block: true` to prevent the tool from executing. The loop emits an error tool
261
+ * result instead, using `reason` as the error text (or a default if omitted).
262
+ *
263
+ * Mutating the `args` reference passed in `BeforeToolCallContext` is supported and
264
+ * survives into execution — the loop does **not** re-validate after this hook runs.
265
+ */
266
+ export interface BeforeToolCallResult {
267
+ block?: boolean;
268
+ reason?: string;
269
+ }
270
+
271
+ /**
272
+ * Partial override returned from `afterToolCall`.
273
+ *
274
+ * Merge semantics are field-by-field; omitted fields keep the executed values.
275
+ * No deep merge is performed.
276
+ */
277
+ export interface AfterToolCallResult {
278
+ /** If provided, replaces the tool result content array in full. */
279
+ content?: (TextContent | ImageContent)[];
280
+ /** If provided, replaces the tool result details payload in full. */
281
+ details?: unknown;
282
+ /** If provided, replaces the error flag carried with the tool result. */
283
+ isError?: boolean;
284
+ }
285
+
286
+ /** Context passed to `beforeToolCall`. */
287
+ export interface BeforeToolCallContext {
288
+ /** The assistant message that requested the tool call. */
289
+ assistantMessage: AssistantMessage;
290
+ /** The raw tool call block from `assistantMessage.content`. */
291
+ toolCall: AgentToolCall;
292
+ /**
293
+ * Validated tool arguments. The same reference is forwarded to `tool.execute`
294
+ * (after any `transformToolCallArguments` pass), so in-place mutations stick.
295
+ */
296
+ args: Record<string, unknown>;
297
+ /** Current agent context at the time the tool call is prepared. */
298
+ context: AgentContext;
299
+ }
300
+
301
+ /** Context passed to `afterToolCall`. */
302
+ export interface AfterToolCallContext {
303
+ /** The assistant message that requested the tool call. */
304
+ assistantMessage: AssistantMessage;
305
+ /** The raw tool call block from `assistantMessage.content`. */
306
+ toolCall: AgentToolCall;
307
+ /** Validated tool arguments used for execution (post `beforeToolCall` mutations). */
308
+ args: Record<string, unknown>;
309
+ /** The executed tool result before any `afterToolCall` overrides are applied. */
310
+ result: AgentToolResult<any>;
311
+ /** Whether the executed tool result is currently treated as an error. */
312
+ isError: boolean;
313
+ /** Current agent context at the time the tool call is finalized. */
314
+ context: AgentContext;
315
+ }
316
+
317
+ /**
318
+ * Extensible interface for custom app messages.
319
+ * Apps can extend via declaration merging:
320
+ *
321
+ * @example
322
+ * ```typescript
323
+ * declare module "@prometheus-ai/agent-core" {
324
+ * interface CustomAgentMessages {
325
+ * artifact: ArtifactMessage;
326
+ * notification: NotificationMessage;
327
+ * }
328
+ * }
329
+ * ```
330
+ */
331
+ export interface CustomAgentMessages {
332
+ // Empty by default - apps extend via declaration merging
333
+ }
334
+
335
+ /**
336
+ * AgentMessage: Union of LLM messages + custom messages.
337
+ * This abstraction allows apps to add custom message types while maintaining
338
+ * type safety and compatibility with the base LLM messages.
339
+ */
340
+ export type AgentMessage = Message | CustomAgentMessages[keyof CustomAgentMessages];
341
+
342
+ /**
343
+ * Agent state containing all configuration and conversation data.
344
+ */
345
+ export interface AgentState {
346
+ systemPrompt: string[];
347
+ model: Model;
348
+ thinkingLevel?: Effort;
349
+ tools: AgentTool<any>[];
350
+ messages: AgentMessage[]; // Can include attachments + custom message types
351
+ isStreaming: boolean;
352
+ streamMessage: AgentMessage | null;
353
+ pendingToolCalls: Set<string>;
354
+ error?: string;
355
+ }
356
+
357
+ export interface AgentToolResult<T = any, _TInput = unknown> {
358
+ // Content blocks supporting text and images
359
+ content: (TextContent | ImageContent)[];
360
+ // Details to be displayed in a UI or logged
361
+ details?: T;
362
+ // Marks a non-throwing failure (e.g. an aggregator catching per-entry errors).
363
+ // agent-loop honors this and surfaces it as a tool error on the wire.
364
+ isError?: boolean;
365
+ }
366
+
367
+ // Callback for streaming tool execution updates
368
+ export type AgentToolUpdateCallback<T = any, TInput = unknown> = (partialResult: AgentToolResult<T, TInput>) => void;
369
+
370
+ /** Options passed to renderResult */
371
+ export interface RenderResultOptions {
372
+ /** Whether the result view is expanded */
373
+ expanded: boolean;
374
+ /** Whether this is a partial/streaming result */
375
+ isPartial: boolean;
376
+ /** Current spinner frame index for animated elements (optional) */
377
+ spinnerFrame?: number;
378
+ }
379
+
380
+ /** Capability tier a tool exercises. Determines which approval modes auto-approve it. */
381
+ export type ToolTier = "read" | "write" | "exec";
382
+
383
+ /**
384
+ * Per-tool approval declaration.
385
+ * - bare tier ("read" / "write" / "exec") — static classification.
386
+ * - object form — adds a `reason` (shown in the prompt) and/or `override: true`
387
+ * (force-prompt even in modes that would otherwise auto-approve this tier).
388
+ * - function — dynamic, given parsed args. Returns either form above.
389
+ *
390
+ * Omitted approvals are treated as "exec" by callers that enforce approvals.
391
+ */
392
+ export type ToolApprovalDecision = ToolTier | { tier: ToolTier; reason?: string; override?: boolean };
393
+ export type ToolApproval = ToolApprovalDecision | ((args: unknown) => ToolApprovalDecision);
394
+
395
+ /**
396
+ * Context passed to tool execution.
397
+ * Apps can extend via declaration merging.
398
+ */
399
+ export interface AgentToolContext {
400
+ // Empty by default - apps extend via declaration merging
401
+ }
402
+
403
+ export type AgentToolExecFn<TParameters extends TSchema = TSchema, TDetails = any, TTheme = unknown> = (
404
+ this: AgentTool<TParameters, TDetails, TTheme>,
405
+ toolCallId: string,
406
+ params: Static<TParameters>,
407
+ signal?: AbortSignal,
408
+ onUpdate?: AgentToolUpdateCallback<TDetails, TParameters>,
409
+ context?: AgentToolContext,
410
+ ) => Promise<AgentToolResult<TDetails, TParameters>>;
411
+
412
+ // AgentTool extends Tool but adds the execute function
413
+ export interface AgentTool<TParameters extends TSchema = TSchema, TDetails = any, TTheme = unknown>
414
+ extends Tool<TParameters> {
415
+ // A human-readable label for the tool to be displayed in UI
416
+ label: string;
417
+ /** If true, tool is excluded unless explicitly listed in --tools or agent's tools field */
418
+ hidden?: boolean;
419
+ /** If true, tool can stage a pending action that requires explicit resolution via the resolve tool. */
420
+ deferrable?: boolean;
421
+ /** Built-in tool loading behavior. "essential" loads initially; "discoverable" can be activated by tool search. */
422
+ loadMode?: "essential" | "discoverable";
423
+ /** Short one-line summary used for tool discovery indexes. */
424
+ summary?: string;
425
+ /** If true, tool execution ignores abort signals (runs to completion) */
426
+ nonAbortable?: boolean;
427
+ /**
428
+ * Concurrency mode for tool scheduling when multiple calls are in one turn.
429
+ * - "shared": can run alongside other shared tools (default)
430
+ * - "exclusive": runs alone; other tools wait until it finishes
431
+ */
432
+ concurrency?: "shared" | "exclusive";
433
+ /** If true, argument validation errors are non-fatal: raw args are passed to execute() instead of returning an error to the LLM. */
434
+ lenientArgValidation?: boolean;
435
+ /**
436
+ * Controls how the INTENT_FIELD (`_i`) is handled for this tool.
437
+ * - `"require"` (default): `_i` is injected and required in the parameter schema.
438
+ * - `"optional"`: `_i` is injected as an optional/nullable field.
439
+ * - `"omit"`: `_i` is NOT injected. Use for tools where intent is obvious (yield, resolve, todo, …).
440
+ * - function: `_i` is NOT injected; intent is derived dynamically from (potentially partial / streaming) args.
441
+ */
442
+ intent?: "omit" | "optional" | "require" | ((args: Partial<Static<TParameters>>) => string | undefined);
443
+
444
+ /**
445
+ * Normalize (potentially partial) streamed arguments into the plain text that
446
+ * stream-content matchers (e.g. TTSR rules) should inspect — the real content
447
+ * the call introduces, without wire grammar such as patch prefixes or JSON
448
+ * string escaping. Return `undefined` to fall back to raw argument-delta
449
+ * matching.
450
+ */
451
+ matcherDigest?: (args: unknown) => string | undefined;
452
+
453
+ /** Capability tier declaration used by approval gates. Omitted means "exec". */
454
+ approval?: ToolApproval;
455
+
456
+ /** Lines appended after the standard approval prompt header. */
457
+ formatApprovalDetails?: (args: unknown) => string | string[] | undefined;
458
+
459
+ /** The main execution callback for this tool. */
460
+ execute: AgentToolExecFn<TParameters, TDetails, TTheme>;
461
+
462
+ /** Optional custom rendering for tool call display (returns UI component) */
463
+ renderCall?: (args: Static<TParameters>, options: RenderResultOptions, theme: TTheme) => unknown;
464
+
465
+ /** Optional custom rendering for tool result display (returns UI component) */
466
+ renderResult?: (
467
+ result: AgentToolResult<TDetails, TParameters>,
468
+ options: RenderResultOptions,
469
+ theme: TTheme,
470
+ ) => unknown;
471
+ }
472
+
473
+ // AgentContext is like Context but uses AgentTool
474
+ export interface AgentContext {
475
+ systemPrompt: string[];
476
+ messages: AgentMessage[];
477
+ tools?: AgentTool<any>[];
478
+ }
479
+
480
+ /**
481
+ * Events emitted by the Agent for UI updates.
482
+ * These events provide fine-grained lifecycle information for messages, turns, and tool executions.
483
+ */
484
+ export type AgentEvent =
485
+ // Agent lifecycle
486
+ | { type: "agent_start" }
487
+ | {
488
+ type: "agent_end";
489
+ messages: AgentMessage[];
490
+ /** Present iff `AgentTelemetryConfig` was supplied on this run. */
491
+ telemetry?: AgentRunSummary;
492
+ coverage?: AgentRunCoverage;
493
+ }
494
+ // Turn lifecycle - a turn is one assistant response + any tool calls/results
495
+ | { type: "turn_start" }
496
+ | { type: "turn_end"; message: AgentMessage; toolResults: ToolResultMessage[] }
497
+ // Message lifecycle - emitted for user, assistant, and toolResult messages
498
+ | { type: "message_start"; message: AgentMessage }
499
+ // Only emitted for assistant messages during streaming
500
+ | { type: "message_update"; message: AgentMessage; assistantMessageEvent: AssistantMessageEvent }
501
+ | { type: "message_end"; message: AgentMessage }
502
+ // Tool execution lifecycle
503
+ | { type: "tool_execution_start"; toolCallId: string; toolName: string; args: any; intent?: string }
504
+ | { type: "tool_execution_update"; toolCallId: string; toolName: string; args: any; partialResult: any }
505
+ | { type: "tool_execution_end"; toolCallId: string; toolName: string; result: any; isError?: boolean };
@@ -0,0 +1,146 @@
1
+ /**
2
+ * Cooperative yield utility for preventing Bun event-loop busy-wait.
3
+ *
4
+ * ## Root Cause
5
+ *
6
+ * Bun 1.3.x (JavaScriptCore) event loop busy-waits (spins in userspace)
7
+ * when the only pending work is an unresolved Promise — even if there are
8
+ * active I/O watchers (stdin, child process pipes, etc.). The event loop
9
+ * continuously polls for microtask resolution instead of blocking in
10
+ * `epoll_wait`, consuming ~100% of a CPU core.
11
+ *
12
+ * This affects any `await` on a never-resolved Promise, including:
13
+ * - `Promise.withResolvers()` used for user input callbacks
14
+ * - `await proc.exited` for long-running child processes
15
+ * - Agent loop iterations waiting for the next tool call
16
+ *
17
+ * ## Fix
18
+ *
19
+ * A recurring `setInterval` keeps the event loop sleeping in `epoll_wait`.
20
+ * The `EventLoopKeepalive` class and `keepaliveWhile()` wrapper provide a
21
+ * clean way to install and clean up this keepalive timer.
22
+ *
23
+ * The older `yieldIfDue()` and `ExponentialYield` approaches (compensated
24
+ * sleep loops) are retained for the agent-loop hot-path where Promises
25
+ * resolve frequently and the keepalive alone is insufficient.
26
+ */
27
+
28
+ import { scheduler } from "node:timers/promises";
29
+
30
+ // ---------------------------------------------------------------------------
31
+ // EventLoopKeepalive — the primary fix for idle-state busy-wait
32
+ // ---------------------------------------------------------------------------
33
+
34
+ export class EventLoopKeepalive {
35
+ #tmr = setInterval(() => {}, 86_400_000).unref();
36
+ [Symbol.dispose](): void {
37
+ clearInterval(this.#tmr);
38
+ }
39
+ }
40
+
41
+ // ---------------------------------------------------------------------------
42
+ // yieldIfDue — retained for agent-loop hot-path
43
+ // ---------------------------------------------------------------------------
44
+
45
+ const YIELD_SLEEP_MS = 20;
46
+ const YIELD_INTERVAL_MS = 50;
47
+
48
+ /**
49
+ * Wall-clock timestamp of the last completed yield. Module-level so that
50
+ * tight loops sharing this helper collectively respect the gate, not just
51
+ * one caller at a time.
52
+ */
53
+ let lastYieldAt = 0;
54
+
55
+ /**
56
+ * Sleep for at least `ms` milliseconds of wall-clock time.
57
+ * Retries the wait if it returns prematurely (which can happen when napi
58
+ * callbacks wake the event loop via `uv_async_send`). When `signal` is
59
+ * provided, the wait is cancellable and silently returns on abort instead
60
+ * of throwing — callers race against another promise that decides what to
61
+ * do next.
62
+ */
63
+ async function sleepAtLeast(ms: number, signal?: AbortSignal): Promise<void> {
64
+ const start = performance.now();
65
+ let remaining = ms;
66
+ while (remaining > 0) {
67
+ if (signal?.aborted) return;
68
+ try {
69
+ await scheduler.wait(remaining, { signal });
70
+ } catch (err) {
71
+ if ((err as { name?: string })?.name === "AbortError") return;
72
+ throw err;
73
+ }
74
+ remaining = ms - (performance.now() - start);
75
+ }
76
+ }
77
+
78
+ /**
79
+ * Yield to the Bun event loop, sleeping for at least 20 ms — but at most
80
+ * once every {@link YIELD_INTERVAL_MS}. Callers in hot paths can invoke
81
+ * this freely; only the slow path actually sleeps.
82
+ */
83
+ export async function yieldIfDue(): Promise<void> {
84
+ const now = Date.now();
85
+ if (now - lastYieldAt < YIELD_INTERVAL_MS) return;
86
+ await sleepAtLeast(YIELD_SLEEP_MS);
87
+ lastYieldAt = Date.now();
88
+ }
89
+
90
+ // ---------------------------------------------------------------------------
91
+ // ExponentialYield — retained for bash-executor long waits
92
+ // ---------------------------------------------------------------------------
93
+
94
+ const EXP_DEFAULT_MIN_MS = 20;
95
+ const EXP_DEFAULT_MAX_MS = 10_000;
96
+ const EXP_DEFAULT_MULTIPLIER = 2;
97
+
98
+ export class ExponentialYield {
99
+ #currentMs: number;
100
+ readonly #minMs: number;
101
+ readonly #maxMs: number;
102
+ readonly #multiplier: number;
103
+
104
+ constructor(opts?: { minMs?: number; maxMs?: number; multiplier?: number }) {
105
+ this.#minMs = opts?.minMs ?? EXP_DEFAULT_MIN_MS;
106
+ this.#maxMs = opts?.maxMs ?? EXP_DEFAULT_MAX_MS;
107
+ this.#multiplier = opts?.multiplier ?? EXP_DEFAULT_MULTIPLIER;
108
+ this.#currentMs = this.#minMs;
109
+ }
110
+
111
+ notifyActivity(): void {
112
+ this.#currentMs = this.#minMs;
113
+ }
114
+
115
+ async sleep(signal?: AbortSignal): Promise<number> {
116
+ const ms = this.#currentMs;
117
+ await sleepAtLeast(ms, signal);
118
+ this.#currentMs = Math.min(this.#currentMs * this.#multiplier, this.#maxMs);
119
+ return ms;
120
+ }
121
+
122
+ /**
123
+ * Race `racers` against an exponentially-backed-off cooperative yield.
124
+ * The losing sleep is cancelled as soon as a racer settles, so no stray
125
+ * timers keep the event loop alive past the racer's resolution.
126
+ */
127
+ async race<T>(racers: Array<Promise<T>>): Promise<T> {
128
+ const racer = Promise.race(racers);
129
+ const controller = new AbortController();
130
+ try {
131
+ const yieldMarker = Symbol("exp-yield");
132
+ for (;;) {
133
+ const result = await Promise.race<T | typeof yieldMarker>([
134
+ racer,
135
+ this.sleep(controller.signal).then(() => yieldMarker as T | typeof yieldMarker),
136
+ ]);
137
+ if (result !== yieldMarker) {
138
+ this.notifyActivity();
139
+ return result;
140
+ }
141
+ }
142
+ } finally {
143
+ controller.abort();
144
+ }
145
+ }
146
+ }