phi-code-agent 0.56.3 → 0.74.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/README.md +109 -33
- package/dist/agent-loop.d.ts +3 -0
- package/dist/agent-loop.d.ts.map +1 -1
- package/dist/agent-loop.js +298 -127
- package/dist/agent-loop.js.map +1 -1
- package/dist/agent.d.ts +88 -127
- package/dist/agent.d.ts.map +1 -1
- package/dist/agent.js +323 -318
- package/dist/agent.js.map +1 -1
- package/dist/harness/agent-harness.d.ts +85 -0
- package/dist/harness/agent-harness.d.ts.map +1 -0
- package/dist/harness/agent-harness.js +728 -0
- package/dist/harness/agent-harness.js.map +1 -0
- package/dist/harness/compaction/branch-summarization.d.ts +88 -0
- package/dist/harness/compaction/branch-summarization.d.ts.map +1 -0
- package/dist/harness/compaction/branch-summarization.js +243 -0
- package/dist/harness/compaction/branch-summarization.js.map +1 -0
- package/dist/harness/compaction/compaction.d.ts +122 -0
- package/dist/harness/compaction/compaction.d.ts.map +1 -0
- package/dist/harness/compaction/compaction.js +616 -0
- package/dist/harness/compaction/compaction.js.map +1 -0
- package/dist/harness/compaction/utils.d.ts +38 -0
- package/dist/harness/compaction/utils.d.ts.map +1 -0
- package/dist/harness/compaction/utils.js +153 -0
- package/dist/harness/compaction/utils.js.map +1 -0
- package/dist/harness/env/nodejs.d.ts +44 -0
- package/dist/harness/env/nodejs.d.ts.map +1 -0
- package/dist/harness/env/nodejs.js +348 -0
- package/dist/harness/env/nodejs.js.map +1 -0
- package/dist/harness/execution-env.d.ts +4 -0
- package/dist/harness/execution-env.d.ts.map +1 -0
- package/dist/harness/execution-env.js +3 -0
- package/dist/harness/execution-env.js.map +1 -0
- package/dist/harness/messages.d.ts +51 -0
- package/dist/harness/messages.d.ts.map +1 -0
- package/dist/harness/messages.js +102 -0
- package/dist/harness/messages.js.map +1 -0
- package/dist/harness/prompt-templates.d.ts +45 -0
- package/dist/harness/prompt-templates.d.ts.map +1 -0
- package/dist/harness/prompt-templates.js +200 -0
- package/dist/harness/prompt-templates.js.map +1 -0
- package/dist/harness/session/repo/jsonl.d.ts +20 -0
- package/dist/harness/session/repo/jsonl.d.ts.map +1 -0
- package/dist/harness/session/repo/jsonl.js +92 -0
- package/dist/harness/session/repo/jsonl.js.map +1 -0
- package/dist/harness/session/repo/memory.d.ts +18 -0
- package/dist/harness/session/repo/memory.d.ts.map +1 -0
- package/dist/harness/session/repo/memory.js +42 -0
- package/dist/harness/session/repo/memory.js.map +1 -0
- package/dist/harness/session/repo/shared.d.ts +10 -0
- package/dist/harness/session/repo/shared.d.ts.map +1 -0
- package/dist/harness/session/repo/shared.js +31 -0
- package/dist/harness/session/repo/shared.js.map +1 -0
- package/dist/harness/session/session.d.ts +32 -0
- package/dist/harness/session/session.d.ts.map +1 -0
- package/dist/harness/session/session.js +196 -0
- package/dist/harness/session/session.js.map +1 -0
- package/dist/harness/session/storage/jsonl.d.ts +30 -0
- package/dist/harness/session/storage/jsonl.d.ts.map +1 -0
- package/dist/harness/session/storage/jsonl.js +170 -0
- package/dist/harness/session/storage/jsonl.js.map +1 -0
- package/dist/harness/session/storage/memory.d.ts +26 -0
- package/dist/harness/session/storage/memory.d.ts.map +1 -0
- package/dist/harness/session/storage/memory.js +90 -0
- package/dist/harness/session/storage/memory.js.map +1 -0
- package/dist/harness/skills.d.ts +41 -0
- package/dist/harness/skills.d.ts.map +1 -0
- package/dist/harness/skills.js +259 -0
- package/dist/harness/skills.js.map +1 -0
- package/dist/harness/system-prompt.d.ts +3 -0
- package/dist/harness/system-prompt.d.ts.map +1 -0
- package/dist/harness/system-prompt.js +30 -0
- package/dist/harness/system-prompt.js.map +1 -0
- package/dist/harness/types.d.ts +525 -0
- package/dist/harness/types.d.ts.map +1 -0
- package/dist/harness/types.js +16 -0
- package/dist/harness/types.js.map +1 -0
- package/dist/harness/utils/shell-output.d.ts +14 -0
- package/dist/harness/utils/shell-output.d.ts.map +1 -0
- package/dist/harness/utils/shell-output.js +97 -0
- package/dist/harness/utils/shell-output.js.map +1 -0
- package/dist/harness/utils/truncate.d.ts +70 -0
- package/dist/harness/utils/truncate.d.ts.map +1 -0
- package/dist/harness/utils/truncate.js +205 -0
- package/dist/harness/utils/truncate.js.map +1 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +16 -0
- package/dist/index.js.map +1 -1
- package/dist/proxy.d.ts +4 -20
- package/dist/proxy.d.ts.map +1 -1
- package/dist/proxy.js +15 -5
- package/dist/proxy.js.map +1 -1
- package/dist/types.d.ts +224 -16
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/package.json +6 -3
package/dist/agent.js
CHANGED
|
@@ -1,287 +1,295 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
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((
|
|
4
|
+
return messages.filter((message) => message.role === "user" || message.role === "assistant" || message.role === "toolResult");
|
|
12
5
|
}
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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
|
-
|
|
68
|
-
*/
|
|
69
|
-
get transport() {
|
|
70
|
-
return this._transport;
|
|
76
|
+
clear() {
|
|
77
|
+
this.messages = [];
|
|
71
78
|
}
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
82
|
-
|
|
140
|
+
subscribe(listener) {
|
|
141
|
+
this.listeners.add(listener);
|
|
142
|
+
return () => this.listeners.delete(listener);
|
|
83
143
|
}
|
|
84
144
|
/**
|
|
85
|
-
*
|
|
86
|
-
*
|
|
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
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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
|
-
|
|
112
|
-
return this.
|
|
156
|
+
get steeringMode() {
|
|
157
|
+
return this.steeringQueue.mode;
|
|
113
158
|
}
|
|
114
|
-
|
|
115
|
-
|
|
159
|
+
/** Controls how queued follow-up messages are drained. */
|
|
160
|
+
set followUpMode(mode) {
|
|
161
|
+
this.followUpQueue.mode = mode;
|
|
116
162
|
}
|
|
117
|
-
|
|
118
|
-
return this.
|
|
163
|
+
get followUpMode() {
|
|
164
|
+
return this.followUpQueue.mode;
|
|
119
165
|
}
|
|
120
|
-
|
|
121
|
-
|
|
166
|
+
/** Queue a message to be injected after the current assistant turn finishes. */
|
|
167
|
+
steer(message) {
|
|
168
|
+
this.steeringQueue.enqueue(message);
|
|
122
169
|
}
|
|
123
|
-
|
|
124
|
-
|
|
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.
|
|
151
|
-
this.
|
|
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.
|
|
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
|
-
|
|
183
|
-
|
|
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
|
|
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.
|
|
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.
|
|
211
|
+
this._state.streamingMessage = undefined;
|
|
195
212
|
this._state.pendingToolCalls = new Set();
|
|
196
|
-
this._state.
|
|
197
|
-
this.
|
|
198
|
-
this.
|
|
213
|
+
this._state.errorMessage = undefined;
|
|
214
|
+
this.clearFollowUpQueue();
|
|
215
|
+
this.clearSteeringQueue();
|
|
199
216
|
}
|
|
200
217
|
async prompt(input, images) {
|
|
201
|
-
if (this.
|
|
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
|
|
205
|
-
|
|
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.
|
|
226
|
+
if (this.activeRun) {
|
|
234
227
|
throw new Error("Agent is already processing. Wait for completion before continuing.");
|
|
235
228
|
}
|
|
236
|
-
const
|
|
237
|
-
if (
|
|
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 (
|
|
241
|
-
const queuedSteering = this.
|
|
233
|
+
if (lastMessage.role === "assistant") {
|
|
234
|
+
const queuedSteering = this.steeringQueue.drain();
|
|
242
235
|
if (queuedSteering.length > 0) {
|
|
243
|
-
await this.
|
|
236
|
+
await this.runPromptMessages(queuedSteering, { skipInitialSteeringPoll: true });
|
|
244
237
|
return;
|
|
245
238
|
}
|
|
246
|
-
const
|
|
247
|
-
if (
|
|
248
|
-
await this.
|
|
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.
|
|
246
|
+
await this.runContinuation();
|
|
254
247
|
}
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
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
|
-
|
|
268
|
-
|
|
269
|
-
this.
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
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
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
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.
|
|
301
|
+
return this.steeringQueue.drain();
|
|
294
302
|
},
|
|
295
|
-
getFollowUpMessages: async () => this.
|
|
303
|
+
getFollowUpMessages: async () => this.followUpQueue.drain(),
|
|
296
304
|
};
|
|
297
|
-
|
|
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
|
-
|
|
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 (
|
|
359
|
-
|
|
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.
|
|
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
|
-
|
|
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(
|
|
398
|
+
await listener(event, signal);
|
|
394
399
|
}
|
|
395
400
|
}
|
|
396
401
|
}
|