phi-code-agent 0.56.3 → 0.74.1

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 (97) hide show
  1. package/README.md +109 -33
  2. package/dist/agent-loop.d.ts +3 -0
  3. package/dist/agent-loop.d.ts.map +1 -1
  4. package/dist/agent-loop.js +298 -127
  5. package/dist/agent-loop.js.map +1 -1
  6. package/dist/agent.d.ts +88 -127
  7. package/dist/agent.d.ts.map +1 -1
  8. package/dist/agent.js +323 -318
  9. package/dist/agent.js.map +1 -1
  10. package/dist/harness/agent-harness.d.ts +85 -0
  11. package/dist/harness/agent-harness.d.ts.map +1 -0
  12. package/dist/harness/agent-harness.js +728 -0
  13. package/dist/harness/agent-harness.js.map +1 -0
  14. package/dist/harness/compaction/branch-summarization.d.ts +88 -0
  15. package/dist/harness/compaction/branch-summarization.d.ts.map +1 -0
  16. package/dist/harness/compaction/branch-summarization.js +243 -0
  17. package/dist/harness/compaction/branch-summarization.js.map +1 -0
  18. package/dist/harness/compaction/compaction.d.ts +122 -0
  19. package/dist/harness/compaction/compaction.d.ts.map +1 -0
  20. package/dist/harness/compaction/compaction.js +632 -0
  21. package/dist/harness/compaction/compaction.js.map +1 -0
  22. package/dist/harness/compaction/utils.d.ts +38 -0
  23. package/dist/harness/compaction/utils.d.ts.map +1 -0
  24. package/dist/harness/compaction/utils.js +153 -0
  25. package/dist/harness/compaction/utils.js.map +1 -0
  26. package/dist/harness/env/nodejs.d.ts +44 -0
  27. package/dist/harness/env/nodejs.d.ts.map +1 -0
  28. package/dist/harness/env/nodejs.js +348 -0
  29. package/dist/harness/env/nodejs.js.map +1 -0
  30. package/dist/harness/execution-env.d.ts +4 -0
  31. package/dist/harness/execution-env.d.ts.map +1 -0
  32. package/dist/harness/execution-env.js +3 -0
  33. package/dist/harness/execution-env.js.map +1 -0
  34. package/dist/harness/messages.d.ts +51 -0
  35. package/dist/harness/messages.d.ts.map +1 -0
  36. package/dist/harness/messages.js +102 -0
  37. package/dist/harness/messages.js.map +1 -0
  38. package/dist/harness/prompt-templates.d.ts +45 -0
  39. package/dist/harness/prompt-templates.d.ts.map +1 -0
  40. package/dist/harness/prompt-templates.js +200 -0
  41. package/dist/harness/prompt-templates.js.map +1 -0
  42. package/dist/harness/session/repo/jsonl.d.ts +20 -0
  43. package/dist/harness/session/repo/jsonl.d.ts.map +1 -0
  44. package/dist/harness/session/repo/jsonl.js +92 -0
  45. package/dist/harness/session/repo/jsonl.js.map +1 -0
  46. package/dist/harness/session/repo/memory.d.ts +18 -0
  47. package/dist/harness/session/repo/memory.d.ts.map +1 -0
  48. package/dist/harness/session/repo/memory.js +42 -0
  49. package/dist/harness/session/repo/memory.js.map +1 -0
  50. package/dist/harness/session/repo/shared.d.ts +10 -0
  51. package/dist/harness/session/repo/shared.d.ts.map +1 -0
  52. package/dist/harness/session/repo/shared.js +31 -0
  53. package/dist/harness/session/repo/shared.js.map +1 -0
  54. package/dist/harness/session/session.d.ts +32 -0
  55. package/dist/harness/session/session.d.ts.map +1 -0
  56. package/dist/harness/session/session.js +196 -0
  57. package/dist/harness/session/session.js.map +1 -0
  58. package/dist/harness/session/storage/jsonl.d.ts +30 -0
  59. package/dist/harness/session/storage/jsonl.d.ts.map +1 -0
  60. package/dist/harness/session/storage/jsonl.js +171 -0
  61. package/dist/harness/session/storage/jsonl.js.map +1 -0
  62. package/dist/harness/session/storage/memory.d.ts +26 -0
  63. package/dist/harness/session/storage/memory.d.ts.map +1 -0
  64. package/dist/harness/session/storage/memory.js +90 -0
  65. package/dist/harness/session/storage/memory.js.map +1 -0
  66. package/dist/harness/skills.d.ts +41 -0
  67. package/dist/harness/skills.d.ts.map +1 -0
  68. package/dist/harness/skills.js +259 -0
  69. package/dist/harness/skills.js.map +1 -0
  70. package/dist/harness/system-prompt.d.ts +3 -0
  71. package/dist/harness/system-prompt.d.ts.map +1 -0
  72. package/dist/harness/system-prompt.js +30 -0
  73. package/dist/harness/system-prompt.js.map +1 -0
  74. package/dist/harness/types.d.ts +525 -0
  75. package/dist/harness/types.d.ts.map +1 -0
  76. package/dist/harness/types.js +16 -0
  77. package/dist/harness/types.js.map +1 -0
  78. package/dist/harness/utils/shell-output.d.ts +14 -0
  79. package/dist/harness/utils/shell-output.d.ts.map +1 -0
  80. package/dist/harness/utils/shell-output.js +97 -0
  81. package/dist/harness/utils/shell-output.js.map +1 -0
  82. package/dist/harness/utils/truncate.d.ts +70 -0
  83. package/dist/harness/utils/truncate.d.ts.map +1 -0
  84. package/dist/harness/utils/truncate.js +205 -0
  85. package/dist/harness/utils/truncate.js.map +1 -0
  86. package/dist/index.d.ts +15 -0
  87. package/dist/index.d.ts.map +1 -1
  88. package/dist/index.js +16 -0
  89. package/dist/index.js.map +1 -1
  90. package/dist/proxy.d.ts +4 -20
  91. package/dist/proxy.d.ts.map +1 -1
  92. package/dist/proxy.js +32 -5
  93. package/dist/proxy.js.map +1 -1
  94. package/dist/types.d.ts +224 -16
  95. package/dist/types.d.ts.map +1 -1
  96. package/dist/types.js.map +1 -1
  97. package/package.json +6 -3
package/dist/agent.js CHANGED
@@ -1,287 +1,295 @@
1
- /**
2
- * Agent class that uses the agent-loop directly.
3
- * No transport abstraction - calls streamSimple via the loop.
4
- */
5
- import { getModel, streamSimple, } from "phi-code-ai";
6
- import { agentLoop, agentLoopContinue } from "./agent-loop.js";
7
- /**
8
- * Default convertToLlm: Keep only LLM-compatible messages, convert attachments.
9
- */
1
+ import { streamSimple, } from "phi-code-ai";
2
+ import { runAgentLoop, runAgentLoopContinue } from "./agent-loop.js";
10
3
  function defaultConvertToLlm(messages) {
11
- return messages.filter((m) => m.role === "user" || m.role === "assistant" || m.role === "toolResult");
4
+ return messages.filter((message) => message.role === "user" || message.role === "assistant" || message.role === "toolResult");
12
5
  }
13
- export class Agent {
14
- constructor(opts = {}) {
15
- this._state = {
16
- systemPrompt: "",
17
- model: getModel("google", "gemini-2.5-flash-lite-preview-06-17"),
18
- thinkingLevel: "off",
19
- tools: [],
20
- messages: [],
21
- isStreaming: false,
22
- streamMessage: null,
23
- pendingToolCalls: new Set(),
24
- error: undefined,
25
- };
26
- this.listeners = new Set();
27
- this.steeringQueue = [];
28
- this.followUpQueue = [];
29
- this._state = { ...this._state, ...opts.initialState };
30
- this.convertToLlm = opts.convertToLlm || defaultConvertToLlm;
31
- this.transformContext = opts.transformContext;
32
- this.steeringMode = opts.steeringMode || "one-at-a-time";
33
- this.followUpMode = opts.followUpMode || "one-at-a-time";
34
- this.streamFn = opts.streamFn || streamSimple;
35
- this._sessionId = opts.sessionId;
36
- this.getApiKey = opts.getApiKey;
37
- this._thinkingBudgets = opts.thinkingBudgets;
38
- this._transport = opts.transport ?? "sse";
39
- this._maxRetryDelayMs = opts.maxRetryDelayMs;
40
- }
41
- /**
42
- * Get the current session ID used for provider caching.
43
- */
44
- get sessionId() {
45
- return this._sessionId;
46
- }
47
- /**
48
- * Set the session ID for provider caching.
49
- * Call this when switching sessions (new session, branch, resume).
50
- */
51
- set sessionId(value) {
52
- this._sessionId = value;
53
- }
54
- /**
55
- * Get the current thinking budgets.
56
- */
57
- get thinkingBudgets() {
58
- return this._thinkingBudgets;
59
- }
60
- /**
61
- * Set custom thinking budgets for token-based providers.
62
- */
63
- set thinkingBudgets(value) {
64
- this._thinkingBudgets = value;
6
+ const EMPTY_USAGE = {
7
+ input: 0,
8
+ output: 0,
9
+ cacheRead: 0,
10
+ cacheWrite: 0,
11
+ totalTokens: 0,
12
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
13
+ };
14
+ const DEFAULT_MODEL = {
15
+ id: "unknown",
16
+ name: "unknown",
17
+ api: "unknown",
18
+ provider: "unknown",
19
+ baseUrl: "",
20
+ reasoning: false,
21
+ input: [],
22
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
23
+ contextWindow: 0,
24
+ maxTokens: 0,
25
+ };
26
+ function createMutableAgentState(initialState) {
27
+ let tools = initialState?.tools?.slice() ?? [];
28
+ let messages = initialState?.messages?.slice() ?? [];
29
+ return {
30
+ systemPrompt: initialState?.systemPrompt ?? "",
31
+ model: initialState?.model ?? DEFAULT_MODEL,
32
+ thinkingLevel: initialState?.thinkingLevel ?? "off",
33
+ get tools() {
34
+ return tools;
35
+ },
36
+ set tools(nextTools) {
37
+ tools = nextTools.slice();
38
+ },
39
+ get messages() {
40
+ return messages;
41
+ },
42
+ set messages(nextMessages) {
43
+ messages = nextMessages.slice();
44
+ },
45
+ isStreaming: false,
46
+ streamingMessage: undefined,
47
+ pendingToolCalls: new Set(),
48
+ errorMessage: undefined,
49
+ };
50
+ }
51
+ class PendingMessageQueue {
52
+ mode;
53
+ messages = [];
54
+ constructor(mode) {
55
+ this.mode = mode;
56
+ }
57
+ enqueue(message) {
58
+ this.messages.push(message);
59
+ }
60
+ hasItems() {
61
+ return this.messages.length > 0;
62
+ }
63
+ drain() {
64
+ if (this.mode === "all") {
65
+ const drained = this.messages.slice();
66
+ this.messages = [];
67
+ return drained;
68
+ }
69
+ const first = this.messages[0];
70
+ if (!first) {
71
+ return [];
72
+ }
73
+ this.messages = this.messages.slice(1);
74
+ return [first];
65
75
  }
66
- /**
67
- * Get the current preferred transport.
68
- */
69
- get transport() {
70
- return this._transport;
76
+ clear() {
77
+ this.messages = [];
71
78
  }
72
- /**
73
- * Set the preferred transport.
74
- */
75
- setTransport(value) {
76
- this._transport = value;
79
+ }
80
+ /**
81
+ * Stateful wrapper around the low-level agent loop.
82
+ *
83
+ * `Agent` owns the current transcript, emits lifecycle events, executes tools,
84
+ * and exposes queueing APIs for steering and follow-up messages.
85
+ */
86
+ export class Agent {
87
+ _state;
88
+ listeners = new Set();
89
+ steeringQueue;
90
+ followUpQueue;
91
+ convertToLlm;
92
+ transformContext;
93
+ streamFn;
94
+ getApiKey;
95
+ onPayload;
96
+ onResponse;
97
+ beforeToolCall;
98
+ afterToolCall;
99
+ prepareNextTurn;
100
+ activeRun;
101
+ /** Session identifier forwarded to providers for cache-aware backends. */
102
+ sessionId;
103
+ /** Optional per-level thinking token budgets forwarded to the stream function. */
104
+ thinkingBudgets;
105
+ /** Preferred transport forwarded to the stream function. */
106
+ transport;
107
+ /** Optional cap for provider-requested retry delays. */
108
+ maxRetryDelayMs;
109
+ /** Tool execution strategy for assistant messages that contain multiple tool calls. */
110
+ toolExecution;
111
+ constructor(options = {}) {
112
+ this._state = createMutableAgentState(options.initialState);
113
+ this.convertToLlm = options.convertToLlm ?? defaultConvertToLlm;
114
+ this.transformContext = options.transformContext;
115
+ this.streamFn = options.streamFn ?? streamSimple;
116
+ this.getApiKey = options.getApiKey;
117
+ this.onPayload = options.onPayload;
118
+ this.onResponse = options.onResponse;
119
+ this.beforeToolCall = options.beforeToolCall;
120
+ this.afterToolCall = options.afterToolCall;
121
+ this.prepareNextTurn = options.prepareNextTurn;
122
+ this.steeringQueue = new PendingMessageQueue(options.steeringMode ?? "one-at-a-time");
123
+ this.followUpQueue = new PendingMessageQueue(options.followUpMode ?? "one-at-a-time");
124
+ this.sessionId = options.sessionId;
125
+ this.thinkingBudgets = options.thinkingBudgets;
126
+ this.transport = options.transport ?? "auto";
127
+ this.maxRetryDelayMs = options.maxRetryDelayMs;
128
+ this.toolExecution = options.toolExecution ?? "parallel";
77
129
  }
78
130
  /**
79
- * Get the current max retry delay in milliseconds.
131
+ * Subscribe to agent lifecycle events.
132
+ *
133
+ * Listener promises are awaited in subscription order and are included in
134
+ * the current run's settlement. Listeners also receive the active abort
135
+ * signal for the current run.
136
+ *
137
+ * `agent_end` is the final emitted event for a run, but the agent does not
138
+ * become idle until all awaited listeners for that event have settled.
80
139
  */
81
- get maxRetryDelayMs() {
82
- return this._maxRetryDelayMs;
140
+ subscribe(listener) {
141
+ this.listeners.add(listener);
142
+ return () => this.listeners.delete(listener);
83
143
  }
84
144
  /**
85
- * Set the maximum delay to wait for server-requested retries.
86
- * Set to 0 to disable the cap.
145
+ * Current agent state.
146
+ *
147
+ * Assigning `state.tools` or `state.messages` copies the provided top-level array.
87
148
  */
88
- set maxRetryDelayMs(value) {
89
- this._maxRetryDelayMs = value;
90
- }
91
149
  get state() {
92
150
  return this._state;
93
151
  }
94
- subscribe(fn) {
95
- this.listeners.add(fn);
96
- return () => this.listeners.delete(fn);
97
- }
98
- // State mutators
99
- setSystemPrompt(v) {
100
- this._state.systemPrompt = v;
101
- }
102
- setModel(m) {
103
- this._state.model = m;
104
- }
105
- setThinkingLevel(l) {
106
- this._state.thinkingLevel = l;
107
- }
108
- setSteeringMode(mode) {
109
- this.steeringMode = mode;
152
+ /** Controls how queued steering messages are drained. */
153
+ set steeringMode(mode) {
154
+ this.steeringQueue.mode = mode;
110
155
  }
111
- getSteeringMode() {
112
- return this.steeringMode;
156
+ get steeringMode() {
157
+ return this.steeringQueue.mode;
113
158
  }
114
- setFollowUpMode(mode) {
115
- this.followUpMode = mode;
159
+ /** Controls how queued follow-up messages are drained. */
160
+ set followUpMode(mode) {
161
+ this.followUpQueue.mode = mode;
116
162
  }
117
- getFollowUpMode() {
118
- return this.followUpMode;
163
+ get followUpMode() {
164
+ return this.followUpQueue.mode;
119
165
  }
120
- setTools(t) {
121
- this._state.tools = t;
166
+ /** Queue a message to be injected after the current assistant turn finishes. */
167
+ steer(message) {
168
+ this.steeringQueue.enqueue(message);
122
169
  }
123
- replaceMessages(ms) {
124
- this._state.messages = ms.slice();
125
- }
126
- appendMessage(m) {
127
- this._state.messages = [...this._state.messages, m];
128
- }
129
- /**
130
- * Queue a steering message to interrupt the agent mid-run.
131
- * Delivered after current tool execution, skips remaining tools.
132
- */
133
- steer(m) {
134
- this.steeringQueue.push(m);
135
- }
136
- /**
137
- * Queue a follow-up message to be processed after the agent finishes.
138
- * Delivered only when agent has no more tool calls or steering messages.
139
- */
140
- followUp(m) {
141
- this.followUpQueue.push(m);
170
+ /** Queue a message to run only after the agent would otherwise stop. */
171
+ followUp(message) {
172
+ this.followUpQueue.enqueue(message);
142
173
  }
174
+ /** Remove all queued steering messages. */
143
175
  clearSteeringQueue() {
144
- this.steeringQueue = [];
176
+ this.steeringQueue.clear();
145
177
  }
178
+ /** Remove all queued follow-up messages. */
146
179
  clearFollowUpQueue() {
147
- this.followUpQueue = [];
180
+ this.followUpQueue.clear();
148
181
  }
182
+ /** Remove all queued steering and follow-up messages. */
149
183
  clearAllQueues() {
150
- this.steeringQueue = [];
151
- this.followUpQueue = [];
184
+ this.clearSteeringQueue();
185
+ this.clearFollowUpQueue();
152
186
  }
187
+ /** Returns true when either queue still contains pending messages. */
153
188
  hasQueuedMessages() {
154
- return this.steeringQueue.length > 0 || this.followUpQueue.length > 0;
155
- }
156
- dequeueSteeringMessages() {
157
- if (this.steeringMode === "one-at-a-time") {
158
- if (this.steeringQueue.length > 0) {
159
- const first = this.steeringQueue[0];
160
- this.steeringQueue = this.steeringQueue.slice(1);
161
- return [first];
162
- }
163
- return [];
164
- }
165
- const steering = this.steeringQueue.slice();
166
- this.steeringQueue = [];
167
- return steering;
168
- }
169
- dequeueFollowUpMessages() {
170
- if (this.followUpMode === "one-at-a-time") {
171
- if (this.followUpQueue.length > 0) {
172
- const first = this.followUpQueue[0];
173
- this.followUpQueue = this.followUpQueue.slice(1);
174
- return [first];
175
- }
176
- return [];
177
- }
178
- const followUp = this.followUpQueue.slice();
179
- this.followUpQueue = [];
180
- return followUp;
189
+ return this.steeringQueue.hasItems() || this.followUpQueue.hasItems();
181
190
  }
182
- clearMessages() {
183
- this._state.messages = [];
191
+ /** Active abort signal for the current run, if any. */
192
+ get signal() {
193
+ return this.activeRun?.abortController.signal;
184
194
  }
195
+ /** Abort the current run, if one is active. */
185
196
  abort() {
186
- this.abortController?.abort();
197
+ this.activeRun?.abortController.abort();
187
198
  }
199
+ /**
200
+ * Resolve when the current run and all awaited event listeners have finished.
201
+ *
202
+ * This resolves after `agent_end` listeners settle.
203
+ */
188
204
  waitForIdle() {
189
- return this.runningPrompt ?? Promise.resolve();
205
+ return this.activeRun?.promise ?? Promise.resolve();
190
206
  }
207
+ /** Clear transcript state, runtime state, and queued messages. */
191
208
  reset() {
192
209
  this._state.messages = [];
193
210
  this._state.isStreaming = false;
194
- this._state.streamMessage = null;
211
+ this._state.streamingMessage = undefined;
195
212
  this._state.pendingToolCalls = new Set();
196
- this._state.error = undefined;
197
- this.steeringQueue = [];
198
- this.followUpQueue = [];
213
+ this._state.errorMessage = undefined;
214
+ this.clearFollowUpQueue();
215
+ this.clearSteeringQueue();
199
216
  }
200
217
  async prompt(input, images) {
201
- if (this._state.isStreaming) {
218
+ if (this.activeRun) {
202
219
  throw new Error("Agent is already processing a prompt. Use steer() or followUp() to queue messages, or wait for completion.");
203
220
  }
204
- const model = this._state.model;
205
- if (!model)
206
- throw new Error("No model configured");
207
- let msgs;
208
- if (Array.isArray(input)) {
209
- msgs = input;
210
- }
211
- else if (typeof input === "string") {
212
- const content = [{ type: "text", text: input }];
213
- if (images && images.length > 0) {
214
- content.push(...images);
215
- }
216
- msgs = [
217
- {
218
- role: "user",
219
- content,
220
- timestamp: Date.now(),
221
- },
222
- ];
223
- }
224
- else {
225
- msgs = [input];
226
- }
227
- await this._runLoop(msgs);
221
+ const messages = this.normalizePromptInput(input, images);
222
+ await this.runPromptMessages(messages);
228
223
  }
229
- /**
230
- * Continue from current context (used for retries and resuming queued messages).
231
- */
224
+ /** Continue from the current transcript. The last message must be a user or tool-result message. */
232
225
  async continue() {
233
- if (this._state.isStreaming) {
226
+ if (this.activeRun) {
234
227
  throw new Error("Agent is already processing. Wait for completion before continuing.");
235
228
  }
236
- const messages = this._state.messages;
237
- if (messages.length === 0) {
229
+ const lastMessage = this._state.messages[this._state.messages.length - 1];
230
+ if (!lastMessage) {
238
231
  throw new Error("No messages to continue from");
239
232
  }
240
- if (messages[messages.length - 1].role === "assistant") {
241
- const queuedSteering = this.dequeueSteeringMessages();
233
+ if (lastMessage.role === "assistant") {
234
+ const queuedSteering = this.steeringQueue.drain();
242
235
  if (queuedSteering.length > 0) {
243
- await this._runLoop(queuedSteering, { skipInitialSteeringPoll: true });
236
+ await this.runPromptMessages(queuedSteering, { skipInitialSteeringPoll: true });
244
237
  return;
245
238
  }
246
- const queuedFollowUp = this.dequeueFollowUpMessages();
247
- if (queuedFollowUp.length > 0) {
248
- await this._runLoop(queuedFollowUp);
239
+ const queuedFollowUps = this.followUpQueue.drain();
240
+ if (queuedFollowUps.length > 0) {
241
+ await this.runPromptMessages(queuedFollowUps);
249
242
  return;
250
243
  }
251
244
  throw new Error("Cannot continue from message role: assistant");
252
245
  }
253
- await this._runLoop(undefined);
246
+ await this.runContinuation();
254
247
  }
255
- /**
256
- * Run the agent loop.
257
- * If messages are provided, starts a new conversation turn with those messages.
258
- * Otherwise, continues from existing context.
259
- */
260
- async _runLoop(messages, options) {
261
- const model = this._state.model;
262
- if (!model)
263
- throw new Error("No model configured");
264
- this.runningPrompt = new Promise((resolve) => {
265
- this.resolveRunningPrompt = resolve;
248
+ normalizePromptInput(input, images) {
249
+ if (Array.isArray(input)) {
250
+ return input;
251
+ }
252
+ if (typeof input !== "string") {
253
+ return [input];
254
+ }
255
+ const content = [{ type: "text", text: input }];
256
+ if (images && images.length > 0) {
257
+ content.push(...images);
258
+ }
259
+ return [{ role: "user", content, timestamp: Date.now() }];
260
+ }
261
+ async runPromptMessages(messages, options = {}) {
262
+ await this.runWithLifecycle(async (signal) => {
263
+ await runAgentLoop(messages, this.createContextSnapshot(), this.createLoopConfig(options), (event) => this.processEvents(event), signal, this.streamFn);
266
264
  });
267
- this.abortController = new AbortController();
268
- this._state.isStreaming = true;
269
- this._state.streamMessage = null;
270
- this._state.error = undefined;
271
- const reasoning = this._state.thinkingLevel === "off" ? undefined : this._state.thinkingLevel;
272
- const context = {
265
+ }
266
+ async runContinuation() {
267
+ await this.runWithLifecycle(async (signal) => {
268
+ await runAgentLoopContinue(this.createContextSnapshot(), this.createLoopConfig(), (event) => this.processEvents(event), signal, this.streamFn);
269
+ });
270
+ }
271
+ createContextSnapshot() {
272
+ return {
273
273
  systemPrompt: this._state.systemPrompt,
274
274
  messages: this._state.messages.slice(),
275
- tools: this._state.tools,
275
+ tools: this._state.tools.slice(),
276
276
  };
277
- let skipInitialSteeringPoll = options?.skipInitialSteeringPoll === true;
278
- const config = {
279
- model,
280
- reasoning,
281
- sessionId: this._sessionId,
282
- transport: this._transport,
283
- thinkingBudgets: this._thinkingBudgets,
284
- maxRetryDelayMs: this._maxRetryDelayMs,
277
+ }
278
+ createLoopConfig(options = {}) {
279
+ let skipInitialSteeringPoll = options.skipInitialSteeringPoll === true;
280
+ return {
281
+ model: this._state.model,
282
+ reasoning: this._state.thinkingLevel === "off" ? undefined : this._state.thinkingLevel,
283
+ sessionId: this.sessionId,
284
+ onPayload: this.onPayload,
285
+ onResponse: this.onResponse,
286
+ transport: this.transport,
287
+ thinkingBudgets: this.thinkingBudgets,
288
+ maxRetryDelayMs: this.maxRetryDelayMs,
289
+ toolExecution: this.toolExecution,
290
+ beforeToolCall: this.beforeToolCall,
291
+ afterToolCall: this.afterToolCall,
292
+ prepareNextTurn: this.prepareNextTurn ? async () => await this.prepareNextTurn?.(this.signal) : undefined,
285
293
  convertToLlm: this.convertToLlm,
286
294
  transformContext: this.transformContext,
287
295
  getApiKey: this.getApiKey,
@@ -290,107 +298,104 @@ export class Agent {
290
298
  skipInitialSteeringPoll = false;
291
299
  return [];
292
300
  }
293
- return this.dequeueSteeringMessages();
301
+ return this.steeringQueue.drain();
294
302
  },
295
- getFollowUpMessages: async () => this.dequeueFollowUpMessages(),
303
+ getFollowUpMessages: async () => this.followUpQueue.drain(),
296
304
  };
297
- let partial = null;
305
+ }
306
+ async runWithLifecycle(executor) {
307
+ if (this.activeRun) {
308
+ throw new Error("Agent is already processing.");
309
+ }
310
+ const abortController = new AbortController();
311
+ let resolvePromise = () => { };
312
+ const promise = new Promise((resolve) => {
313
+ resolvePromise = resolve;
314
+ });
315
+ this.activeRun = { promise, resolve: resolvePromise, abortController };
316
+ this._state.isStreaming = true;
317
+ this._state.streamingMessage = undefined;
318
+ this._state.errorMessage = undefined;
298
319
  try {
299
- const stream = messages
300
- ? agentLoop(messages, context, config, this.abortController.signal, this.streamFn)
301
- : agentLoopContinue(context, config, this.abortController.signal, this.streamFn);
302
- for await (const event of stream) {
303
- // Update internal state based on events
304
- switch (event.type) {
305
- case "message_start":
306
- partial = event.message;
307
- this._state.streamMessage = event.message;
308
- break;
309
- case "message_update":
310
- partial = event.message;
311
- this._state.streamMessage = event.message;
312
- break;
313
- case "message_end":
314
- partial = null;
315
- this._state.streamMessage = null;
316
- this.appendMessage(event.message);
317
- break;
318
- case "tool_execution_start": {
319
- const s = new Set(this._state.pendingToolCalls);
320
- s.add(event.toolCallId);
321
- this._state.pendingToolCalls = s;
322
- break;
323
- }
324
- case "tool_execution_end": {
325
- const s = new Set(this._state.pendingToolCalls);
326
- s.delete(event.toolCallId);
327
- this._state.pendingToolCalls = s;
328
- break;
329
- }
330
- case "turn_end":
331
- if (event.message.role === "assistant" && event.message.errorMessage) {
332
- this._state.error = event.message.errorMessage;
333
- }
334
- break;
335
- case "agent_end":
336
- this._state.isStreaming = false;
337
- this._state.streamMessage = null;
338
- break;
339
- }
340
- // Emit to listeners
341
- this.emit(event);
342
- }
343
- // Handle any remaining partial message
344
- if (partial && partial.role === "assistant" && partial.content.length > 0) {
345
- const onlyEmpty = !partial.content.some((c) => (c.type === "thinking" && c.thinking.trim().length > 0) ||
346
- (c.type === "text" && c.text.trim().length > 0) ||
347
- (c.type === "toolCall" && c.name.trim().length > 0));
348
- if (!onlyEmpty) {
349
- this.appendMessage(partial);
350
- }
351
- else {
352
- if (this.abortController?.signal.aborted) {
353
- throw new Error("Request was aborted");
354
- }
355
- }
356
- }
320
+ await executor(abortController.signal);
357
321
  }
358
- catch (err) {
359
- const errorMsg = {
360
- role: "assistant",
361
- content: [{ type: "text", text: "" }],
362
- api: model.api,
363
- provider: model.provider,
364
- model: model.id,
365
- usage: {
366
- input: 0,
367
- output: 0,
368
- cacheRead: 0,
369
- cacheWrite: 0,
370
- totalTokens: 0,
371
- cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
372
- },
373
- stopReason: this.abortController?.signal.aborted ? "aborted" : "error",
374
- errorMessage: err?.message || String(err),
375
- timestamp: Date.now(),
376
- };
377
- this.appendMessage(errorMsg);
378
- this._state.error = err?.message || String(err);
379
- this.emit({ type: "agent_end", messages: [errorMsg] });
322
+ catch (error) {
323
+ await this.handleRunFailure(error, abortController.signal.aborted);
380
324
  }
381
325
  finally {
382
- this._state.isStreaming = false;
383
- this._state.streamMessage = null;
384
- this._state.pendingToolCalls = new Set();
385
- this.abortController = undefined;
386
- this.resolveRunningPrompt?.();
387
- this.runningPrompt = undefined;
388
- this.resolveRunningPrompt = undefined;
326
+ this.finishRun();
389
327
  }
390
328
  }
391
- emit(e) {
329
+ async handleRunFailure(error, aborted) {
330
+ const failureMessage = {
331
+ role: "assistant",
332
+ content: [{ type: "text", text: "" }],
333
+ api: this._state.model.api,
334
+ provider: this._state.model.provider,
335
+ model: this._state.model.id,
336
+ usage: EMPTY_USAGE,
337
+ stopReason: aborted ? "aborted" : "error",
338
+ errorMessage: error instanceof Error ? error.message : String(error),
339
+ timestamp: Date.now(),
340
+ };
341
+ await this.processEvents({ type: "message_start", message: failureMessage });
342
+ await this.processEvents({ type: "message_end", message: failureMessage });
343
+ await this.processEvents({ type: "turn_end", message: failureMessage, toolResults: [] });
344
+ await this.processEvents({ type: "agent_end", messages: [failureMessage] });
345
+ }
346
+ finishRun() {
347
+ this._state.isStreaming = false;
348
+ this._state.streamingMessage = undefined;
349
+ this._state.pendingToolCalls = new Set();
350
+ this.activeRun?.resolve();
351
+ this.activeRun = undefined;
352
+ }
353
+ /**
354
+ * Reduce internal state for a loop event, then await listeners.
355
+ *
356
+ * `agent_end` only means no further loop events will be emitted. The run is
357
+ * considered idle later, after all awaited listeners for `agent_end` finish
358
+ * and `finishRun()` clears runtime-owned state.
359
+ */
360
+ async processEvents(event) {
361
+ switch (event.type) {
362
+ case "message_start":
363
+ this._state.streamingMessage = event.message;
364
+ break;
365
+ case "message_update":
366
+ this._state.streamingMessage = event.message;
367
+ break;
368
+ case "message_end":
369
+ this._state.streamingMessage = undefined;
370
+ this._state.messages.push(event.message);
371
+ break;
372
+ case "tool_execution_start": {
373
+ const pendingToolCalls = new Set(this._state.pendingToolCalls);
374
+ pendingToolCalls.add(event.toolCallId);
375
+ this._state.pendingToolCalls = pendingToolCalls;
376
+ break;
377
+ }
378
+ case "tool_execution_end": {
379
+ const pendingToolCalls = new Set(this._state.pendingToolCalls);
380
+ pendingToolCalls.delete(event.toolCallId);
381
+ this._state.pendingToolCalls = pendingToolCalls;
382
+ break;
383
+ }
384
+ case "turn_end":
385
+ if (event.message.role === "assistant" && event.message.errorMessage) {
386
+ this._state.errorMessage = event.message.errorMessage;
387
+ }
388
+ break;
389
+ case "agent_end":
390
+ this._state.streamingMessage = undefined;
391
+ break;
392
+ }
393
+ const signal = this.activeRun?.abortController.signal;
394
+ if (!signal) {
395
+ throw new Error("Agent listener invoked outside active run");
396
+ }
392
397
  for (const listener of this.listeners) {
393
- listener(e);
398
+ await listener(event, signal);
394
399
  }
395
400
  }
396
401
  }