@ddwang/magnitude-core 0.3.1-ddwang.17 → 0.3.1-ddwang.18

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.
@@ -26,3 +26,7 @@ export declare class PlannerTimeoutError extends AgentError {
26
26
  readonly timeoutMs: number;
27
27
  constructor(timeoutMs: number);
28
28
  }
29
+ export declare class ExtractTimeoutError extends AgentError {
30
+ readonly timeoutMs: number;
31
+ constructor(timeoutMs: number);
32
+ }
@@ -63,6 +63,14 @@ export class PlannerTimeoutError extends AgentError {
63
63
  this.name = 'PlannerTimeoutError';
64
64
  }
65
65
  }
66
+ export class ExtractTimeoutError extends AgentError {
67
+ timeoutMs;
68
+ constructor(timeoutMs) {
69
+ super(`Extract call exceeded its ${timeoutMs} ms request budget`, { variant: 'extract_timeout' });
70
+ this.timeoutMs = timeoutMs;
71
+ this.name = 'ExtractTimeoutError';
72
+ }
73
+ }
66
74
  // // Agent will only throw these types of errors
67
75
  // import { ActionIngredient } from "./recipe/types";
68
76
  // import { WebAction } from "./web/types";
@@ -4,7 +4,7 @@ import { Action } from "@/actions/types";
4
4
  import { AgentEvents } from "@/common/events";
5
5
  import { AgentConnector } from '@/connectors';
6
6
  import { RenderableContent } from '@/memory/observation';
7
- import { LLMClient, type PlannerOptions } from "@/ai/types";
7
+ import { LLMClient, type TimeoutPolicy } from "@/ai/types";
8
8
  import { type OperationDiagnostics, type OperationKind, type OperationOptions } from '@/common/operation';
9
9
  import { AgentMemory, MemoryRenderOptions } from "@/memory";
10
10
  import { ActionDefinition } from "@/actions";
@@ -17,7 +17,9 @@ export interface AgentOptions {
17
17
  telemetry?: boolean;
18
18
  maxActions?: number;
19
19
  /** Omitted preserves existing planner retry behavior without a per-call timeout. */
20
- planner?: PlannerOptions;
20
+ planner?: TimeoutPolicy;
21
+ /** Omitted runs extract() without a per-call timeout. */
22
+ extract?: TimeoutPolicy;
21
23
  }
22
24
  export interface ActOptions extends OperationOptions {
23
25
  prompt?: string;
@@ -98,7 +98,7 @@ export class Agent {
98
98
  }
99
99
  }
100
100
  //this.model = new ModelHarness({ llm: this.options.llm });
101
- this.models = new MultiModelHarness(llms, this.options.planner);
101
+ this.models = new MultiModelHarness(llms, { planner: this.options.planner, extract: this.options.extract });
102
102
  this.models.events.on('tokensUsed', (usage) => this.events.emit('tokensUsed', usage), this);
103
103
  this.doneActing = false;
104
104
  this._paused = false;
@@ -1,6 +1,6 @@
1
1
  import { AgentContext } from "@/ai/baml_client";
2
2
  import { ClientRegistry } from "@boundaryml/baml";
3
- import { LLMClient, ModelUsage, type PlannerOptions } from "@/ai/types";
3
+ import { LLMClient, ModelUsage, type TimeoutPolicy } from "@/ai/types";
4
4
  import { ActionDefinition } from "@/actions";
5
5
  import { Schema, z } from 'zod';
6
6
  import { Image } from '@/memory/image';
@@ -9,7 +9,8 @@ import { MultiMediaContentPart } from "@/memory/rendering";
9
9
  import { type PlannerResponse } from './plannerResponse';
10
10
  interface ModelHarnessOptions {
11
11
  llm: LLMClient;
12
- planner?: PlannerOptions;
12
+ planner?: TimeoutPolicy;
13
+ extract?: TimeoutPolicy;
13
14
  }
14
15
  export interface ModelHarnessEvents {
15
16
  'tokensUsed': (usage: ModelUsage) => {};
@@ -33,6 +34,12 @@ export declare class ModelHarness {
33
34
  private _withUsage;
34
35
  private _reportCallUsage;
35
36
  partialAct<T>(context: AgentContext, task: string, data: MultiMediaContentPart[], actionVocabulary: ActionDefinition<T>[]): Promise<PlannerResponse>;
37
+ /**
38
+ * Runs one model call under an optional timeout policy: abort at timeoutMs, wait for the request to
39
+ * settle, then retry up to maxRetries times. Caller cancellation and deadlines take precedence.
40
+ * Planner calls are also recorded in the operation's plannerCalls.
41
+ */
42
+ private withTimeout;
36
43
  private partialActAttempt;
37
44
  extract<T extends Schema>(instructions: string, schema: T, screenshot: Image, domContent: string): Promise<z.infer<T>>;
38
45
  query<T extends Schema>(context: AgentContext, query: string, schema: T): Promise<z.infer<T>>;
@@ -12,7 +12,7 @@ import { anthropicOutputFormat, BASETEN_ENFORCED, plannerSchema, usesStructuredO
12
12
  import { ModelResponseError } from './modelResponseError';
13
13
  import { DEFAULT_BASETEN_MODEL } from './baseten';
14
14
  import { beginOperationPhase, checkOperation, currentOperation, operationOptions } from '@/common/operation';
15
- import { OperationDeadlineError, PlannerTimeoutError } from '@/agent/errors';
15
+ import { ExtractTimeoutError, OperationDeadlineError, PlannerTimeoutError } from '@/agent/errors';
16
16
  export class ModelHarness {
17
17
  /**
18
18
  * Strong reasoning agent for high level strategy and planning.
@@ -25,17 +25,21 @@ export class ModelHarness {
25
25
  logger;
26
26
  planner;
27
27
  constructor(options) {
28
- if (options.planner) {
29
- if (!Number.isInteger(options.planner.timeoutMs) || options.planner.timeoutMs <= 0 || options.planner.timeoutMs > 2_147_483_647) {
30
- throw new TypeError('planner.timeoutMs must be an integer between 1 and 2147483647');
28
+ for (const name of ['planner', 'extract']) {
29
+ const policy = options[name];
30
+ if (!policy)
31
+ continue;
32
+ if (!Number.isInteger(policy.timeoutMs) || policy.timeoutMs <= 0 || policy.timeoutMs > 2_147_483_647) {
33
+ throw new TypeError(`${name}.timeoutMs must be an integer between 1 and 2147483647`);
31
34
  }
32
- if (!Number.isSafeInteger(options.planner.maxRetries ?? 1) || (options.planner.maxRetries ?? 1) < 0) {
33
- throw new TypeError('planner.maxRetries must be a non-negative safe integer');
35
+ if (!Number.isSafeInteger(policy.maxRetries ?? 1) || (policy.maxRetries ?? 1) < 0) {
36
+ throw new TypeError(`${name}.maxRetries must be a non-negative safe integer`);
34
37
  }
35
38
  }
36
39
  this.options = {
37
40
  llm: options.llm,
38
41
  planner: options.planner && { ...options.planner },
42
+ extract: options.extract && { ...options.extract },
39
43
  };
40
44
  this.logger = logger.child({ name: 'llm' });
41
45
  }
@@ -279,11 +283,19 @@ export class ModelHarness {
279
283
  return usage;
280
284
  }
281
285
  async partialAct(context, task, data, actionVocabulary) {
282
- const policy = this.options.planner;
286
+ return this.withTimeout(this.options.planner, timeoutMs => new PlannerTimeoutError(timeoutMs), (checkRequest, callId) => this.partialActAttempt(context, task, data, actionVocabulary, checkRequest, callId), true);
287
+ }
288
+ /**
289
+ * Runs one model call under an optional timeout policy: abort at timeoutMs, wait for the request to
290
+ * settle, then retry up to maxRetries times. Caller cancellation and deadlines take precedence.
291
+ * Planner calls are also recorded in the operation's plannerCalls.
292
+ */
293
+ async withTimeout(policy, timeoutError, call, plannerCall = false) {
283
294
  for (let attempt = 1;; attempt++) {
284
295
  checkOperation();
285
296
  const operation = currentOperation();
286
297
  const controller = new AbortController();
298
+ const timeout = policy && timeoutError(policy.timeoutMs);
287
299
  const abortFromParent = () => controller.abort(operation.signal.reason);
288
300
  const started = performance.now();
289
301
  const checkRequest = () => {
@@ -291,17 +303,17 @@ export class ModelHarness {
291
303
  // Synchronous validation can occupy the event loop past the
292
304
  // timer's due time, on either success or failure.
293
305
  if (policy && performance.now() - started >= policy.timeoutMs)
294
- controller.abort(new PlannerTimeoutError(policy.timeoutMs));
306
+ controller.abort(timeout);
295
307
  controller.signal.throwIfAborted();
296
308
  return controller.signal;
297
309
  };
298
310
  let timer;
299
- const callId = operation?.startPlannerCall({
311
+ const callId = plannerCall ? operation?.startPlannerCall({
300
312
  attempt, provider: this.options.llm.provider,
301
313
  model: (this.options.llm.options.model ?? 'unknown').slice(0, 200),
302
314
  ...(policy ? { timeoutMs: policy.timeoutMs } : {}),
303
- });
304
- const abortOutcome = () => controller.signal.reason instanceof PlannerTimeoutError ? 'timeout'
315
+ }) : undefined;
316
+ const abortOutcome = () => controller.signal.reason === timeout ? 'timeout'
305
317
  : controller.signal.reason instanceof OperationDeadlineError ? 'deadline' : 'cancelled';
306
318
  controller.signal.addEventListener('abort', () => {
307
319
  if (callId)
@@ -311,28 +323,28 @@ export class ModelHarness {
311
323
  if (operation?.signal.aborted)
312
324
  abortFromParent();
313
325
  if (policy)
314
- timer = setTimeout(() => controller.abort(new PlannerTimeoutError(policy.timeoutMs)), policy.timeoutMs);
326
+ timer = setTimeout(() => controller.abort(timeout), policy.timeoutMs);
315
327
  let outcome = 'failed';
316
328
  try {
317
329
  checkRequest();
318
330
  // Await native settlement, not a Promise.race: a timeout must
319
331
  // never release ownership or overlap a still-running request.
320
- let plan;
332
+ let result;
321
333
  try {
322
- plan = await this.partialActAttempt(context, task, data, actionVocabulary, checkRequest, callId);
334
+ result = await call(checkRequest, callId);
323
335
  }
324
336
  finally {
325
337
  checkRequest();
326
338
  }
327
339
  outcome = 'succeeded';
328
- return plan;
340
+ return result;
329
341
  }
330
342
  catch (error) {
331
343
  checkOperation();
332
344
  if (!controller.signal.aborted)
333
345
  throw error;
334
346
  outcome = abortOutcome();
335
- if (!(controller.signal.reason instanceof PlannerTimeoutError) || attempt > (policy?.maxRetries ?? 1)) {
347
+ if (controller.signal.reason !== timeout || attempt > (policy?.maxRetries ?? 1)) {
336
348
  throw controller.signal.reason;
337
349
  }
338
350
  }
@@ -419,7 +431,7 @@ export class ModelHarness {
419
431
  // }
420
432
  const clientRegistry = this.clientForSchema(schema instanceof z.ZodObject ? schema : z.object({ data: schema }));
421
433
  const bamlScreenshot = await screenshot.toBaml();
422
- const resp = await this._withUsage(collector => this.baml.ExtractData(instructions, bamlScreenshot, domContent, this.options.llm.provider === 'claude-code', { tb, collector, clientRegistry, signal: operationOptions().signal }));
434
+ const resp = await this.withTimeout(this.options.extract, timeoutMs => new ExtractTimeoutError(timeoutMs), checkRequest => this._withUsage(collector => this.baml.ExtractData(instructions, bamlScreenshot, domContent, this.options.llm.provider === 'claude-code', { tb, collector, clientRegistry, signal: checkRequest() })));
423
435
  return schema.parse(schema instanceof z.ZodObject ? resp : resp.data);
424
436
  }
425
437
  // ^ extract could prob be a subset of query w trimmed mem
@@ -1,6 +1,6 @@
1
1
  import { MultiMediaContentPart } from "@/memory/rendering";
2
2
  import { ModelHarnessEvents } from "./modelHarness";
3
- import { LLMClient, type PlannerOptions } from "./types";
3
+ import { LLMClient, type TimeoutPolicy } from "./types";
4
4
  import { ActionDefinition } from "@/actions";
5
5
  import { AgentContext } from "./baml_client";
6
6
  import { Image } from '@/memory/image';
@@ -14,7 +14,10 @@ export declare class MultiModelHarness {
14
14
  private roles;
15
15
  private uniqueModels;
16
16
  readonly events: EventEmitter<ModelHarnessEvents>;
17
- constructor(clients: LLMClient[], planner?: PlannerOptions);
17
+ constructor(clients: LLMClient[], policies?: {
18
+ planner?: TimeoutPolicy;
19
+ extract?: TimeoutPolicy;
20
+ });
18
21
  setup(): Promise<void>;
19
22
  describe(): string;
20
23
  partialAct<T>(context: AgentContext, task: string, data: MultiMediaContentPart[], actionVocabulary: ActionDefinition<T>[]): Promise<PlannerResponse>;
@@ -10,11 +10,11 @@ export class MultiModelHarness {
10
10
  roles = {};
11
11
  uniqueModels = [];
12
12
  events = new EventEmitter();
13
- constructor(clients, planner) {
13
+ constructor(clients, policies = {}) {
14
14
  // Sort by specificity (from least specific to most specific)
15
15
  const sortedClients = [...clients].sort((a, b) => (b.roles ? b.roles.length : 9999) - (a.roles ? a.roles.length : 9999));
16
16
  for (const client of sortedClients) {
17
- const harness = new ModelHarness({ llm: client, planner });
17
+ const harness = new ModelHarness({ llm: client, ...policies });
18
18
  this.uniqueModels.push(harness);
19
19
  if (client.roles) {
20
20
  for (const role of client.roles) {
@@ -1,12 +1,14 @@
1
1
  export type BrowserAgentRole = 'act' | 'extract' | 'query';
2
2
  export declare const allBrowserAgentRoles: BrowserAgentRole[];
3
- /** Optional request budget for act() planning, not extraction or query. */
4
- export interface PlannerOptions {
5
- /** Includes transport retries and format correction within one planner invocation. */
3
+ /** Optional request budget for one model call, used for act() planning and for extract(). */
4
+ export interface TimeoutPolicy {
5
+ /** Includes transport retries and format correction within one invocation. */
6
6
  timeoutMs: number;
7
7
  /** Retries after timeout, once the previous invocation settles. Default: 1. */
8
8
  maxRetries?: number;
9
9
  }
10
+ /** The published name for the planner's policy, kept for existing imports. */
11
+ export type PlannerOptions = TimeoutPolicy;
10
12
  export type LLMClient = (AnthropicClient | ClaudeCodeClient | BedrockClient | GoogleAIClient | GoogleVertexClient | OpenAIClient | BasetenClient | OpenAIGenericClient | AzureOpenAIClient) & {
11
13
  roles?: BrowserAgentRole[];
12
14
  };
package/dist/index.cjs CHANGED
@@ -1157,6 +1157,13 @@ class PlannerTimeoutError extends AgentError {
1157
1157
  this.name = "PlannerTimeoutError";
1158
1158
  }
1159
1159
  }
1160
+ class ExtractTimeoutError extends AgentError {
1161
+ constructor(timeoutMs) {
1162
+ super(`Extract call exceeded its ${timeoutMs} ms request budget`, { variant: "extract_timeout" });
1163
+ this.timeoutMs = timeoutMs;
1164
+ this.name = "ExtractTimeoutError";
1165
+ }
1166
+ }
1160
1167
 
1161
1168
  function attachOperationDiagnostics(error, diagnostics) {
1162
1169
  if (!(error instanceof Error)) return;
@@ -3628,17 +3635,20 @@ class ModelHarness {
3628
3635
  logger;
3629
3636
  planner;
3630
3637
  constructor(options) {
3631
- if (options.planner) {
3632
- if (!Number.isInteger(options.planner.timeoutMs) || options.planner.timeoutMs <= 0 || options.planner.timeoutMs > 2147483647) {
3633
- throw new TypeError("planner.timeoutMs must be an integer between 1 and 2147483647");
3638
+ for (const name of ["planner", "extract"]) {
3639
+ const policy = options[name];
3640
+ if (!policy) continue;
3641
+ if (!Number.isInteger(policy.timeoutMs) || policy.timeoutMs <= 0 || policy.timeoutMs > 2147483647) {
3642
+ throw new TypeError(`${name}.timeoutMs must be an integer between 1 and 2147483647`);
3634
3643
  }
3635
- if (!Number.isSafeInteger(options.planner.maxRetries ?? 1) || (options.planner.maxRetries ?? 1) < 0) {
3636
- throw new TypeError("planner.maxRetries must be a non-negative safe integer");
3644
+ if (!Number.isSafeInteger(policy.maxRetries ?? 1) || (policy.maxRetries ?? 1) < 0) {
3645
+ throw new TypeError(`${name}.maxRetries must be a non-negative safe integer`);
3637
3646
  }
3638
3647
  }
3639
3648
  this.options = {
3640
3649
  llm: options.llm,
3641
- planner: options.planner && { ...options.planner }
3650
+ planner: options.planner && { ...options.planner },
3651
+ extract: options.extract && { ...options.extract }
3642
3652
  };
3643
3653
  this.logger = logger.child({ name: "llm" });
3644
3654
  }
@@ -3842,49 +3852,62 @@ class ModelHarness {
3842
3852
  return usage;
3843
3853
  }
3844
3854
  async partialAct(context, task, data, actionVocabulary) {
3845
- const policy = this.options.planner;
3855
+ return this.withTimeout(
3856
+ this.options.planner,
3857
+ (timeoutMs) => new PlannerTimeoutError(timeoutMs),
3858
+ (checkRequest, callId) => this.partialActAttempt(context, task, data, actionVocabulary, checkRequest, callId),
3859
+ true
3860
+ );
3861
+ }
3862
+ /**
3863
+ * Runs one model call under an optional timeout policy: abort at timeoutMs, wait for the request to
3864
+ * settle, then retry up to maxRetries times. Caller cancellation and deadlines take precedence.
3865
+ * Planner calls are also recorded in the operation's plannerCalls.
3866
+ */
3867
+ async withTimeout(policy, timeoutError, call, plannerCall = false) {
3846
3868
  for (let attempt = 1; ; attempt++) {
3847
3869
  checkOperation();
3848
3870
  const operation = currentOperation();
3849
3871
  const controller = new AbortController();
3872
+ const timeout = policy && timeoutError(policy.timeoutMs);
3850
3873
  const abortFromParent = () => controller.abort(operation.signal.reason);
3851
3874
  const started = performance.now();
3852
3875
  const checkRequest = () => {
3853
3876
  checkOperation();
3854
- if (policy && performance.now() - started >= policy.timeoutMs) controller.abort(new PlannerTimeoutError(policy.timeoutMs));
3877
+ if (policy && performance.now() - started >= policy.timeoutMs) controller.abort(timeout);
3855
3878
  controller.signal.throwIfAborted();
3856
3879
  return controller.signal;
3857
3880
  };
3858
3881
  let timer;
3859
- const callId = operation?.startPlannerCall({
3882
+ const callId = plannerCall ? operation?.startPlannerCall({
3860
3883
  attempt,
3861
3884
  provider: this.options.llm.provider,
3862
3885
  model: (this.options.llm.options.model ?? "unknown").slice(0, 200),
3863
3886
  ...policy ? { timeoutMs: policy.timeoutMs } : {}
3864
- });
3865
- const abortOutcome = () => controller.signal.reason instanceof PlannerTimeoutError ? "timeout" : controller.signal.reason instanceof OperationDeadlineError ? "deadline" : "cancelled";
3887
+ }) : void 0;
3888
+ const abortOutcome = () => controller.signal.reason === timeout ? "timeout" : controller.signal.reason instanceof OperationDeadlineError ? "deadline" : "cancelled";
3866
3889
  controller.signal.addEventListener("abort", () => {
3867
3890
  if (callId) operation.updatePlannerCall(callId, abortOutcome(), "draining");
3868
3891
  }, { once: true });
3869
3892
  operation?.signal.addEventListener("abort", abortFromParent, { once: true });
3870
3893
  if (operation?.signal.aborted) abortFromParent();
3871
- if (policy) timer = setTimeout(() => controller.abort(new PlannerTimeoutError(policy.timeoutMs)), policy.timeoutMs);
3894
+ if (policy) timer = setTimeout(() => controller.abort(timeout), policy.timeoutMs);
3872
3895
  let outcome = "failed";
3873
3896
  try {
3874
3897
  checkRequest();
3875
- let plan;
3898
+ let result;
3876
3899
  try {
3877
- plan = await this.partialActAttempt(context, task, data, actionVocabulary, checkRequest, callId);
3900
+ result = await call(checkRequest, callId);
3878
3901
  } finally {
3879
3902
  checkRequest();
3880
3903
  }
3881
3904
  outcome = "succeeded";
3882
- return plan;
3905
+ return result;
3883
3906
  } catch (error) {
3884
3907
  checkOperation();
3885
3908
  if (!controller.signal.aborted) throw error;
3886
3909
  outcome = abortOutcome();
3887
- if (!(controller.signal.reason instanceof PlannerTimeoutError) || attempt > (policy?.maxRetries ?? 1)) {
3910
+ if (controller.signal.reason !== timeout || attempt > (policy?.maxRetries ?? 1)) {
3888
3911
  throw controller.signal.reason;
3889
3912
  }
3890
3913
  } finally {
@@ -3957,13 +3980,17 @@ class ModelHarness {
3957
3980
  }
3958
3981
  const clientRegistry = this.clientForSchema(schema instanceof z.z.ZodObject ? schema : z.z.object({ data: schema }));
3959
3982
  const bamlScreenshot = await screenshot.toBaml();
3960
- const resp = await this._withUsage((collector) => this.baml.ExtractData(
3961
- instructions,
3962
- bamlScreenshot,
3963
- domContent,
3964
- this.options.llm.provider === "claude-code",
3965
- { tb, collector, clientRegistry, signal: operationOptions().signal }
3966
- ));
3983
+ const resp = await this.withTimeout(
3984
+ this.options.extract,
3985
+ (timeoutMs) => new ExtractTimeoutError(timeoutMs),
3986
+ (checkRequest) => this._withUsage((collector) => this.baml.ExtractData(
3987
+ instructions,
3988
+ bamlScreenshot,
3989
+ domContent,
3990
+ this.options.llm.provider === "claude-code",
3991
+ { tb, collector, clientRegistry, signal: checkRequest() }
3992
+ ))
3993
+ );
3967
3994
  return schema.parse(schema instanceof z.z.ZodObject ? resp : resp.data);
3968
3995
  }
3969
3996
  // ^ extract could prob be a subset of query w trimmed mem
@@ -4066,10 +4093,10 @@ class MultiModelHarness {
4066
4093
  roles = {};
4067
4094
  uniqueModels = [];
4068
4095
  events = new EventEmitter();
4069
- constructor(clients, planner) {
4096
+ constructor(clients, policies = {}) {
4070
4097
  const sortedClients = [...clients].sort((a, b) => (b.roles ? b.roles.length : 9999) - (a.roles ? a.roles.length : 9999));
4071
4098
  for (const client of sortedClients) {
4072
- const harness = new ModelHarness({ llm: client, planner });
4099
+ const harness = new ModelHarness({ llm: client, ...policies });
4073
4100
  this.uniqueModels.push(harness);
4074
4101
  if (client.roles) {
4075
4102
  for (const role of client.roles) {
@@ -4549,7 +4576,7 @@ class Agent {
4549
4576
  }
4550
4577
  }
4551
4578
  }
4552
- this.models = new MultiModelHarness(llms, this.options.planner);
4579
+ this.models = new MultiModelHarness(llms, { planner: this.options.planner, extract: this.options.extract });
4553
4580
  this.models.events.on("tokensUsed", (usage) => this.events.emit("tokensUsed", usage), this);
4554
4581
  this.doneActing = false;
4555
4582
  this._paused = false;
@@ -8292,6 +8319,7 @@ exports.BrowserBlockedError = BrowserBlockedError;
8292
8319
  exports.BrowserConnector = BrowserConnector;
8293
8320
  exports.BrowserProvider = BrowserProvider;
8294
8321
  exports.DesktopConnector = DesktopConnector;
8322
+ exports.ExtractTimeoutError = ExtractTimeoutError;
8295
8323
  exports.Observation = Observation;
8296
8324
  exports.OperationCancelledError = OperationCancelledError;
8297
8325
  exports.OperationDeadlineError = OperationDeadlineError;
package/dist/index.d.cts CHANGED
@@ -83,13 +83,15 @@ interface CheckIntent {
83
83
 
84
84
  type BrowserAgentRole = 'act' | 'extract' | 'query';
85
85
  declare const allBrowserAgentRoles: BrowserAgentRole[];
86
- /** Optional request budget for act() planning, not extraction or query. */
87
- interface PlannerOptions {
88
- /** Includes transport retries and format correction within one planner invocation. */
86
+ /** Optional request budget for one model call, used for act() planning and for extract(). */
87
+ interface TimeoutPolicy {
88
+ /** Includes transport retries and format correction within one invocation. */
89
89
  timeoutMs: number;
90
90
  /** Retries after timeout, once the previous invocation settles. Default: 1. */
91
91
  maxRetries?: number;
92
92
  }
93
+ /** The published name for the planner's policy, kept for existing imports. */
94
+ type PlannerOptions = TimeoutPolicy;
93
95
  type LLMClient = (AnthropicClient | ClaudeCodeClient | BedrockClient | GoogleAIClient | GoogleVertexClient | OpenAIClient | BasetenClient | OpenAIGenericClient | AzureOpenAIClient) & {
94
96
  roles?: BrowserAgentRole[];
95
97
  };
@@ -558,7 +560,10 @@ declare class MultiModelHarness {
558
560
  private roles;
559
561
  private uniqueModels;
560
562
  readonly events: EventEmitter<ModelHarnessEvents>;
561
- constructor(clients: LLMClient[], planner?: PlannerOptions);
563
+ constructor(clients: LLMClient[], policies?: {
564
+ planner?: TimeoutPolicy;
565
+ extract?: TimeoutPolicy;
566
+ });
562
567
  setup(): Promise<void>;
563
568
  describe(): string;
564
569
  partialAct<T>(context: AgentContext, task: string, data: MultiMediaContentPart[], actionVocabulary: ActionDefinition<T>[]): Promise<PlannerResponse>;
@@ -575,7 +580,9 @@ interface AgentOptions {
575
580
  telemetry?: boolean;
576
581
  maxActions?: number;
577
582
  /** Omitted preserves existing planner retry behavior without a per-call timeout. */
578
- planner?: PlannerOptions;
583
+ planner?: TimeoutPolicy;
584
+ /** Omitted runs extract() without a per-call timeout. */
585
+ extract?: TimeoutPolicy;
579
586
  }
580
587
  interface ActOptions extends OperationOptions {
581
588
  prompt?: string;
@@ -1012,6 +1019,10 @@ declare class PlannerTimeoutError extends AgentError {
1012
1019
  readonly timeoutMs: number;
1013
1020
  constructor(timeoutMs: number);
1014
1021
  }
1022
+ declare class ExtractTimeoutError extends AgentError {
1023
+ readonly timeoutMs: number;
1024
+ constructor(timeoutMs: number);
1025
+ }
1015
1026
 
1016
1027
  /**
1017
1028
  * Represents any reason why a test case could have failed, for example:
@@ -1151,5 +1162,5 @@ declare function buildDefaultBrowserAgentOptions({ agentOptions, browserOptions
1151
1162
 
1152
1163
  declare const logger: pino.Logger<never, boolean>;
1153
1164
 
1154
- export { ActionLimitError, Agent, AgentBusyError, AgentError, AgentMemory, BrowserAgent, BrowserBlockedError, BrowserConnector, BrowserProvider, DesktopConnector, Observation, OperationCancelledError, OperationDeadlineError, PlannerTimeoutError, WebHarness, allBrowserAgentRoles, buildDefaultBrowserAgentOptions, createAction, createId, deepEquals, getCodebaseId, getMachineId, logger, mergeMessages, posthog, retryOnError, retryOnErrorIsSuccess, sendTelemetry, startBrowserAgent };
1155
- export type { ActOptions, Action, ActionContext, ActionDefinition, ActionIntent, ActionPayload, AgentConnector, AgentErrorOptions, AgentEvents, AgentMemoryOptions, AgentOptions, AnthropicClient, ApiKeyFailure, AzureOpenAIClient, Base64Image, BasetenClient, BedrockClient, BlockReason, BrowserAgentRole, BrowserBlock, BrowserClickDiagnostics, BrowserConnectorOptions, BrowserConnectorStateData, BrowserFailure, BrowserOptions, BugDetectedFailure, BugSeverity, CancelledFailure, CheckIntent, ClaudeCodeClient, ClickIntent, ClickWebAction, DesktopConnectorOptions, DesktopInterface, FailedTestCaseResult, FailureDescriptor, GoogleAIClient, GoogleVertexClient, HoverIntent, HoverWebAction, HttpDiagnostic, Intent, LLMClient, LLMClientIdentifier, MemoryRenderOptions, MisalignmentFailure, ModelUsage, NavigateWebAction, NetworkFailure, ObservableDataArray, ObservableDataObject, ObservableDataPrimitive, ObservationRetentionOptions, ObservationRole, ObservationSource, OpenAIClient, OpenAIGenericClient, OperationDiagnostics, OperationKind, OperationOptions, OperationPhase, OperationTiming, PixelCoordinate, PlannerCallDiagnostics, PlannerOptions, ProviderAttemptDiagnostics, RateLimitFailure, RecoveryOptions, RenderableContent, RetryMode, RetryOptions, RetryParams, ScrollIntent, ScrollWebAction, SerializedAgentMemory, SuccessfulTestCaseResult, SwitchTabIntent, SwitchTabWebAction, TestCaseDefinition, TestCaseResult, TestData, TestDataEntry, TestStepDefinition, TypeIntent, TypeWebAction, UnknownFailure, WebAction, WebHarnessEvents, WebHarnessOptions };
1165
+ export { ActionLimitError, Agent, AgentBusyError, AgentError, AgentMemory, BrowserAgent, BrowserBlockedError, BrowserConnector, BrowserProvider, DesktopConnector, ExtractTimeoutError, Observation, OperationCancelledError, OperationDeadlineError, PlannerTimeoutError, WebHarness, allBrowserAgentRoles, buildDefaultBrowserAgentOptions, createAction, createId, deepEquals, getCodebaseId, getMachineId, logger, mergeMessages, posthog, retryOnError, retryOnErrorIsSuccess, sendTelemetry, startBrowserAgent };
1166
+ export type { ActOptions, Action, ActionContext, ActionDefinition, ActionIntent, ActionPayload, AgentConnector, AgentErrorOptions, AgentEvents, AgentMemoryOptions, AgentOptions, AnthropicClient, ApiKeyFailure, AzureOpenAIClient, Base64Image, BasetenClient, BedrockClient, BlockReason, BrowserAgentRole, BrowserBlock, BrowserClickDiagnostics, BrowserConnectorOptions, BrowserConnectorStateData, BrowserFailure, BrowserOptions, BugDetectedFailure, BugSeverity, CancelledFailure, CheckIntent, ClaudeCodeClient, ClickIntent, ClickWebAction, DesktopConnectorOptions, DesktopInterface, FailedTestCaseResult, FailureDescriptor, GoogleAIClient, GoogleVertexClient, HoverIntent, HoverWebAction, HttpDiagnostic, Intent, LLMClient, LLMClientIdentifier, MemoryRenderOptions, MisalignmentFailure, ModelUsage, NavigateWebAction, NetworkFailure, ObservableDataArray, ObservableDataObject, ObservableDataPrimitive, ObservationRetentionOptions, ObservationRole, ObservationSource, OpenAIClient, OpenAIGenericClient, OperationDiagnostics, OperationKind, OperationOptions, OperationPhase, OperationTiming, PixelCoordinate, PlannerCallDiagnostics, PlannerOptions, ProviderAttemptDiagnostics, RateLimitFailure, RecoveryOptions, RenderableContent, RetryMode, RetryOptions, RetryParams, ScrollIntent, ScrollWebAction, SerializedAgentMemory, SuccessfulTestCaseResult, SwitchTabIntent, SwitchTabWebAction, TestCaseDefinition, TestCaseResult, TestData, TestDataEntry, TestStepDefinition, TimeoutPolicy, TypeIntent, TypeWebAction, UnknownFailure, WebAction, WebHarnessEvents, WebHarnessOptions };
package/dist/index.mjs CHANGED
@@ -1134,6 +1134,13 @@ class PlannerTimeoutError extends AgentError {
1134
1134
  this.name = "PlannerTimeoutError";
1135
1135
  }
1136
1136
  }
1137
+ class ExtractTimeoutError extends AgentError {
1138
+ constructor(timeoutMs) {
1139
+ super(`Extract call exceeded its ${timeoutMs} ms request budget`, { variant: "extract_timeout" });
1140
+ this.timeoutMs = timeoutMs;
1141
+ this.name = "ExtractTimeoutError";
1142
+ }
1143
+ }
1137
1144
 
1138
1145
  function attachOperationDiagnostics(error, diagnostics) {
1139
1146
  if (!(error instanceof Error)) return;
@@ -3605,17 +3612,20 @@ class ModelHarness {
3605
3612
  logger;
3606
3613
  planner;
3607
3614
  constructor(options) {
3608
- if (options.planner) {
3609
- if (!Number.isInteger(options.planner.timeoutMs) || options.planner.timeoutMs <= 0 || options.planner.timeoutMs > 2147483647) {
3610
- throw new TypeError("planner.timeoutMs must be an integer between 1 and 2147483647");
3615
+ for (const name of ["planner", "extract"]) {
3616
+ const policy = options[name];
3617
+ if (!policy) continue;
3618
+ if (!Number.isInteger(policy.timeoutMs) || policy.timeoutMs <= 0 || policy.timeoutMs > 2147483647) {
3619
+ throw new TypeError(`${name}.timeoutMs must be an integer between 1 and 2147483647`);
3611
3620
  }
3612
- if (!Number.isSafeInteger(options.planner.maxRetries ?? 1) || (options.planner.maxRetries ?? 1) < 0) {
3613
- throw new TypeError("planner.maxRetries must be a non-negative safe integer");
3621
+ if (!Number.isSafeInteger(policy.maxRetries ?? 1) || (policy.maxRetries ?? 1) < 0) {
3622
+ throw new TypeError(`${name}.maxRetries must be a non-negative safe integer`);
3614
3623
  }
3615
3624
  }
3616
3625
  this.options = {
3617
3626
  llm: options.llm,
3618
- planner: options.planner && { ...options.planner }
3627
+ planner: options.planner && { ...options.planner },
3628
+ extract: options.extract && { ...options.extract }
3619
3629
  };
3620
3630
  this.logger = logger.child({ name: "llm" });
3621
3631
  }
@@ -3819,49 +3829,62 @@ class ModelHarness {
3819
3829
  return usage;
3820
3830
  }
3821
3831
  async partialAct(context, task, data, actionVocabulary) {
3822
- const policy = this.options.planner;
3832
+ return this.withTimeout(
3833
+ this.options.planner,
3834
+ (timeoutMs) => new PlannerTimeoutError(timeoutMs),
3835
+ (checkRequest, callId) => this.partialActAttempt(context, task, data, actionVocabulary, checkRequest, callId),
3836
+ true
3837
+ );
3838
+ }
3839
+ /**
3840
+ * Runs one model call under an optional timeout policy: abort at timeoutMs, wait for the request to
3841
+ * settle, then retry up to maxRetries times. Caller cancellation and deadlines take precedence.
3842
+ * Planner calls are also recorded in the operation's plannerCalls.
3843
+ */
3844
+ async withTimeout(policy, timeoutError, call, plannerCall = false) {
3823
3845
  for (let attempt = 1; ; attempt++) {
3824
3846
  checkOperation();
3825
3847
  const operation = currentOperation();
3826
3848
  const controller = new AbortController();
3849
+ const timeout = policy && timeoutError(policy.timeoutMs);
3827
3850
  const abortFromParent = () => controller.abort(operation.signal.reason);
3828
3851
  const started = performance.now();
3829
3852
  const checkRequest = () => {
3830
3853
  checkOperation();
3831
- if (policy && performance.now() - started >= policy.timeoutMs) controller.abort(new PlannerTimeoutError(policy.timeoutMs));
3854
+ if (policy && performance.now() - started >= policy.timeoutMs) controller.abort(timeout);
3832
3855
  controller.signal.throwIfAborted();
3833
3856
  return controller.signal;
3834
3857
  };
3835
3858
  let timer;
3836
- const callId = operation?.startPlannerCall({
3859
+ const callId = plannerCall ? operation?.startPlannerCall({
3837
3860
  attempt,
3838
3861
  provider: this.options.llm.provider,
3839
3862
  model: (this.options.llm.options.model ?? "unknown").slice(0, 200),
3840
3863
  ...policy ? { timeoutMs: policy.timeoutMs } : {}
3841
- });
3842
- const abortOutcome = () => controller.signal.reason instanceof PlannerTimeoutError ? "timeout" : controller.signal.reason instanceof OperationDeadlineError ? "deadline" : "cancelled";
3864
+ }) : void 0;
3865
+ const abortOutcome = () => controller.signal.reason === timeout ? "timeout" : controller.signal.reason instanceof OperationDeadlineError ? "deadline" : "cancelled";
3843
3866
  controller.signal.addEventListener("abort", () => {
3844
3867
  if (callId) operation.updatePlannerCall(callId, abortOutcome(), "draining");
3845
3868
  }, { once: true });
3846
3869
  operation?.signal.addEventListener("abort", abortFromParent, { once: true });
3847
3870
  if (operation?.signal.aborted) abortFromParent();
3848
- if (policy) timer = setTimeout(() => controller.abort(new PlannerTimeoutError(policy.timeoutMs)), policy.timeoutMs);
3871
+ if (policy) timer = setTimeout(() => controller.abort(timeout), policy.timeoutMs);
3849
3872
  let outcome = "failed";
3850
3873
  try {
3851
3874
  checkRequest();
3852
- let plan;
3875
+ let result;
3853
3876
  try {
3854
- plan = await this.partialActAttempt(context, task, data, actionVocabulary, checkRequest, callId);
3877
+ result = await call(checkRequest, callId);
3855
3878
  } finally {
3856
3879
  checkRequest();
3857
3880
  }
3858
3881
  outcome = "succeeded";
3859
- return plan;
3882
+ return result;
3860
3883
  } catch (error) {
3861
3884
  checkOperation();
3862
3885
  if (!controller.signal.aborted) throw error;
3863
3886
  outcome = abortOutcome();
3864
- if (!(controller.signal.reason instanceof PlannerTimeoutError) || attempt > (policy?.maxRetries ?? 1)) {
3887
+ if (controller.signal.reason !== timeout || attempt > (policy?.maxRetries ?? 1)) {
3865
3888
  throw controller.signal.reason;
3866
3889
  }
3867
3890
  } finally {
@@ -3934,13 +3957,17 @@ class ModelHarness {
3934
3957
  }
3935
3958
  const clientRegistry = this.clientForSchema(schema instanceof z.ZodObject ? schema : z.object({ data: schema }));
3936
3959
  const bamlScreenshot = await screenshot.toBaml();
3937
- const resp = await this._withUsage((collector) => this.baml.ExtractData(
3938
- instructions,
3939
- bamlScreenshot,
3940
- domContent,
3941
- this.options.llm.provider === "claude-code",
3942
- { tb, collector, clientRegistry, signal: operationOptions().signal }
3943
- ));
3960
+ const resp = await this.withTimeout(
3961
+ this.options.extract,
3962
+ (timeoutMs) => new ExtractTimeoutError(timeoutMs),
3963
+ (checkRequest) => this._withUsage((collector) => this.baml.ExtractData(
3964
+ instructions,
3965
+ bamlScreenshot,
3966
+ domContent,
3967
+ this.options.llm.provider === "claude-code",
3968
+ { tb, collector, clientRegistry, signal: checkRequest() }
3969
+ ))
3970
+ );
3944
3971
  return schema.parse(schema instanceof z.ZodObject ? resp : resp.data);
3945
3972
  }
3946
3973
  // ^ extract could prob be a subset of query w trimmed mem
@@ -4043,10 +4070,10 @@ class MultiModelHarness {
4043
4070
  roles = {};
4044
4071
  uniqueModels = [];
4045
4072
  events = new EventEmitter();
4046
- constructor(clients, planner) {
4073
+ constructor(clients, policies = {}) {
4047
4074
  const sortedClients = [...clients].sort((a, b) => (b.roles ? b.roles.length : 9999) - (a.roles ? a.roles.length : 9999));
4048
4075
  for (const client of sortedClients) {
4049
- const harness = new ModelHarness({ llm: client, planner });
4076
+ const harness = new ModelHarness({ llm: client, ...policies });
4050
4077
  this.uniqueModels.push(harness);
4051
4078
  if (client.roles) {
4052
4079
  for (const role of client.roles) {
@@ -4526,7 +4553,7 @@ class Agent {
4526
4553
  }
4527
4554
  }
4528
4555
  }
4529
- this.models = new MultiModelHarness(llms, this.options.planner);
4556
+ this.models = new MultiModelHarness(llms, { planner: this.options.planner, extract: this.options.extract });
4530
4557
  this.models.events.on("tokensUsed", (usage) => this.events.emit("tokensUsed", usage), this);
4531
4558
  this.doneActing = false;
4532
4559
  this._paused = false;
@@ -8255,4 +8282,4 @@ class DesktopConnector {
8255
8282
  process.env.BAML_LOG = "off";
8256
8283
  setLogLevel("OFF");
8257
8284
 
8258
- export { ActionLimitError, Agent, AgentBusyError, AgentError, AgentMemory, BrowserAgent, BrowserBlockedError, BrowserConnector, BrowserProvider, DesktopConnector, Observation, OperationCancelledError, OperationDeadlineError, PlannerTimeoutError, WebHarness, allBrowserAgentRoles, buildDefaultBrowserAgentOptions, createAction, createId, deepEquals, getCodebaseId, getMachineId, logger, mergeMessages, posthog, retryOnError, retryOnErrorIsSuccess, sendTelemetry, startBrowserAgent };
8285
+ export { ActionLimitError, Agent, AgentBusyError, AgentError, AgentMemory, BrowserAgent, BrowserBlockedError, BrowserConnector, BrowserProvider, DesktopConnector, ExtractTimeoutError, Observation, OperationCancelledError, OperationDeadlineError, PlannerTimeoutError, WebHarness, allBrowserAgentRoles, buildDefaultBrowserAgentOptions, createAction, createId, deepEquals, getCodebaseId, getMachineId, logger, mergeMessages, posthog, retryOnError, retryOnErrorIsSuccess, sendTelemetry, startBrowserAgent };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ddwang/magnitude-core",
3
- "version": "0.3.1-ddwang.17",
3
+ "version": "0.3.1-ddwang.18",
4
4
  "description": "Magnitude e2e testing agent",
5
5
  "publishConfig": {
6
6
  "access": "public"