@ddwang/magnitude-core 0.3.1-ddwang.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.
- package/dist/actions/desktopActions.d.ts +58 -0
- package/dist/actions/desktopActions.js +154 -0
- package/dist/actions/index.d.ts +26 -0
- package/dist/actions/index.js +11 -0
- package/dist/actions/taskActions.d.ts +8 -0
- package/dist/actions/taskActions.js +27 -0
- package/dist/actions/types.d.ts +31 -0
- package/dist/actions/types.js +5 -0
- package/dist/actions/util.d.ts +20 -0
- package/dist/actions/util.js +297 -0
- package/dist/actions/webActions.d.ts +79 -0
- package/dist/actions/webActions.js +190 -0
- package/dist/agent/browserAgent.d.ts +29 -0
- package/dist/agent/browserAgent.js +122 -0
- package/dist/agent/errors.d.ts +8 -0
- package/dist/agent/errors.js +68 -0
- package/dist/agent/index.d.ts +53 -0
- package/dist/agent/index.js +377 -0
- package/dist/agent/narrator.d.ts +4 -0
- package/dist/agent/narrator.js +63 -0
- package/dist/agent/pause.test.d.ts +1 -0
- package/dist/agent/pause.test.js +194 -0
- package/dist/ai/baml_client/async_client.d.ts +73 -0
- package/dist/ai/baml_client/async_client.js +152 -0
- package/dist/ai/baml_client/async_request.d.ts +35 -0
- package/dist/ai/baml_client/async_request.js +98 -0
- package/dist/ai/baml_client/config.d.ts +11 -0
- package/dist/ai/baml_client/config.js +20 -0
- package/dist/ai/baml_client/globals.d.ts +16 -0
- package/dist/ai/baml_client/globals.js +37 -0
- package/dist/ai/baml_client/index.d.ts +16 -0
- package/dist/ai/baml_client/index.js +41 -0
- package/dist/ai/baml_client/inlinedbaml.d.ts +19 -0
- package/dist/ai/baml_client/inlinedbaml.js +31 -0
- package/dist/ai/baml_client/parser.d.ts +53 -0
- package/dist/ai/baml_client/parser.js +86 -0
- package/dist/ai/baml_client/partial_types.d.ts +61 -0
- package/dist/ai/baml_client/partial_types.js +10 -0
- package/dist/ai/baml_client/sync_client.d.ts +50 -0
- package/dist/ai/baml_client/sync_client.js +100 -0
- package/dist/ai/baml_client/sync_request.d.ts +35 -0
- package/dist/ai/baml_client/sync_request.js +98 -0
- package/dist/ai/baml_client/tracing.d.ts +16 -0
- package/dist/ai/baml_client/tracing.js +18 -0
- package/dist/ai/baml_client/type_builder.d.ts +38 -0
- package/dist/ai/baml_client/type_builder.js +96 -0
- package/dist/ai/baml_client/types.d.ts +69 -0
- package/dist/ai/baml_client/types.js +15 -0
- package/dist/ai/claudeCode.d.ts +1 -0
- package/dist/ai/claudeCode.js +186 -0
- package/dist/ai/modelHarness.d.ts +39 -0
- package/dist/ai/modelHarness.js +189 -0
- package/dist/ai/multiModelHarness.d.ts +27 -0
- package/dist/ai/multiModelHarness.js +52 -0
- package/dist/ai/types.d.ts +90 -0
- package/dist/ai/types.js +5 -0
- package/dist/ai/util.d.ts +13 -0
- package/dist/ai/util.js +165 -0
- package/dist/cli.d.ts +0 -0
- package/dist/cli.js +3 -0
- package/dist/common/actions.d.ts +0 -0
- package/dist/common/actions.js +14 -0
- package/dist/common/events.d.ts +15 -0
- package/dist/common/events.js +3 -0
- package/dist/common/failure.d.ts +68 -0
- package/dist/common/failure.js +1 -0
- package/dist/common/index.d.ts +3 -0
- package/dist/common/index.js +3 -0
- package/dist/common/retry.d.ts +16 -0
- package/dist/common/retry.js +30 -0
- package/dist/common/util.d.ts +23 -0
- package/dist/common/util.js +132 -0
- package/dist/connectors/browserConnector.d.ts +40 -0
- package/dist/connectors/browserConnector.js +127 -0
- package/dist/connectors/desktopConnector.d.ts +58 -0
- package/dist/connectors/desktopConnector.js +68 -0
- package/dist/connectors/index.d.ts +10 -0
- package/dist/connectors/index.js +2 -0
- package/dist/index.cjs +6143 -0
- package/dist/index.d.cts +825 -0
- package/dist/index.d.ts +20 -0
- package/dist/index.js +26 -0
- package/dist/index.mjs +6097 -0
- package/dist/logger.d.ts +3 -0
- package/dist/logger.js +15 -0
- package/dist/memory/agentMemory.d.ts +37 -0
- package/dist/memory/agentMemory.js +121 -0
- package/dist/memory/image.d.ts +26 -0
- package/dist/memory/image.js +106 -0
- package/dist/memory/index.d.ts +3 -0
- package/dist/memory/index.js +3 -0
- package/dist/memory/masking.d.ts +7 -0
- package/dist/memory/masking.js +141 -0
- package/dist/memory/masking.test.d.ts +1 -0
- package/dist/memory/masking.test.js +117 -0
- package/dist/memory/observation.d.ts +38 -0
- package/dist/memory/observation.js +79 -0
- package/dist/memory/rendering/index.d.ts +10 -0
- package/dist/memory/rendering/index.js +13 -0
- package/dist/memory/rendering/renderJsonParts.d.ts +3 -0
- package/dist/memory/rendering/renderJsonParts.js +115 -0
- package/dist/memory/rendering/renderJsonParts.test.d.ts +1 -0
- package/dist/memory/rendering/renderJsonParts.test.js +217 -0
- package/dist/memory/rendering/renderXmlParts.d.ts +7 -0
- package/dist/memory/rendering/renderXmlParts.js +127 -0
- package/dist/memory/serde.d.ts +20 -0
- package/dist/memory/serde.js +81 -0
- package/dist/memory/util.d.ts +2 -0
- package/dist/memory/util.js +17 -0
- package/dist/telemetry/events.d.ts +9 -0
- package/dist/telemetry/events.js +80 -0
- package/dist/telemetry/index.d.ts +6 -0
- package/dist/telemetry/index.js +91 -0
- package/dist/types.d.ts +30 -0
- package/dist/types.js +1 -0
- package/dist/util.d.ts +1 -0
- package/dist/util.js +12 -0
- package/dist/version.d.ts +1 -0
- package/dist/version.js +3 -0
- package/dist/web/browserProvider.d.ts +23 -0
- package/dist/web/browserProvider.js +153 -0
- package/dist/web/harness.d.ts +102 -0
- package/dist/web/harness.js +340 -0
- package/dist/web/stability.d.ts +57 -0
- package/dist/web/stability.js +316 -0
- package/dist/web/tabs.d.ts +58 -0
- package/dist/web/tabs.js +293 -0
- package/dist/web/transformer.d.ts +7 -0
- package/dist/web/transformer.js +41 -0
- package/dist/web/types.d.ts +32 -0
- package/dist/web/types.js +2 -0
- package/dist/web/util.d.ts +3 -0
- package/dist/web/util.js +81 -0
- package/dist/web/visualizer/cursor.d.ts +16 -0
- package/dist/web/visualizer/cursor.js +152 -0
- package/dist/web/visualizer/index.d.ts +22 -0
- package/dist/web/visualizer/index.js +47 -0
- package/dist/web/visualizer/mouseEffects.d.ts +18 -0
- package/dist/web/visualizer/mouseEffects.js +287 -0
- package/dist/web/visualizer/typeEffects.d.ts +10 -0
- package/dist/web/visualizer/typeEffects.js +190 -0
- package/package.json +71 -0
|
@@ -0,0 +1,377 @@
|
|
|
1
|
+
import logger from '@/logger';
|
|
2
|
+
import EventEmitter from "eventemitter3";
|
|
3
|
+
import z from "zod";
|
|
4
|
+
import { Observation } from '@/memory/observation';
|
|
5
|
+
import { AgentError } from "@/agent/errors";
|
|
6
|
+
import { AgentMemory } from "@/memory";
|
|
7
|
+
import { taskActions } from "@/actions/taskActions";
|
|
8
|
+
import { traceAsync } from "@/ai/baml_client";
|
|
9
|
+
import { telemetrifyAgent } from '@/telemetry/events';
|
|
10
|
+
import { isClaude } from '@/ai/util';
|
|
11
|
+
import { retryOnError } from '@/common';
|
|
12
|
+
import { renderContentParts } from '@/memory/rendering';
|
|
13
|
+
import { MultiModelHarness } from '@/ai/multiModelHarness';
|
|
14
|
+
// Options for the startAgent helper function
|
|
15
|
+
const DEFAULT_CONFIG = {
|
|
16
|
+
actions: [...taskActions], // Default to taskActions; other actions come from connectors
|
|
17
|
+
connectors: [],
|
|
18
|
+
llm: {
|
|
19
|
+
provider: 'google-ai',
|
|
20
|
+
options: {
|
|
21
|
+
model: 'gemini-2.5-pro-preview-05-06',
|
|
22
|
+
apiKey: process.env.GOOGLE_API_KEY || "YOUR_GOOGLE_API_KEY"
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
prompt: null,
|
|
26
|
+
telemetry: true,
|
|
27
|
+
};
|
|
28
|
+
export class Agent {
|
|
29
|
+
// maybe remove conns/actions from options since stored sep
|
|
30
|
+
options; //Omit<Required<AgentOptions>, 'actions'>;
|
|
31
|
+
connectors;
|
|
32
|
+
actions; // actions from connectors + any other additional ones configured
|
|
33
|
+
memoryOptions;
|
|
34
|
+
models;
|
|
35
|
+
//public readonly model: ModelHarness;
|
|
36
|
+
//public readonly micro: GroundingService;
|
|
37
|
+
//public readonly events: EventEmitter<AgentEvents>;
|
|
38
|
+
//protected readonly _emitter: EventEmitter<AgentEvents>;
|
|
39
|
+
events = new EventEmitter();
|
|
40
|
+
//public readonly memory: AgentMemory;
|
|
41
|
+
doneActing;
|
|
42
|
+
_paused = false;
|
|
43
|
+
_pauseResolve = null;
|
|
44
|
+
latestTaskMemory; // | null = null;
|
|
45
|
+
constructor(baseConfig = {}) {
|
|
46
|
+
this.options = {
|
|
47
|
+
...DEFAULT_CONFIG,
|
|
48
|
+
...baseConfig,
|
|
49
|
+
connectors: baseConfig.connectors ?? [],
|
|
50
|
+
actions: [...(baseConfig.actions || DEFAULT_CONFIG.actions)],
|
|
51
|
+
};
|
|
52
|
+
this.connectors = this.options.connectors;
|
|
53
|
+
// Aggregate actions from connectors
|
|
54
|
+
//const aggregatedActions = [...this.options.actions];
|
|
55
|
+
this.actions = [...this.options.actions];
|
|
56
|
+
for (const connector of this.connectors) {
|
|
57
|
+
this.actions.push(...(connector.getActionSpace ? connector.getActionSpace() : []));
|
|
58
|
+
}
|
|
59
|
+
// Deduplicate actions by name
|
|
60
|
+
// TODO: maybe error instead, or automatically differentiate them?
|
|
61
|
+
//this.options.actions = Array.from(new Map(aggregatedActions.map(actDef => [actDef.name, actDef])).values());
|
|
62
|
+
const llms = Array.isArray(this.options.llm) ? this.options.llm : [this.options.llm];
|
|
63
|
+
let doPromptCaching = false;
|
|
64
|
+
for (const client of llms) {
|
|
65
|
+
// If any LLM is prompt-caching compatible, turn on prompt caching overall for memory etc.
|
|
66
|
+
if (isClaude(client) && (client.provider === 'anthropic' || client.provider === 'claude-code')) {
|
|
67
|
+
// Prompt-caching compatible client
|
|
68
|
+
if ('promptCaching' in client.options && client.options.promptCaching !== undefined) {
|
|
69
|
+
doPromptCaching = client.options.promptCaching;
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
// Default to true if not specified, and override on client config to true
|
|
73
|
+
doPromptCaching = true;
|
|
74
|
+
client.options.promptCaching = true;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
//this.model = new ModelHarness({ llm: this.options.llm });
|
|
79
|
+
this.models = new MultiModelHarness(llms);
|
|
80
|
+
this.models.events.on('tokensUsed', (usage) => this.events.emit('tokensUsed', usage), this);
|
|
81
|
+
this.doneActing = false;
|
|
82
|
+
this._paused = false;
|
|
83
|
+
this._pauseResolve = null;
|
|
84
|
+
this.memoryOptions = {
|
|
85
|
+
// TODO: maybe do if Gemini or other prompt caching supported providers as well
|
|
86
|
+
// Claude supports prompt caching but only via Anthropic, not on Bedrock
|
|
87
|
+
promptCaching: doPromptCaching
|
|
88
|
+
};
|
|
89
|
+
// Empty memory will get replaced on first act(), but this prevents errors from having undefined memory
|
|
90
|
+
this.latestTaskMemory = new AgentMemory(this.memoryOptions);
|
|
91
|
+
}
|
|
92
|
+
getConnector(connectorClass) {
|
|
93
|
+
return this.connectors.find(c => c instanceof connectorClass);
|
|
94
|
+
}
|
|
95
|
+
require(connectorClass) {
|
|
96
|
+
const connector = this.getConnector(connectorClass);
|
|
97
|
+
if (!connector)
|
|
98
|
+
throw new Error(`Missing required connector ${connectorClass}`);
|
|
99
|
+
return connector;
|
|
100
|
+
}
|
|
101
|
+
async start() {
|
|
102
|
+
// Register telemetry if enabled - do on start instead of cons to prevent weird subclass event issues
|
|
103
|
+
if (this.options.telemetry)
|
|
104
|
+
telemetrifyAgent(this);
|
|
105
|
+
//console.log('setting up model')
|
|
106
|
+
await this.models.setup();
|
|
107
|
+
//console.log('done setting up model')
|
|
108
|
+
logger.info("Agent: Starting connectors...");
|
|
109
|
+
for (const connector of this.connectors) {
|
|
110
|
+
if (connector.onStart)
|
|
111
|
+
await connector.onStart();
|
|
112
|
+
}
|
|
113
|
+
this.events.emit('start');
|
|
114
|
+
logger.info("Agent: All connectors started.");
|
|
115
|
+
// logger.info("Making initial observations...");
|
|
116
|
+
// await this._recordConnectorObservations();
|
|
117
|
+
// logger.info("Initial observations recorded");
|
|
118
|
+
// Initial observations are handled by the first getObservations call in exec
|
|
119
|
+
}
|
|
120
|
+
identifyAction(action) {
|
|
121
|
+
// Get definition corresponding to an action
|
|
122
|
+
const actionDefinition = this.actions.find(def => def.name === action.variant);
|
|
123
|
+
if (!actionDefinition) {
|
|
124
|
+
// It's possible the action name was from a connector that is no longer active,
|
|
125
|
+
// or the action space was not correctly aggregated.
|
|
126
|
+
throw new AgentError(`Undefined action type '${action.variant}'. Ensure agent is configured with appropriate action definitions from connectors.`);
|
|
127
|
+
}
|
|
128
|
+
return actionDefinition;
|
|
129
|
+
}
|
|
130
|
+
async exec(action, memory) {
|
|
131
|
+
/**
|
|
132
|
+
* Execute an action that belongs to this Agent's action space.
|
|
133
|
+
* Provide memory to record the action taken, its results, and any connector observations to that memory.
|
|
134
|
+
*/
|
|
135
|
+
let actionDefinition = this.identifyAction(action);
|
|
136
|
+
let input;
|
|
137
|
+
if (actionDefinition.schema instanceof z.ZodObject) {
|
|
138
|
+
let variant;
|
|
139
|
+
({ variant, ...input } = action);
|
|
140
|
+
}
|
|
141
|
+
else {
|
|
142
|
+
input = action.input;
|
|
143
|
+
}
|
|
144
|
+
let parsed = actionDefinition.schema.safeParse(input);
|
|
145
|
+
if (!parsed.success) {
|
|
146
|
+
throw new AgentError(`Generated action '${action.variant}' violates input schema: ${parsed.error.message}`, { adaptable: true });
|
|
147
|
+
}
|
|
148
|
+
this.events.emit('actionStarted', action);
|
|
149
|
+
const data = await actionDefinition.resolver({ input: parsed.data, agent: this });
|
|
150
|
+
this.events.emit('actionDone', action);
|
|
151
|
+
if (memory) {
|
|
152
|
+
// Record action taken
|
|
153
|
+
memory.recordObservation(Observation.fromActionTaken(actionDefinition.name, JSON.stringify(action)));
|
|
154
|
+
// Record results of action
|
|
155
|
+
if (data) {
|
|
156
|
+
memory.recordObservation(Observation.fromActionResult(actionDefinition.name, data));
|
|
157
|
+
}
|
|
158
|
+
// Collect and record observations from connectors
|
|
159
|
+
await this._recordConnectorObservations(memory);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
async _recordConnectorObservations(memory) {
|
|
163
|
+
for (const connector of this.connectors) {
|
|
164
|
+
// could do Promise.all if matters
|
|
165
|
+
const connObservations = connector.collectObservations ? await connector.collectObservations() : [];
|
|
166
|
+
//observations.push(...connObservations);
|
|
167
|
+
for (const obs of connObservations) {
|
|
168
|
+
memory.recordObservation(obs);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
get memory() {
|
|
173
|
+
//if (!this.latestTaskMemory) throw new Error("No memory available");
|
|
174
|
+
return this.latestTaskMemory;
|
|
175
|
+
}
|
|
176
|
+
async act(taskOrSteps, options = {}) {
|
|
177
|
+
const instructions = [
|
|
178
|
+
...(this.options.prompt ? [this.options.prompt] : []),
|
|
179
|
+
...(options.prompt ? [options.prompt] : []),
|
|
180
|
+
].join('\n');
|
|
181
|
+
const taskMemory = options.memory ?? new AgentMemory({ ...this.memoryOptions, instructions: instructions === '' ? undefined : instructions });
|
|
182
|
+
if (Array.isArray(taskOrSteps)) {
|
|
183
|
+
const steps = taskOrSteps;
|
|
184
|
+
//this.events.emit('actStarted', steps.join(', '));
|
|
185
|
+
// trace overall task
|
|
186
|
+
await (traceAsync('multistep', async (steps, options) => {
|
|
187
|
+
for (const step of steps) {
|
|
188
|
+
this.events.emit('actStarted', step, options);
|
|
189
|
+
await this._traceAct(step, taskMemory, options);
|
|
190
|
+
this.events.emit('actDone', step, options);
|
|
191
|
+
}
|
|
192
|
+
})(steps, options));
|
|
193
|
+
//this.events.emit('actDone', steps.join(', '));
|
|
194
|
+
}
|
|
195
|
+
else {
|
|
196
|
+
const task = taskOrSteps;
|
|
197
|
+
this.events.emit('actStarted', task, options);
|
|
198
|
+
await this._traceAct(task, taskMemory, options);
|
|
199
|
+
this.events.emit('actDone', task, options);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
async _traceAct(task, memory, options = {}) {
|
|
203
|
+
// memory not serializable to trace so bake it
|
|
204
|
+
await (traceAsync('act', async (task) => {
|
|
205
|
+
await this._act(task, memory, options);
|
|
206
|
+
})(task));
|
|
207
|
+
}
|
|
208
|
+
async _buildContext(memory) {
|
|
209
|
+
const messages = await memory.render();
|
|
210
|
+
const connectorInstructions = [];
|
|
211
|
+
for (const connector of this.connectors) {
|
|
212
|
+
if (connector.getInstructions) {
|
|
213
|
+
const instructions = await connector.getInstructions();
|
|
214
|
+
if (instructions) {
|
|
215
|
+
connectorInstructions.push({
|
|
216
|
+
connectorId: connector.id,
|
|
217
|
+
instructions: instructions
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
return {
|
|
223
|
+
instructions: memory.instructions,
|
|
224
|
+
observationContent: messages,
|
|
225
|
+
//observationContent: content,
|
|
226
|
+
connectorInstructions: connectorInstructions
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
async _act(description, memory, options = {}) {
|
|
230
|
+
this.doneActing = false;
|
|
231
|
+
logger.info(`Act: ${description}`);
|
|
232
|
+
// for now simply add data to task
|
|
233
|
+
let dataContentParts = [];
|
|
234
|
+
if (options.data) {
|
|
235
|
+
//description += "\nUse the following data where appropriate:\n";
|
|
236
|
+
// description += "\n<data>\n";
|
|
237
|
+
// // if (typeof options.data === 'string') {
|
|
238
|
+
// // description += options.data;
|
|
239
|
+
// // } else {
|
|
240
|
+
// // description += Object.entries(options.data).map(([k, v]) => `${k}: ${v}`).join("\n");
|
|
241
|
+
// // }
|
|
242
|
+
// const parts = renderParts(options.data);
|
|
243
|
+
// description += "\n</data>";
|
|
244
|
+
dataContentParts = await renderContentParts(options.data, { mode: 'json', indent: 2 });
|
|
245
|
+
}
|
|
246
|
+
//this.events.emit('stepStart', description);
|
|
247
|
+
//const testData = convertOptionsToTestData(options);
|
|
248
|
+
// Initialize task memory and record initial observations
|
|
249
|
+
// Combine any agent-level and task-level instructions
|
|
250
|
+
this.latestTaskMemory = memory;
|
|
251
|
+
// record initial observations
|
|
252
|
+
logger.info("Making initial observations...");
|
|
253
|
+
await this._recordConnectorObservations(memory);
|
|
254
|
+
logger.info("Initial observations recorded");
|
|
255
|
+
while (true) {
|
|
256
|
+
// Removed direct screenshot/tabState access here; it's part of memoryContext via connectors
|
|
257
|
+
logger.info(`Creating partial recipe`);
|
|
258
|
+
let reasoning = "";
|
|
259
|
+
let actions = [];
|
|
260
|
+
try {
|
|
261
|
+
const memoryContext = await this._buildContext(memory);
|
|
262
|
+
await retryOnError(async () => {
|
|
263
|
+
({ reasoning, actions } = await this.models.partialAct(memoryContext, description, dataContentParts, this.actions));
|
|
264
|
+
if (actions.length === 0) {
|
|
265
|
+
// Empty action list behavior - default wait else ... err? what if not in action space?
|
|
266
|
+
//actions.push()
|
|
267
|
+
throw new AgentError(`No actions generated`);
|
|
268
|
+
}
|
|
269
|
+
},
|
|
270
|
+
// HTTP body is not JSON - comes from Anthropic sometimes, weird error
|
|
271
|
+
// Sometimes Anthropic will give 401 Unauthorized randomly even when authorized
|
|
272
|
+
{
|
|
273
|
+
mode: 'retry_on_partial_message',
|
|
274
|
+
errorSubstrings: ['HTTP body is not JSON', '401 Unauthorized', 'No actions generated'],
|
|
275
|
+
retryLimit: 3,
|
|
276
|
+
delayMs: 1000,
|
|
277
|
+
showWarnOnRetry: true
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
catch (error) {
|
|
281
|
+
logger.error(`Error planning actions: ${error instanceof Error ? error.message : String(error)}`);
|
|
282
|
+
/**
|
|
283
|
+
* (1) Failure to conform to JSON
|
|
284
|
+
* (2) Misconfigured BAML client / bad API key
|
|
285
|
+
* (3) Network error (past max retries)
|
|
286
|
+
*/
|
|
287
|
+
// this.fail({
|
|
288
|
+
// variant: 'misalignment',
|
|
289
|
+
// message: `Could not create partial recipe -> ${(error as Error).message}`
|
|
290
|
+
// });
|
|
291
|
+
throw new AgentError(`Error planning actions: ${error.message}`, { variant: 'misalignment' });
|
|
292
|
+
}
|
|
293
|
+
logger.info({ reasoning, actions }, `Partial recipe created`);
|
|
294
|
+
// Could be emitted in memory and bubbled up instead of recordThought was called in more places
|
|
295
|
+
this.events.emit('thought', reasoning);
|
|
296
|
+
memory.recordThought(reasoning);
|
|
297
|
+
// Execute partial recipe
|
|
298
|
+
for (const action of actions) {
|
|
299
|
+
await this._waitIfPaused();
|
|
300
|
+
if (this.doneActing)
|
|
301
|
+
break;
|
|
302
|
+
await this.exec(action, memory);
|
|
303
|
+
// const postActionScreenshot = await this.screenshot();
|
|
304
|
+
// const actionDescriptor: ActionDescriptor = { ...action, screenshot: postActionScreenshot.image } as ActionDescriptor;
|
|
305
|
+
// this.events.emit('action', actionDescriptor);
|
|
306
|
+
logger.info({ action }, `Action taken`);
|
|
307
|
+
}
|
|
308
|
+
// If macro expects these actions should complete the step, break
|
|
309
|
+
// if (finished) {
|
|
310
|
+
// break;
|
|
311
|
+
// }
|
|
312
|
+
await this._waitIfPaused();
|
|
313
|
+
if (this.doneActing) {
|
|
314
|
+
break;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
logger.info(`Done with step`);
|
|
318
|
+
//this.events.emit('stepSuccess');
|
|
319
|
+
//this.currentTaskMemory = null;
|
|
320
|
+
}
|
|
321
|
+
async query(query, schema) {
|
|
322
|
+
// Record observations in case no act() was used beforehand
|
|
323
|
+
await this._recordConnectorObservations(this.latestTaskMemory);
|
|
324
|
+
const memoryContext = await this._buildContext(this.memory); //this.memory.buildContext(this.connectors);
|
|
325
|
+
return await this.models.query(memoryContext, query, schema);
|
|
326
|
+
}
|
|
327
|
+
async queueDone() {
|
|
328
|
+
this.doneActing = true;
|
|
329
|
+
}
|
|
330
|
+
async _waitIfPaused() {
|
|
331
|
+
if (!this._paused)
|
|
332
|
+
return;
|
|
333
|
+
this.events.emit('pause');
|
|
334
|
+
logger.info("Agent: Paused");
|
|
335
|
+
await new Promise((resolve) => {
|
|
336
|
+
this._pauseResolve = resolve;
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
pause() {
|
|
340
|
+
this._paused = true;
|
|
341
|
+
}
|
|
342
|
+
resume() {
|
|
343
|
+
this._paused = false;
|
|
344
|
+
if (this._pauseResolve) {
|
|
345
|
+
this._pauseResolve();
|
|
346
|
+
this._pauseResolve = null;
|
|
347
|
+
}
|
|
348
|
+
this.events.emit('resume');
|
|
349
|
+
logger.info("Agent: Resumed");
|
|
350
|
+
}
|
|
351
|
+
get paused() {
|
|
352
|
+
return this._paused;
|
|
353
|
+
}
|
|
354
|
+
async stop() {
|
|
355
|
+
/**
|
|
356
|
+
* Stop the agent and close the browser context.
|
|
357
|
+
* May be called asynchronously and interrupt an agent in the middle of a action sequence.
|
|
358
|
+
*/
|
|
359
|
+
this.doneActing = true;
|
|
360
|
+
if (this._paused) {
|
|
361
|
+
this.resume(); // unblock so loop can see doneActing and exit
|
|
362
|
+
}
|
|
363
|
+
logger.info("Agent: Stopping connectors...");
|
|
364
|
+
for (const connector of this.connectors) {
|
|
365
|
+
try {
|
|
366
|
+
if (connector.onStop)
|
|
367
|
+
await connector.onStop();
|
|
368
|
+
}
|
|
369
|
+
catch (error) {
|
|
370
|
+
logger.warn(`Agent: Error stopping connector ${connector.id}: ${error instanceof Error ? error.message : String(error)}`);
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
this.events.emit('stop');
|
|
374
|
+
logger.info("Agent: All connectors stopped.");
|
|
375
|
+
logger.info("Agent: Stopped successfully.");
|
|
376
|
+
}
|
|
377
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { blueBright, bold, cyanBright, gray } from 'ansis';
|
|
2
|
+
export function narrateAgent(agent) {
|
|
3
|
+
let totalInputTokens = 0;
|
|
4
|
+
let totalOutputTokens = 0;
|
|
5
|
+
let totalCachedWriteInputTokens = 0;
|
|
6
|
+
let totalCachedReadInputTokens = 0;
|
|
7
|
+
let totalInputTokenCost = 0.0;
|
|
8
|
+
let totalOutputTokenCost = 0.0;
|
|
9
|
+
agent.events.on('tokensUsed', (usage) => {
|
|
10
|
+
totalInputTokens += usage.inputTokens;
|
|
11
|
+
totalOutputTokens += usage.outputTokens;
|
|
12
|
+
totalCachedWriteInputTokens += usage.cacheWriteInputTokens ?? 0;
|
|
13
|
+
totalCachedReadInputTokens += usage.cacheReadInputTokens ?? 0;
|
|
14
|
+
totalInputTokenCost += usage.inputCost ?? 0.0;
|
|
15
|
+
totalOutputTokenCost += usage.outputCost ?? 0.0;
|
|
16
|
+
});
|
|
17
|
+
agent.events.on('start', () => {
|
|
18
|
+
console.log(bold(blueBright(`▶ [start] agent started with ${agent.models.describe()}`)));
|
|
19
|
+
});
|
|
20
|
+
agent.events.on('stop', () => {
|
|
21
|
+
console.log(bold(blueBright(`■ [stop] agent stopped`)));
|
|
22
|
+
console.log(` Total usage: ` + bold `${totalInputTokens + totalCachedWriteInputTokens + totalCachedReadInputTokens}` + ` input tokens` + (totalCachedWriteInputTokens > 0 || totalCachedReadInputTokens > 0 ? ` (${totalCachedWriteInputTokens} cache write, ${totalCachedReadInputTokens} cache read)` : '') + ` / ` + bold `${totalOutputTokens}` + ` output tokens`);
|
|
23
|
+
if (totalInputTokenCost > 0 || totalOutputTokenCost > 0) {
|
|
24
|
+
if (agent.models.numUniqueModels === 1 && agent.models.describe().startsWith('claude-code')) {
|
|
25
|
+
console.log(` Cost: ` + cyanBright `None - using Claude Pro or Max subscription`);
|
|
26
|
+
}
|
|
27
|
+
else {
|
|
28
|
+
console.log(` Cost: $${(totalInputTokenCost + totalOutputTokenCost).toFixed(3)}`);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
// Show token usage and cost if available
|
|
32
|
+
// if (totalInputTokenCost > 0 || totalOutputTokenCost > 0) {
|
|
33
|
+
// console.log(` Total usage: ` + bold`${totalInputTokens}` + ` input tokens (` + `$${totalInputTokenCost.toFixed(3)}` + `)` + ` / ` + bold`${totalOutputTokens}` + ` output tokens (` + `$${totalOutputTokenCost.toFixed(3)}` + `)`);
|
|
34
|
+
// } else {
|
|
35
|
+
// console.log(` Total usage: ` + bold`${totalInputTokens}` + ` input tokens` + ` / ` + bold`${totalOutputTokens}` + ` output tokens`);
|
|
36
|
+
// }
|
|
37
|
+
});
|
|
38
|
+
agent.events.on('thought', (thought) => {
|
|
39
|
+
console.log(gray `${thought}`);
|
|
40
|
+
//console.log(gray`⚙︎ ${thought}`);
|
|
41
|
+
});
|
|
42
|
+
agent.events.on('actStarted', (task, options) => {
|
|
43
|
+
console.log(bold(cyanBright(`◆ [act] ${task}`)));
|
|
44
|
+
});
|
|
45
|
+
agent.events.on('actionStarted', (action) => {
|
|
46
|
+
const actionDefinition = agent.identifyAction(action);
|
|
47
|
+
console.log(bold ` ${actionDefinition.render(action)}`);
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
export function narrateBrowserAgent(agent) {
|
|
51
|
+
narrateAgent(agent);
|
|
52
|
+
agent.browserAgentEvents.on('nav', (url) => {
|
|
53
|
+
console.log(bold(cyanBright `⛓︎ [nav] ${url}`));
|
|
54
|
+
});
|
|
55
|
+
agent.browserAgentEvents.on('extractStarted', (instructions, schema) => {
|
|
56
|
+
console.log(bold(cyanBright `⛏ [extract] ${instructions}`));
|
|
57
|
+
});
|
|
58
|
+
agent.browserAgentEvents.on('extractDone', (instructions, data) => {
|
|
59
|
+
// console.log has a decent default formatter for arbitrary data e.g. objects
|
|
60
|
+
console.log(data);
|
|
61
|
+
//console.log(blueBright`${JSON.stringify(data, null, 2)}`);
|
|
62
|
+
});
|
|
63
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test';
|
|
2
|
+
import EventEmitter from 'eventemitter3';
|
|
3
|
+
// Minimal Agent-like class that isolates pause/resume logic for testing
|
|
4
|
+
// without requiring LLM clients, connectors, or BAML dependencies
|
|
5
|
+
class PauseableLoop {
|
|
6
|
+
events = new EventEmitter();
|
|
7
|
+
doneActing = false;
|
|
8
|
+
_paused = false;
|
|
9
|
+
_pauseResolve = null;
|
|
10
|
+
async _waitIfPaused() {
|
|
11
|
+
if (!this._paused)
|
|
12
|
+
return;
|
|
13
|
+
this.events.emit('pause');
|
|
14
|
+
await new Promise((resolve) => {
|
|
15
|
+
this._pauseResolve = resolve;
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
pause() {
|
|
19
|
+
this._paused = true;
|
|
20
|
+
}
|
|
21
|
+
resume() {
|
|
22
|
+
this._paused = false;
|
|
23
|
+
if (this._pauseResolve) {
|
|
24
|
+
this._pauseResolve();
|
|
25
|
+
this._pauseResolve = null;
|
|
26
|
+
}
|
|
27
|
+
this.events.emit('resume');
|
|
28
|
+
}
|
|
29
|
+
get paused() {
|
|
30
|
+
return this._paused;
|
|
31
|
+
}
|
|
32
|
+
queueDone() {
|
|
33
|
+
this.doneActing = true;
|
|
34
|
+
}
|
|
35
|
+
stop() {
|
|
36
|
+
this.doneActing = true;
|
|
37
|
+
if (this._paused) {
|
|
38
|
+
this.resume();
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Simulates the Agent._act() loop structure.
|
|
43
|
+
* Each "batch" is an array of action labels. The loop processes batches
|
|
44
|
+
* until doneActing is set, mirroring the real while(true) loop.
|
|
45
|
+
*/
|
|
46
|
+
async runLoop(batches, onAction) {
|
|
47
|
+
this.doneActing = false;
|
|
48
|
+
let batchIndex = 0;
|
|
49
|
+
while (true) {
|
|
50
|
+
const actions = batches[batchIndex % batches.length];
|
|
51
|
+
batchIndex++;
|
|
52
|
+
for (const action of actions) {
|
|
53
|
+
await this._waitIfPaused();
|
|
54
|
+
if (this.doneActing)
|
|
55
|
+
return;
|
|
56
|
+
onAction(action);
|
|
57
|
+
}
|
|
58
|
+
await this._waitIfPaused();
|
|
59
|
+
if (this.doneActing)
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
describe('Agent pause/resume', () => {
|
|
65
|
+
test('pause() sets paused flag', () => {
|
|
66
|
+
const loop = new PauseableLoop();
|
|
67
|
+
expect(loop.paused).toBe(false);
|
|
68
|
+
loop.pause();
|
|
69
|
+
expect(loop.paused).toBe(true);
|
|
70
|
+
});
|
|
71
|
+
test('resume() clears paused flag', () => {
|
|
72
|
+
const loop = new PauseableLoop();
|
|
73
|
+
loop.pause();
|
|
74
|
+
loop.resume();
|
|
75
|
+
expect(loop.paused).toBe(false);
|
|
76
|
+
});
|
|
77
|
+
test('resume() when not paused is a no-op (no error)', () => {
|
|
78
|
+
const loop = new PauseableLoop();
|
|
79
|
+
expect(() => loop.resume()).not.toThrow();
|
|
80
|
+
expect(loop.paused).toBe(false);
|
|
81
|
+
});
|
|
82
|
+
test('loop pauses before action and resumes on resume()', async () => {
|
|
83
|
+
const loop = new PauseableLoop();
|
|
84
|
+
const executed = [];
|
|
85
|
+
// Pause immediately so first action blocks
|
|
86
|
+
loop.pause();
|
|
87
|
+
const loopPromise = loop.runLoop([['a', 'b']], (label) => {
|
|
88
|
+
executed.push(label);
|
|
89
|
+
// After executing both actions, stop
|
|
90
|
+
if (executed.length === 2)
|
|
91
|
+
loop.queueDone();
|
|
92
|
+
});
|
|
93
|
+
// Give microtasks a chance to settle — loop should be blocked
|
|
94
|
+
await new Promise(r => setTimeout(r, 20));
|
|
95
|
+
expect(executed).toEqual([]);
|
|
96
|
+
// Resume — loop should execute actions then stop
|
|
97
|
+
loop.resume();
|
|
98
|
+
await loopPromise;
|
|
99
|
+
expect(executed).toEqual(['a', 'b']);
|
|
100
|
+
});
|
|
101
|
+
test('pause mid-batch stops before next action', async () => {
|
|
102
|
+
const loop = new PauseableLoop();
|
|
103
|
+
const executed = [];
|
|
104
|
+
const loopPromise = loop.runLoop([['a', 'b', 'c']], (label) => {
|
|
105
|
+
executed.push(label);
|
|
106
|
+
if (label === 'a')
|
|
107
|
+
loop.pause(); // pause after first action
|
|
108
|
+
});
|
|
109
|
+
// Wait for loop to pause after 'a'
|
|
110
|
+
await new Promise(r => setTimeout(r, 20));
|
|
111
|
+
expect(executed).toEqual(['a']);
|
|
112
|
+
// Resume and let it finish
|
|
113
|
+
loop.resume();
|
|
114
|
+
await new Promise(r => setTimeout(r, 20));
|
|
115
|
+
// 'b' should now execute, pause again since flag was cleared by resume
|
|
116
|
+
// Actually after resume, _paused is false, so b and c run
|
|
117
|
+
expect(executed).toContain('b');
|
|
118
|
+
expect(executed).toContain('c');
|
|
119
|
+
// Stop the loop
|
|
120
|
+
loop.stop();
|
|
121
|
+
await loopPromise;
|
|
122
|
+
});
|
|
123
|
+
test('pause emits pause event, resume emits resume event', async () => {
|
|
124
|
+
const loop = new PauseableLoop();
|
|
125
|
+
const events = [];
|
|
126
|
+
loop.events.on('pause', () => events.push('pause'));
|
|
127
|
+
loop.events.on('resume', () => events.push('resume'));
|
|
128
|
+
loop.pause();
|
|
129
|
+
const loopPromise = loop.runLoop([['a']], () => loop.queueDone());
|
|
130
|
+
// Wait for loop to hit _waitIfPaused and emit 'pause'
|
|
131
|
+
await new Promise(r => setTimeout(r, 20));
|
|
132
|
+
expect(events).toEqual(['pause']);
|
|
133
|
+
loop.resume();
|
|
134
|
+
await loopPromise;
|
|
135
|
+
expect(events).toEqual(['pause', 'resume']);
|
|
136
|
+
});
|
|
137
|
+
test('stop() while paused unblocks the loop', async () => {
|
|
138
|
+
const loop = new PauseableLoop();
|
|
139
|
+
const executed = [];
|
|
140
|
+
loop.pause();
|
|
141
|
+
const loopPromise = loop.runLoop([['a', 'b']], (label) => executed.push(label));
|
|
142
|
+
await new Promise(r => setTimeout(r, 20));
|
|
143
|
+
expect(executed).toEqual([]);
|
|
144
|
+
// stop() should set doneActing and resume so loop exits
|
|
145
|
+
loop.stop();
|
|
146
|
+
await loopPromise;
|
|
147
|
+
// No actions should have executed — loop exits immediately after unblocking
|
|
148
|
+
expect(executed).toEqual([]);
|
|
149
|
+
});
|
|
150
|
+
test('pause between batches blocks next iteration', async () => {
|
|
151
|
+
const loop = new PauseableLoop();
|
|
152
|
+
const executed = [];
|
|
153
|
+
let batchCount = 0;
|
|
154
|
+
const loopPromise = loop.runLoop([['a'], ['b']], (label) => {
|
|
155
|
+
executed.push(label);
|
|
156
|
+
batchCount++;
|
|
157
|
+
// After first batch completes, pause before second batch
|
|
158
|
+
if (batchCount === 1)
|
|
159
|
+
loop.pause();
|
|
160
|
+
if (batchCount === 2)
|
|
161
|
+
loop.queueDone();
|
|
162
|
+
});
|
|
163
|
+
// Let first batch run, then loop should pause at between-batch check
|
|
164
|
+
await new Promise(r => setTimeout(r, 20));
|
|
165
|
+
expect(executed).toEqual(['a']);
|
|
166
|
+
loop.resume();
|
|
167
|
+
await loopPromise;
|
|
168
|
+
expect(executed).toEqual(['a', 'b']);
|
|
169
|
+
});
|
|
170
|
+
test('multiple pause/resume cycles work correctly', async () => {
|
|
171
|
+
const loop = new PauseableLoop();
|
|
172
|
+
const executed = [];
|
|
173
|
+
let actionCount = 0;
|
|
174
|
+
const loopPromise = loop.runLoop([['x']], (label) => {
|
|
175
|
+
actionCount++;
|
|
176
|
+
executed.push(`${label}${actionCount}`);
|
|
177
|
+
if (actionCount < 3) {
|
|
178
|
+
loop.pause(); // pause after each action
|
|
179
|
+
}
|
|
180
|
+
else {
|
|
181
|
+
loop.queueDone(); // done after 3rd
|
|
182
|
+
}
|
|
183
|
+
});
|
|
184
|
+
// First action runs, then pauses
|
|
185
|
+
await new Promise(r => setTimeout(r, 20));
|
|
186
|
+
expect(executed).toEqual(['x1']);
|
|
187
|
+
loop.resume();
|
|
188
|
+
await new Promise(r => setTimeout(r, 20));
|
|
189
|
+
expect(executed).toEqual(['x1', 'x2']);
|
|
190
|
+
loop.resume();
|
|
191
|
+
await loopPromise;
|
|
192
|
+
expect(executed).toEqual(['x1', 'x2', 'x3']);
|
|
193
|
+
});
|
|
194
|
+
});
|