@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.
Files changed (142) hide show
  1. package/dist/actions/desktopActions.d.ts +58 -0
  2. package/dist/actions/desktopActions.js +154 -0
  3. package/dist/actions/index.d.ts +26 -0
  4. package/dist/actions/index.js +11 -0
  5. package/dist/actions/taskActions.d.ts +8 -0
  6. package/dist/actions/taskActions.js +27 -0
  7. package/dist/actions/types.d.ts +31 -0
  8. package/dist/actions/types.js +5 -0
  9. package/dist/actions/util.d.ts +20 -0
  10. package/dist/actions/util.js +297 -0
  11. package/dist/actions/webActions.d.ts +79 -0
  12. package/dist/actions/webActions.js +190 -0
  13. package/dist/agent/browserAgent.d.ts +29 -0
  14. package/dist/agent/browserAgent.js +122 -0
  15. package/dist/agent/errors.d.ts +8 -0
  16. package/dist/agent/errors.js +68 -0
  17. package/dist/agent/index.d.ts +53 -0
  18. package/dist/agent/index.js +377 -0
  19. package/dist/agent/narrator.d.ts +4 -0
  20. package/dist/agent/narrator.js +63 -0
  21. package/dist/agent/pause.test.d.ts +1 -0
  22. package/dist/agent/pause.test.js +194 -0
  23. package/dist/ai/baml_client/async_client.d.ts +73 -0
  24. package/dist/ai/baml_client/async_client.js +152 -0
  25. package/dist/ai/baml_client/async_request.d.ts +35 -0
  26. package/dist/ai/baml_client/async_request.js +98 -0
  27. package/dist/ai/baml_client/config.d.ts +11 -0
  28. package/dist/ai/baml_client/config.js +20 -0
  29. package/dist/ai/baml_client/globals.d.ts +16 -0
  30. package/dist/ai/baml_client/globals.js +37 -0
  31. package/dist/ai/baml_client/index.d.ts +16 -0
  32. package/dist/ai/baml_client/index.js +41 -0
  33. package/dist/ai/baml_client/inlinedbaml.d.ts +19 -0
  34. package/dist/ai/baml_client/inlinedbaml.js +31 -0
  35. package/dist/ai/baml_client/parser.d.ts +53 -0
  36. package/dist/ai/baml_client/parser.js +86 -0
  37. package/dist/ai/baml_client/partial_types.d.ts +61 -0
  38. package/dist/ai/baml_client/partial_types.js +10 -0
  39. package/dist/ai/baml_client/sync_client.d.ts +50 -0
  40. package/dist/ai/baml_client/sync_client.js +100 -0
  41. package/dist/ai/baml_client/sync_request.d.ts +35 -0
  42. package/dist/ai/baml_client/sync_request.js +98 -0
  43. package/dist/ai/baml_client/tracing.d.ts +16 -0
  44. package/dist/ai/baml_client/tracing.js +18 -0
  45. package/dist/ai/baml_client/type_builder.d.ts +38 -0
  46. package/dist/ai/baml_client/type_builder.js +96 -0
  47. package/dist/ai/baml_client/types.d.ts +69 -0
  48. package/dist/ai/baml_client/types.js +15 -0
  49. package/dist/ai/claudeCode.d.ts +1 -0
  50. package/dist/ai/claudeCode.js +186 -0
  51. package/dist/ai/modelHarness.d.ts +39 -0
  52. package/dist/ai/modelHarness.js +189 -0
  53. package/dist/ai/multiModelHarness.d.ts +27 -0
  54. package/dist/ai/multiModelHarness.js +52 -0
  55. package/dist/ai/types.d.ts +90 -0
  56. package/dist/ai/types.js +5 -0
  57. package/dist/ai/util.d.ts +13 -0
  58. package/dist/ai/util.js +165 -0
  59. package/dist/cli.d.ts +0 -0
  60. package/dist/cli.js +3 -0
  61. package/dist/common/actions.d.ts +0 -0
  62. package/dist/common/actions.js +14 -0
  63. package/dist/common/events.d.ts +15 -0
  64. package/dist/common/events.js +3 -0
  65. package/dist/common/failure.d.ts +68 -0
  66. package/dist/common/failure.js +1 -0
  67. package/dist/common/index.d.ts +3 -0
  68. package/dist/common/index.js +3 -0
  69. package/dist/common/retry.d.ts +16 -0
  70. package/dist/common/retry.js +30 -0
  71. package/dist/common/util.d.ts +23 -0
  72. package/dist/common/util.js +132 -0
  73. package/dist/connectors/browserConnector.d.ts +40 -0
  74. package/dist/connectors/browserConnector.js +127 -0
  75. package/dist/connectors/desktopConnector.d.ts +58 -0
  76. package/dist/connectors/desktopConnector.js +68 -0
  77. package/dist/connectors/index.d.ts +10 -0
  78. package/dist/connectors/index.js +2 -0
  79. package/dist/index.cjs +6143 -0
  80. package/dist/index.d.cts +825 -0
  81. package/dist/index.d.ts +20 -0
  82. package/dist/index.js +26 -0
  83. package/dist/index.mjs +6097 -0
  84. package/dist/logger.d.ts +3 -0
  85. package/dist/logger.js +15 -0
  86. package/dist/memory/agentMemory.d.ts +37 -0
  87. package/dist/memory/agentMemory.js +121 -0
  88. package/dist/memory/image.d.ts +26 -0
  89. package/dist/memory/image.js +106 -0
  90. package/dist/memory/index.d.ts +3 -0
  91. package/dist/memory/index.js +3 -0
  92. package/dist/memory/masking.d.ts +7 -0
  93. package/dist/memory/masking.js +141 -0
  94. package/dist/memory/masking.test.d.ts +1 -0
  95. package/dist/memory/masking.test.js +117 -0
  96. package/dist/memory/observation.d.ts +38 -0
  97. package/dist/memory/observation.js +79 -0
  98. package/dist/memory/rendering/index.d.ts +10 -0
  99. package/dist/memory/rendering/index.js +13 -0
  100. package/dist/memory/rendering/renderJsonParts.d.ts +3 -0
  101. package/dist/memory/rendering/renderJsonParts.js +115 -0
  102. package/dist/memory/rendering/renderJsonParts.test.d.ts +1 -0
  103. package/dist/memory/rendering/renderJsonParts.test.js +217 -0
  104. package/dist/memory/rendering/renderXmlParts.d.ts +7 -0
  105. package/dist/memory/rendering/renderXmlParts.js +127 -0
  106. package/dist/memory/serde.d.ts +20 -0
  107. package/dist/memory/serde.js +81 -0
  108. package/dist/memory/util.d.ts +2 -0
  109. package/dist/memory/util.js +17 -0
  110. package/dist/telemetry/events.d.ts +9 -0
  111. package/dist/telemetry/events.js +80 -0
  112. package/dist/telemetry/index.d.ts +6 -0
  113. package/dist/telemetry/index.js +91 -0
  114. package/dist/types.d.ts +30 -0
  115. package/dist/types.js +1 -0
  116. package/dist/util.d.ts +1 -0
  117. package/dist/util.js +12 -0
  118. package/dist/version.d.ts +1 -0
  119. package/dist/version.js +3 -0
  120. package/dist/web/browserProvider.d.ts +23 -0
  121. package/dist/web/browserProvider.js +153 -0
  122. package/dist/web/harness.d.ts +102 -0
  123. package/dist/web/harness.js +340 -0
  124. package/dist/web/stability.d.ts +57 -0
  125. package/dist/web/stability.js +316 -0
  126. package/dist/web/tabs.d.ts +58 -0
  127. package/dist/web/tabs.js +293 -0
  128. package/dist/web/transformer.d.ts +7 -0
  129. package/dist/web/transformer.js +41 -0
  130. package/dist/web/types.d.ts +32 -0
  131. package/dist/web/types.js +2 -0
  132. package/dist/web/util.d.ts +3 -0
  133. package/dist/web/util.js +81 -0
  134. package/dist/web/visualizer/cursor.d.ts +16 -0
  135. package/dist/web/visualizer/cursor.js +152 -0
  136. package/dist/web/visualizer/index.d.ts +22 -0
  137. package/dist/web/visualizer/index.js +47 -0
  138. package/dist/web/visualizer/mouseEffects.d.ts +18 -0
  139. package/dist/web/visualizer/mouseEffects.js +287 -0
  140. package/dist/web/visualizer/typeEffects.d.ts +10 -0
  141. package/dist/web/visualizer/typeEffects.js +190 -0
  142. 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,4 @@
1
+ import { Agent } from '@/agent';
2
+ import { BrowserAgent } from './browserAgent';
3
+ export declare function narrateAgent(agent: Agent): void;
4
+ export declare function narrateBrowserAgent(agent: BrowserAgent): void;
@@ -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
+ });