@stackfactor/agent-utils 1.2.16 → 1.2.19

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.
@@ -6,7 +6,7 @@ import * as grpc from "@grpc/grpc-js";
6
6
  * zero or more `Progress` frames (driven by the agent's `onProgress`) followed
7
7
  * by exactly one `Result` or `Error`.
8
8
  */
9
- export declare const PROTO = "\nsyntax = \"proto3\";\npackage stackfactor.agent.v1;\n\nservice Agent {\n rpc Execute(ExecuteRequest) returns (stream Update);\n // Liveness/health probe served by serve() itself \u2014 no agent code required.\n // deep=true also exercises the StackFactor auth callback (BACKEND_URL).\n rpc HealthCheck(HealthCheckRequest) returns (HealthCheckResponse);\n}\n\nmessage ExecuteRequest {\n string content_type = 1;\n string data_json = 2;\n string config_json = 3;\n string request_json = 4;\n int32 event = 5;\n}\n\nmessage HealthCheckRequest {\n // Optional JSON carrying { authToken } for the deep backend-callback check.\n string request_json = 1;\n bool deep = 2;\n // Deep check only: repo-relative path to the agent's self-check module to run\n // (e.g. \"src/check.js\"). Empty \u2192 fall back to the conventional \"src/check.js\".\n string check_code = 3;\n // Deep check only: JSON of the resolved agent config (constantsAndVars/\n // secrets) made available to the self-check as ctx.config. May be empty.\n string config_json = 4;\n}\n\n// One result row from the agent's self-check module. The agent owns only its\n// own checks; reachability/ingress/version-drift rows are synthesised backend-\n// side. severity is optional: \"warn\" marks a failing check as non-blocking;\n// empty or \"error\" is blocking.\nmessage CheckResult {\n string name = 1;\n bool ok = 2;\n string detail = 3;\n string severity = 4;\n}\n\nmessage HealthCheckResponse {\n bool ok = 1;\n bool backend_reachable = 2;\n string version = 3;\n string message = 4;\n repeated CheckResult checks = 5;\n}\n\nmessage Update {\n oneof payload {\n Progress progress = 1;\n Result result = 2;\n ErrorInfo error = 3;\n }\n}\n\nmessage Progress { int32 progress = 1; string message = 2; }\nmessage Result { string result_json = 1; }\nmessage ErrorInfo { int32 code = 1; string message = 2; }\n";
9
+ export declare const PROTO = "\nsyntax = \"proto3\";\npackage stackfactor.agent.v1;\n\nservice Agent {\n rpc Execute(ExecuteRequest) returns (stream Update);\n // Liveness/health probe served by serve() itself \u2014 no agent code required.\n // deep=true also exercises the StackFactor auth callback (BACKEND_URL).\n rpc HealthCheck(HealthCheckRequest) returns (HealthCheckResponse);\n}\n\nmessage ExecuteRequest {\n string content_type = 1;\n string data_json = 2;\n string config_json = 3;\n string request_json = 4;\n int32 event = 5;\n // Optional repo-relative module to run INSTEAD of the agent's main (e.g.\n // \"src/webhooks/inbound-sms.js\"). Empty -> main, which is every pre-webhook\n // caller, so this stays wire-compatible with agents built before it existed.\n // Set by the webhook receiver from the integration's webHooks[].code. Follows\n // the same untrusted-path rules as HealthCheck's check_code.\n string code = 6;\n}\n\nmessage HealthCheckRequest {\n // Optional JSON carrying { authToken } for the deep backend-callback check.\n string request_json = 1;\n bool deep = 2;\n // Deep check only: repo-relative path to the agent's self-check module to run\n // (e.g. \"src/check.js\"). Empty \u2192 fall back to the conventional \"src/check.js\".\n string check_code = 3;\n // Deep check only: JSON of the resolved agent config (constantsAndVars/\n // secrets) made available to the self-check as ctx.config. May be empty.\n string config_json = 4;\n}\n\n// One result row from the agent's self-check module. The agent owns only its\n// own checks; reachability/ingress/version-drift rows are synthesised backend-\n// side. severity is optional: \"warn\" marks a failing check as non-blocking;\n// empty or \"error\" is blocking.\nmessage CheckResult {\n string name = 1;\n bool ok = 2;\n string detail = 3;\n string severity = 4;\n}\n\nmessage HealthCheckResponse {\n bool ok = 1;\n bool backend_reachable = 2;\n string version = 3;\n string message = 4;\n repeated CheckResult checks = 5;\n}\n\nmessage Update {\n oneof payload {\n Progress progress = 1;\n Result result = 2;\n ErrorInfo error = 3;\n }\n}\n\nmessage Progress { int32 progress = 1; string message = 2; }\nmessage Result { string result_json = 1; }\nmessage ErrorInfo { int32 code = 1; string message = 2; }\n";
10
10
  /**
11
11
  * Loads the Agent proto package. proto-loader reads from a file, so the embedded
12
12
  * schema is written to a temp path — keeps the package self-contained across the
@@ -1 +1 @@
1
- {"version":3,"file":"agentProto.d.ts","sourceRoot":"","sources":["../../src/agentProto.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,IAAI,MAAM,eAAe,CAAC;AAMtC;;;;;;GAMG;AACH,eAAO,MAAM,KAAK,09DA6DjB,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,gBAAgB,QAAO,GAWnC,CAAC;AAEF,oEAAoE;AACpE,eAAO,MAAM,gBAAgB,QAAO,IAAI,CAAC,iBACc,CAAC"}
1
+ {"version":3,"file":"agentProto.d.ts","sourceRoot":"","sources":["../../src/agentProto.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,IAAI,MAAM,eAAe,CAAC;AAMtC;;;;;;GAMG;AACH,eAAO,MAAM,KAAK,y3EAmEjB,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,gBAAgB,QAAO,GAWnC,CAAC;AAEF,oEAAoE;AACpE,eAAO,MAAM,gBAAgB,QAAO,IAAI,CAAC,iBACc,CAAC"}
@@ -27,6 +27,12 @@ message ExecuteRequest {
27
27
  string config_json = 3;
28
28
  string request_json = 4;
29
29
  int32 event = 5;
30
+ // Optional repo-relative module to run INSTEAD of the agent's main (e.g.
31
+ // "src/webhooks/inbound-sms.js"). Empty -> main, which is every pre-webhook
32
+ // caller, so this stays wire-compatible with agents built before it existed.
33
+ // Set by the webhook receiver from the integration's webHooks[].code. Follows
34
+ // the same untrusted-path rules as HealthCheck's check_code.
35
+ string code = 6;
30
36
  }
31
37
 
32
38
  message HealthCheckRequest {
@@ -10,6 +10,13 @@ export interface AgentExecuteRequest {
10
10
  config_json: string;
11
11
  request_json: string;
12
12
  event: number;
13
+ /**
14
+ * Optional repo-relative module to run instead of the agent's `main` — the
15
+ * webhook receiver sets it from the integration's `webHooks[].code`. Omit (or
16
+ * pass "") for every other caller; the agent then runs `main` exactly as
17
+ * before. Agents built against an older agent-utils ignore the field.
18
+ */
19
+ code?: string;
13
20
  }
14
21
  export interface CallAgentOptions {
15
22
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../src/client.ts"],"names":[],"mappings":"AAGA;;;;;GAKG;AACH,MAAM,WAAW,mBAAmB;IAClC,YAAY,EAAE,MAAM,CAAC;IACrB,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,MAAM,CAAC;IACrB,KAAK,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,gBAAgB;IAC/B;;;;OAIG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,+EAA+E;IAC/E,UAAU,CAAC,EAAE,CAAC,MAAM,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,KAAK,IAAI,CAAC;IACrE,gFAAgF;IAChF,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,gFAAgF;IAChF,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AA8DD;;;;;;;;GAQG;AACH,eAAO,MAAM,SAAS,GACpB,UAAU,MAAM,EAChB,SAAS,mBAAmB,EAC5B,UAAS,gBAAqB,KAC7B,OAAO,CAAC,GAAG,CA0DV,CAAC;AAEL,MAAM,WAAW,iBAAiB;IAChC,wEAAwE;IACxE,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB;;;OAGG;IACH,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,0EAA0E;IAC1E,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;OAGG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;OAGG;IACH,MAAM,CAAC,EAAE,GAAG,CAAC;IACb,uEAAuE;IACvE,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,+DAA+D;AAC/D,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,EAAE,EAAE,OAAO,CAAC;IACZ,MAAM,EAAE,MAAM,CAAC;IACf,uDAAuD;IACvD,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,WAAW;IAC1B,2EAA2E;IAC3E,EAAE,EAAE,OAAO,CAAC;IACZ,gFAAgF;IAChF,gBAAgB,EAAE,OAAO,CAAC;IAC1B,8DAA8D;IAC9D,OAAO,EAAE,MAAM,CAAC;IAChB,6EAA6E;IAC7E,OAAO,EAAE,MAAM,CAAC;IAChB,gFAAgF;IAChF,MAAM,EAAE,UAAU,EAAE,CAAC;CACtB;AAID;;;;;;;GAOG;AACH,eAAO,MAAM,UAAU,GACrB,UAAU,MAAM,EAChB,UAAS,iBAAsB,KAC9B,OAAO,CAAC,WAAW,CA8ClB,CAAC"}
1
+ {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../src/client.ts"],"names":[],"mappings":"AAGA;;;;;GAKG;AACH,MAAM,WAAW,mBAAmB;IAClC,YAAY,EAAE,MAAM,CAAC;IACrB,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,MAAM,CAAC;IACrB,KAAK,EAAE,MAAM,CAAC;IACd;;;;;OAKG;IACH,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,gBAAgB;IAC/B;;;;OAIG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,+EAA+E;IAC/E,UAAU,CAAC,EAAE,CAAC,MAAM,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,KAAK,IAAI,CAAC;IACrE,gFAAgF;IAChF,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,gFAAgF;IAChF,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AA8DD;;;;;;;;GAQG;AACH,eAAO,MAAM,SAAS,GACpB,UAAU,MAAM,EAChB,SAAS,mBAAmB,EAC5B,UAAS,gBAAqB,KAC7B,OAAO,CAAC,GAAG,CA0DV,CAAC;AAEL,MAAM,WAAW,iBAAiB;IAChC,wEAAwE;IACxE,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB;;;OAGG;IACH,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,0EAA0E;IAC1E,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;OAGG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;OAGG;IACH,MAAM,CAAC,EAAE,GAAG,CAAC;IACb,uEAAuE;IACvE,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,+DAA+D;AAC/D,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,EAAE,EAAE,OAAO,CAAC;IACZ,MAAM,EAAE,MAAM,CAAC;IACf,uDAAuD;IACvD,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,WAAW;IAC1B,2EAA2E;IAC3E,EAAE,EAAE,OAAO,CAAC;IACZ,gFAAgF;IAChF,gBAAgB,EAAE,OAAO,CAAC;IAC1B,8DAA8D;IAC9D,OAAO,EAAE,MAAM,CAAC;IAChB,6EAA6E;IAC7E,OAAO,EAAE,MAAM,CAAC;IAChB,gFAAgF;IAChF,MAAM,EAAE,UAAU,EAAE,CAAC;CACtB;AAID;;;;;;;GAOG;AACH,eAAO,MAAM,UAAU,GACrB,UAAU,MAAM,EAChB,UAAS,iBAAsB,KAC9B,OAAO,CAAC,WAAW,CA8ClB,CAAC"}
@@ -9,7 +9,7 @@ export { constants };
9
9
  export { errorHandling, AppError };
10
10
  export type { ParsedError } from "./errorHandling.js";
11
11
  export { langChain };
12
- export type { UsageTracker } from "./langChain.js";
12
+ export type { UsageTracker, WebSearchConfig } from "./langChain.js";
13
13
  export { logger };
14
14
  export { serve };
15
15
  export { callAgent };
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,SAAS,MAAM,YAAY,CAAC;AACnC,OAAO,aAAa,EAAE,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAC;AAC7D,OAAO,SAAS,MAAM,gBAAgB,CAAC;AACvC,OAAO,MAAM,MAAM,aAAa,CAAC;AACjC,OAAO,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AACnC,OAAO,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACpD,OAAO,KAAK,cAAc,MAAM,qBAAqB,CAAC;AAEtD,OAAO,EAAE,SAAS,EAAE,CAAC;AAErB,OAAO,EAAE,aAAa,EAAE,QAAQ,EAAE,CAAC;AAEnC,YAAY,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AAEtD,OAAO,EAAE,SAAS,EAAE,CAAC;AAErB,YAAY,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAEnD,OAAO,EAAE,MAAM,EAAE,CAAC;AAElB,OAAO,EAAE,KAAK,EAAE,CAAC;AAEjB,OAAO,EAAE,SAAS,EAAE,CAAC;AAErB,OAAO,EAAE,UAAU,EAAE,CAAC;AAEtB,YAAY,EACV,mBAAmB,EACnB,gBAAgB,EAChB,iBAAiB,EACjB,WAAW,GACZ,MAAM,aAAa,CAAC;AAErB,OAAO,EAAE,cAAc,EAAE,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,SAAS,MAAM,YAAY,CAAC;AACnC,OAAO,aAAa,EAAE,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAC;AAC7D,OAAO,SAAS,MAAM,gBAAgB,CAAC;AACvC,OAAO,MAAM,MAAM,aAAa,CAAC;AACjC,OAAO,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AACnC,OAAO,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACpD,OAAO,KAAK,cAAc,MAAM,qBAAqB,CAAC;AAEtD,OAAO,EAAE,SAAS,EAAE,CAAC;AAErB,OAAO,EAAE,aAAa,EAAE,QAAQ,EAAE,CAAC;AAEnC,YAAY,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AAEtD,OAAO,EAAE,SAAS,EAAE,CAAC;AAErB,YAAY,EAAE,YAAY,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAC;AAEpE,OAAO,EAAE,MAAM,EAAE,CAAC;AAElB,OAAO,EAAE,KAAK,EAAE,CAAC;AAEjB,OAAO,EAAE,SAAS,EAAE,CAAC;AAErB,OAAO,EAAE,UAAU,EAAE,CAAC;AAEtB,YAAY,EACV,mBAAmB,EACnB,gBAAgB,EAChB,iBAAiB,EACjB,WAAW,GACZ,MAAM,aAAa,CAAC;AAErB,OAAO,EAAE,cAAc,EAAE,CAAC"}
@@ -9,6 +9,73 @@ export type UsageTracker = {
9
9
  tokens: {
10
10
  [tokenKey: string]: number;
11
11
  };
12
+ /**
13
+ * Sources returned by native web search, deduplicated by URL across every
14
+ * call in the run. Only present once a search has actually run. Anthropic and
15
+ * Google both require the original sources to be cited when their output is
16
+ * shown to end users, so they are surfaced here rather than discarded with
17
+ * the rest of the non-text content blocks.
18
+ */
19
+ webSearchSources?: {
20
+ url: string;
21
+ title?: string;
22
+ }[];
23
+ };
24
+ /**
25
+ * Caller-facing options for the providers' native web search tools. Enable web
26
+ * search by setting `config.webSearch` to `true` (provider defaults) or to one
27
+ * of these objects. Every field is optional and is only forwarded to the
28
+ * providers that accept it — see `buildWebSearchTool` for the mapping.
29
+ */
30
+ export type WebSearchConfig = {
31
+ /** Anthropic only: hard cap on searches per request (`max_uses`). */
32
+ maxUses?: number;
33
+ /** Anthropic (`allowed_domains`) and OpenAI (`filters.allowed_domains`). */
34
+ allowedDomains?: string[];
35
+ /** Anthropic only (`blocked_domains`); cannot be combined with `allowedDomains`. */
36
+ blockedDomains?: string[];
37
+ /** Anthropic and OpenAI: approximate location used to localize results. */
38
+ userLocation?: {
39
+ city?: string;
40
+ region?: string;
41
+ /** Two-letter ISO 3166-1 alpha-2 code, e.g. `"US"`. */
42
+ country?: string;
43
+ /** IANA timezone ID, e.g. `"America/Los_Angeles"`. */
44
+ timezone?: string;
45
+ };
46
+ /**
47
+ * OpenAI only: how much of the context window search results may consume.
48
+ * OpenAI's default is `"medium"`; `"low"` minimizes context at some cost to
49
+ * answer quality, `"high"` is the expensive end.
50
+ */
51
+ searchContextSize?: "low" | "medium" | "high";
52
+ /** Gemini only: RFC 3339 window the search is restricted to. */
53
+ timeRange?: {
54
+ startTime: string;
55
+ endTime: string;
56
+ };
57
+ /**
58
+ * Anthropic only: opt out of dynamic filtering by forcing the search to be
59
+ * called directly (`allowed_callers: ["direct"]`). Defaults to `true` on
60
+ * models that support it — see `supportsAnthropicDynamicFiltering`. Turning
61
+ * this off means every raw search result lands in the context window.
62
+ */
63
+ dynamicFiltering?: boolean;
64
+ /**
65
+ * Anthropic only: whether search result blocks consumed by dynamic filtering
66
+ * are echoed back in the response. Defaults to `"excluded"`, which drops them
67
+ * and cuts the output tokens billed for content nothing downstream reads.
68
+ */
69
+ responseInclusion?: "full" | "excluded";
70
+ /**
71
+ * Anthropic only: pin the dated tool version instead of letting
72
+ * `buildWebSearchTool` pick per model. `web_search_20250305` is basic search,
73
+ * `web_search_20260209` adds dynamic filtering, `web_search_20260318` adds
74
+ * response-inclusion control. Pinning a filtering version on a model that
75
+ * cannot do programmatic tool calling returns a 400 unless
76
+ * `dynamicFiltering: false` is also set.
77
+ */
78
+ toolVersion?: string;
12
79
  };
13
80
  declare const _default: {
14
81
  checkIfAIProviderConfigured: (config: any) => void;
@@ -1 +1 @@
1
- {"version":3,"file":"langChain.d.ts","sourceRoot":"","sources":["../../src/langChain.ts"],"names":[],"mappings":"AAuGA;;;;;GAKG;AACH,MAAM,MAAM,YAAY,GAAG;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE;QAAE,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAAA;KAAE,CAAC;CACxC,CAAC;;0CAE2C,GAAG,KAAG,IAAI;wBAktB/C,MAAM,aACD,MAAM,gBACH,MAAM,SACb,GAAG,EAAE,kBACI,GAAG,UACX,GAAG,KACV,GAAG;sBA0BG,GAAG,UACF,MAAM,UACN,GAAG,eACC,QAAQ,GAAG,IAAI,iBACb,YAAY,GAAG,IAAI,KAChC,OAAO,CAAC,GAAG,CAAC;oCA4XF,MAAM,UACT,GAAG,UACH,GAAG,oBACO,GAAG,eACT,MAAM,eACN,MAAM,wBACG,OAAO,WACpB,GAAG,cACA,MAAM,UACV,GAAG,EAAE,iBACE,YAAY,GAAG,IAAI,KAChC,OAAO,CAAC,GAAG,CAAC;sDA2tBF,MAAM,UACT,GAAG,UACH,MAAM,YACL,GAAG,iBACE,YAAY,GAAG,IAAI,KAChC,OAAO,CAAC,GAAG,CAAC;0CA//B8B,GAAG,KAAG,MAAM;+CAprB9C,YAAY,GAAG,IAAI,GAAG,SAAS,aAC7B,MAAM,kBACD,MAAM,UACd,GAAG,KACV,IAAI;mCAosBU,MAAM,mBACJ,MAAM,EAAE,KACxB,MAAM;;AAsiCT,wBASE"}
1
+ {"version":3,"file":"langChain.d.ts","sourceRoot":"","sources":["../../src/langChain.ts"],"names":[],"mappings":"AAuGA;;;;;GAKG;AACH,MAAM,MAAM,YAAY,GAAG;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE;QAAE,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAAA;KAAE,CAAC;IACvC;;;;;;OAMG;IACH,gBAAgB,CAAC,EAAE;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;CACtD,CAAC;AAskBF;;;;;GAKG;AACH,MAAM,MAAM,eAAe,GAAG;IAC5B,qEAAqE;IACrE,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,4EAA4E;IAC5E,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;IAC1B,oFAAoF;IACpF,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;IAC1B,2EAA2E;IAC3E,YAAY,CAAC,EAAE;QACb,IAAI,CAAC,EAAE,MAAM,CAAC;QACd,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,uDAAuD;QACvD,OAAO,CAAC,EAAE,MAAM,CAAC;QACjB,sDAAsD;QACtD,QAAQ,CAAC,EAAE,MAAM,CAAC;KACnB,CAAC;IACF;;;;OAIG;IACH,iBAAiB,CAAC,EAAE,KAAK,GAAG,QAAQ,GAAG,MAAM,CAAC;IAC9C,gEAAgE;IAChE,SAAS,CAAC,EAAE;QAAE,SAAS,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC;IACnD;;;;;OAKG;IACH,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B;;;;OAIG;IACH,iBAAiB,CAAC,EAAE,MAAM,GAAG,UAAU,CAAC;IACxC;;;;;;;OAOG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB,CAAC;;0CAxnB2C,GAAG,KAAG,IAAI;wBAujC/C,MAAM,aACD,MAAM,gBACH,MAAM,SACb,GAAG,EAAE,kBACI,GAAG,UACX,GAAG,KACV,GAAG;sBAiCG,GAAG,UACF,MAAM,UACN,GAAG,eACC,QAAQ,GAAG,IAAI,iBACb,YAAY,GAAG,IAAI,KAChC,OAAO,CAAC,GAAG,CAAC;oCA0YF,MAAM,UACT,GAAG,UACH,GAAG,oBACO,GAAG,eACT,MAAM,eACN,MAAM,wBACG,OAAO,WACpB,GAAG,cACA,MAAM,UACV,GAAG,EAAE,iBACE,YAAY,GAAG,IAAI,KAChC,OAAO,CAAC,GAAG,CAAC;sDAuuBF,MAAM,UACT,GAAG,UACH,MAAM,YACL,GAAG,iBACE,YAAY,GAAG,IAAI,KAChC,OAAO,CAAC,GAAG,CAAC;0CAjhC8B,GAAG,KAAG,MAAM;+CAviC9C,YAAY,GAAG,IAAI,GAAG,SAAS,aAC7B,MAAM,kBACD,MAAM,UACd,GAAG,KACV,IAAI;mCAujCU,MAAM,mBACJ,MAAM,EAAE,KACxB,MAAM;;AAwjCT,wBASE"}
@@ -603,6 +603,234 @@ const resolveTemperatureSetting = (modelName, config) => {
603
603
  }
604
604
  return { temperature: clamped };
605
605
  };
606
+ /**
607
+ * Whether a Claude model can run web search through dynamic filtering, where
608
+ * Claude writes and runs code that filters the search results before they reach
609
+ * the context window instead of loading every result into it. Requires Claude
610
+ * 4.6 or later (the models with programmatic tool calling); on anything earlier
611
+ * the filtering tool versions return a 400 unless search is pinned to
612
+ * `allowed_callers: ["direct"]`.
613
+ * @param modelName - The Claude model identifier being routed
614
+ * @returns `true` when the model supports dynamic filtering
615
+ */
616
+ const supportsAnthropicDynamicFiltering = (modelName) => /^claude-(opus|sonnet|haiku)-4-(?:[6-9]|\d\d)\b/.test(modelName) ||
617
+ /^claude-(opus|sonnet|haiku|fable|mythos)-(?:[5-9]|\d\d)\b/.test(modelName);
618
+ /**
619
+ * Normalizes `config.webSearch` into an options object, returning `null` when
620
+ * web search is off so callers can use it as the single enablement gate.
621
+ */
622
+ const getWebSearchOptions = (config) => {
623
+ const webSearch = config?.webSearch;
624
+ if (!webSearch)
625
+ return null;
626
+ return webSearch === true ? {} : webSearch;
627
+ };
628
+ /**
629
+ * Builds the provider-native web search tool definition for a model:
630
+ * - `claude-` → Anthropic's `web_search` server tool, executed by the Messages
631
+ * API within a single request and answered with citations. Defaults to the
632
+ * dynamic-filtering tool version on models that support it, so search results
633
+ * are filtered by code before they reach the context window.
634
+ * - `gpt-` → OpenAI's hosted `web_search` tool (Responses API). Context spend
635
+ * is governed by `search_context_size` (OpenAI defaults to `medium`).
636
+ * - `gemini-` → Google's `googleSearch` grounding tool. Google exposes no
637
+ * result-filtering or context-size control; leaving `searchTypes` unset keeps
638
+ * grounding on text-only web results rather than image bytes.
639
+ * Returns `null` for providers with no native web search (DeepSeek, Kimi, GLM),
640
+ * warning instead of throwing so one config can be pointed at any model.
641
+ * @param modelName - The model identifier being routed
642
+ * @param options - Normalized options from `getWebSearchOptions`
643
+ * @returns The provider's tool definition, or `null` when unsupported
644
+ */
645
+ const buildWebSearchTool = (modelName, options) => {
646
+ const { allowedDomains, blockedDomains, userLocation } = options;
647
+ if (modelName.startsWith("claude-")) {
648
+ // The API returns a 400 when both filters are present, so fail locally
649
+ // rather than paying for the round trip.
650
+ if (allowedDomains && blockedDomains) {
651
+ throw errorHandlingHelper.create(constants.HTTP_CODES.BAD_REQUEST, "Anthropic web search accepts allowedDomains or blockedDomains, not both.");
652
+ }
653
+ // Prefer the newest tool version the model can actually run. Basic search
654
+ // loads every result into the context window; from `web_search_20260209`
655
+ // Claude filters them with code first, and `web_search_20260318` can also
656
+ // keep the consumed results out of the response.
657
+ const canFilter = supportsAnthropicDynamicFiltering(modelName);
658
+ const type = options.toolVersion ||
659
+ (canFilter ? "web_search_20260318" : "web_search_20250305");
660
+ const version = Number(type.slice(-8));
661
+ // Filtering versions default to running search from inside code execution.
662
+ // Say so explicitly when it is not wanted (or not possible), which is what
663
+ // the API requires from models without programmatic tool calling.
664
+ const directOnly = version >= 20260209 && (options.dynamicFiltering === false || !canFilter);
665
+ return {
666
+ type,
667
+ name: "web_search",
668
+ ...(options.maxUses ? { max_uses: options.maxUses } : {}),
669
+ ...(allowedDomains ? { allowed_domains: allowedDomains } : {}),
670
+ ...(blockedDomains ? { blocked_domains: blockedDomains } : {}),
671
+ ...(userLocation
672
+ ? { user_location: { type: "approximate", ...userLocation } }
673
+ : {}),
674
+ ...(directOnly ? { allowed_callers: ["direct"] } : {}),
675
+ ...(version >= 20260318
676
+ ? { response_inclusion: options.responseInclusion || "excluded" }
677
+ : {}),
678
+ };
679
+ }
680
+ if (modelName.startsWith("gpt-")) {
681
+ return {
682
+ type: "web_search",
683
+ ...(allowedDomains
684
+ ? { filters: { allowed_domains: allowedDomains } }
685
+ : {}),
686
+ ...(userLocation
687
+ ? { user_location: { type: "approximate", ...userLocation } }
688
+ : {}),
689
+ ...(options.searchContextSize
690
+ ? { search_context_size: options.searchContextSize }
691
+ : {}),
692
+ };
693
+ }
694
+ if (modelName.startsWith("gemini-")) {
695
+ // `timeRangeFilter` is the only filter the Gemini API exposes;
696
+ // `excludeDomains` is a Vertex AI field and is rejected here, so domain
697
+ // filters are deliberately not mapped for Google.
698
+ return {
699
+ googleSearch: options.timeRange
700
+ ? { timeRangeFilter: options.timeRange }
701
+ : {},
702
+ };
703
+ }
704
+ logger.log(null, logger.levels.warn, `Model "${modelName}" has no native web search tool; ignoring the configured webSearch options.`);
705
+ return null;
706
+ };
707
+ /**
708
+ * Flattens LangChain message content into plain text. Content is a string for
709
+ * ordinary completions, but every provider switches to an array of blocks once
710
+ * a server-side tool runs — Anthropic interleaves `server_tool_use` and
711
+ * `web_search_tool_result` blocks with the answer text, and OpenAI's Responses
712
+ * API returns annotated text blocks — so without this the JSON parse pipeline
713
+ * would receive a non-string and every web-search call would fail.
714
+ * @param content - A message's `content` field, or a raw string
715
+ * @returns The concatenated text of all text blocks
716
+ */
717
+ const extractTextContent = (content) => {
718
+ if (typeof content === "string")
719
+ return content;
720
+ if (!Array.isArray(content))
721
+ return "";
722
+ let text = "";
723
+ for (const block of content) {
724
+ if (typeof block === "string")
725
+ text += block;
726
+ else if (block?.type === "text" && typeof block.text === "string") {
727
+ text += block.text;
728
+ }
729
+ }
730
+ return text;
731
+ };
732
+ const createWebSearchUsage = () => ({
733
+ reportedRequests: 0,
734
+ callIds: new Set(),
735
+ grounded: false,
736
+ sources: new Map(),
737
+ });
738
+ /**
739
+ * Folds one message — or one streaming chunk — into a `WebSearchUsage`. Safe to
740
+ * call on every chunk of a stream and on messages that involved no search.
741
+ */
742
+ const collectWebSearchUsage = (payload, usage) => {
743
+ if (!payload)
744
+ return;
745
+ const addSource = (url, title) => {
746
+ if (typeof url === "string" && url && !usage.sources.has(url)) {
747
+ usage.sources.set(url, {
748
+ url,
749
+ ...(typeof title === "string" ? { title } : {}),
750
+ });
751
+ }
752
+ };
753
+ if (Array.isArray(payload.content)) {
754
+ for (const block of payload.content) {
755
+ if (!block || typeof block !== "object")
756
+ continue;
757
+ // Anthropic: results of a search the API executed server-side.
758
+ if (block.type === "web_search_tool_result" &&
759
+ Array.isArray(block.content)) {
760
+ for (const result of block.content) {
761
+ addSource(result?.url, result?.title);
762
+ }
763
+ }
764
+ // Anthropic: citations attached to the answer's text blocks.
765
+ if (Array.isArray(block.citations)) {
766
+ for (const citation of block.citations) {
767
+ addSource(citation?.url, citation?.title);
768
+ }
769
+ }
770
+ // OpenAI Responses API: one block per executed search, plus url citations.
771
+ if (block.type === "web_search_call" && block.id) {
772
+ usage.callIds.add(block.id);
773
+ }
774
+ if (Array.isArray(block.annotations)) {
775
+ for (const annotation of block.annotations) {
776
+ if (annotation?.type === "url_citation") {
777
+ addSource(annotation.url, annotation.title);
778
+ }
779
+ }
780
+ }
781
+ }
782
+ }
783
+ const metadata = payload.response_metadata;
784
+ if (!metadata)
785
+ return;
786
+ const requests = metadata.usage?.server_tool_use?.web_search_requests;
787
+ if (typeof requests === "number" && requests > usage.reportedRequests) {
788
+ usage.reportedRequests = requests;
789
+ }
790
+ const grounding = metadata.groundingMetadata;
791
+ if (grounding) {
792
+ usage.grounded = true;
793
+ for (const chunk of grounding.groundingChunks || []) {
794
+ addSource(chunk?.web?.uri, chunk?.web?.title);
795
+ }
796
+ }
797
+ };
798
+ /**
799
+ * Adds a call's web-search usage to the caller-supplied tracker. Searches are
800
+ * billed per request rather than per token (Anthropic charges $10 per 1,000
801
+ * searches; Google charges per grounded request), so the rate is read from the
802
+ * `<model>-web-search-costs` constant expressed in USD per 1,000 searches and
803
+ * accumulated under `<model>_webSearches`. Sources are appended to
804
+ * `tracker.webSearchSources`, deduplicated by URL across the whole run.
805
+ */
806
+ const updateWebSearchUsageTracker = (tracker, modelName, usage, config) => {
807
+ if (!tracker || !modelName)
808
+ return;
809
+ // The three signals describe the same searches from different providers, so
810
+ // the largest one is the count rather than their sum.
811
+ const searches = Math.max(usage.reportedRequests, usage.callIds.size, usage.grounded ? 1 : 0);
812
+ if (!searches && usage.sources.size === 0)
813
+ return;
814
+ if (typeof tracker.cost !== "number")
815
+ tracker.cost = 0;
816
+ if (!tracker.tokens || typeof tracker.tokens !== "object")
817
+ tracker.tokens = {};
818
+ if (searches > 0) {
819
+ const addedCost = (searches / 1_000) * getModelRate(modelName, config, "web-search");
820
+ if (Number.isFinite(addedCost) && addedCost > 0)
821
+ tracker.cost += addedCost;
822
+ const key = `${modelName}_webSearches`;
823
+ tracker.tokens[key] = (tracker.tokens[key] || 0) + searches;
824
+ }
825
+ if (usage.sources.size > 0) {
826
+ const sources = tracker.webSearchSources || (tracker.webSearchSources = []);
827
+ for (const source of usage.sources.values()) {
828
+ if (!sources.some((existing) => existing.url === source.url)) {
829
+ sources.push(source);
830
+ }
831
+ }
832
+ }
833
+ };
606
834
  /**
607
835
  * Instantiates and returns the appropriate LangChain chat model based on the model
608
836
  * name prefix. `claude-` maps to `ChatAnthropic`, `gemini-` maps to
@@ -629,6 +857,13 @@ const getLLMModel = (modelName, config, schema = null) => {
629
857
  // Resolve `temperature` with presence/support/range handling (see
630
858
  // resolveTemperatureSetting). Applied uniformly to every provider below.
631
859
  const modelSettings = resolveTemperatureSetting(modelName, config);
860
+ // Native web search (see buildWebSearchTool). The tool is bound to the model
861
+ // so both `.invoke()` and `.stream()` pick it up; the provider runs the search
862
+ // server-side within the same request, so no client-side agent loop is needed.
863
+ const webSearchOptions = getWebSearchOptions(config);
864
+ const webSearchTool = webSearchOptions
865
+ ? buildWebSearchTool(modelName, webSearchOptions)
866
+ : null;
632
867
  // Claude models (Anthropic)
633
868
  if (modelName.startsWith("claude-")) {
634
869
  // Anthropic's SDK rejects non-streamed requests when max_tokens is large
@@ -653,7 +888,7 @@ const getLLMModel = (modelName, config, schema = null) => {
653
888
  },
654
889
  }
655
890
  : {};
656
- return new ChatAnthropic({
891
+ const model = new ChatAnthropic({
657
892
  apiKey: config.anthropicAPIKey,
658
893
  maxTokens,
659
894
  modelName: modelName,
@@ -662,6 +897,7 @@ const getLLMModel = (modelName, config, schema = null) => {
662
897
  ...outputConfig,
663
898
  ...modelSettings,
664
899
  });
900
+ return webSearchTool ? model.bindTools([webSearchTool]) : model;
665
901
  }
666
902
  // Gemini models (Google)
667
903
  else if (modelName.startsWith("gemini-")) {
@@ -674,6 +910,11 @@ const getLLMModel = (modelName, config, schema = null) => {
674
910
  ...(schema ? { json: true } : {}),
675
911
  ...modelSettings,
676
912
  });
913
+ // Combining grounding with structured output requires Gemini 3 or later;
914
+ // Gemini 1.5/2.x reject `responseSchema` alongside `googleSearch` with a 400.
915
+ const bound = webSearchTool
916
+ ? model.bindTools([webSearchTool])
917
+ : model;
677
918
  // `responseSchema` additionally constrains the output shape. It is a
678
919
  // call-time option (not a constructor field), so it is bound onto the model
679
920
  // via `withConfig`. Gemini's schema is an OpenAPI 3.0 subset, so
@@ -681,9 +922,9 @@ const getLLMModel = (modelName, config, schema = null) => {
681
922
  // parse/validate pipeline is unchanged.
682
923
  if (schema) {
683
924
  const jsonSchema = sanitizeGeminiSchema(buildJsonSchema(schema));
684
- return model.withConfig({ responseSchema: jsonSchema });
925
+ return bound.withConfig({ responseSchema: jsonSchema });
685
926
  }
686
- return model;
927
+ return bound;
687
928
  }
688
929
  // GPT models (OpenAI)
689
930
  else if (modelName.startsWith("gpt-")) {
@@ -691,23 +932,41 @@ const getLLMModel = (modelName, config, schema = null) => {
691
932
  apiKey: config.openAIAPIKey,
692
933
  max_tokens: config.maxTokens || 200000,
693
934
  modelName: modelName,
935
+ // `web_search` is a hosted Responses API tool, so the request has to go to
936
+ // `/v1/responses` rather than `/v1/chat/completions`.
937
+ ...(webSearchTool ? { useResponsesApi: true } : {}),
694
938
  ...modelSettings,
695
939
  };
696
- // Use native response_format with JSON schema for structured output
940
+ // Use native structured output with a JSON schema. The two endpoints spell
941
+ // the same thing differently — Chat Completions takes `response_format`,
942
+ // the Responses API takes `text.format` with the schema flattened one level
943
+ // — and `modelKwargs` is spread verbatim into whichever request is built.
697
944
  if (schema) {
698
945
  const jsonSchema = strictifyJsonSchema(buildJsonSchema(schema));
699
- openAISettings.modelKwargs = {
700
- response_format: {
701
- type: "json_schema",
702
- json_schema: {
703
- name: "response_schema",
704
- strict: true,
705
- schema: jsonSchema,
946
+ openAISettings.modelKwargs = webSearchTool
947
+ ? {
948
+ text: {
949
+ format: {
950
+ type: "json_schema",
951
+ name: "response_schema",
952
+ strict: true,
953
+ schema: jsonSchema,
954
+ },
706
955
  },
707
- },
708
- };
956
+ }
957
+ : {
958
+ response_format: {
959
+ type: "json_schema",
960
+ json_schema: {
961
+ name: "response_schema",
962
+ strict: true,
963
+ schema: jsonSchema,
964
+ },
965
+ },
966
+ };
709
967
  }
710
- return new ChatOpenAI(openAISettings);
968
+ const model = new ChatOpenAI(openAISettings);
969
+ return webSearchTool ? model.bindTools([webSearchTool]) : model;
711
970
  }
712
971
  // OpenAI-compatible providers: DeepSeek, Kimi (Moonshot), GLM (Zhipu)
713
972
  const openAICompatible = getOpenAICompatibleProvider(modelName, config);
@@ -736,11 +995,18 @@ const getLLMModel = (modelName, config, schema = null) => {
736
995
  * @returns A configured LangChain agent instance ready to be run with `runAgent`
737
996
  */
738
997
  const createAgent = (name, modelName, systemPrompt, tools = [], responseFormat, config) => {
998
+ // Native web search joins the agent's tool list instead of being bound inside
999
+ // `getLLMModel`: the agent binds its own tools to the model, which would drop
1000
+ // anything already bound there.
1001
+ const webSearchOptions = getWebSearchOptions(config);
1002
+ const webSearchTool = webSearchOptions
1003
+ ? buildWebSearchTool(modelName, webSearchOptions)
1004
+ : null;
739
1005
  const agent = createLangChainAgent({
740
1006
  name: name,
741
- model: getLLMModel(modelName, config),
1007
+ model: getLLMModel(modelName, { ...config, webSearch: null }),
742
1008
  systemPrompt: systemPrompt.trim(),
743
- tools,
1009
+ tools: webSearchTool ? [...tools, webSearchTool] : tools,
744
1010
  ...(responseFormat ? { responseFormat: responseFormat } : {}),
745
1011
  });
746
1012
  return agent;
@@ -827,6 +1093,13 @@ const runAgent = async (agent, prompt, config, onProgress = null, usageTracker =
827
1093
  }
828
1094
  }
829
1095
  updateUsageTracker(usageTracker, modelName, sumAgentResponseUsage(response), config);
1096
+ // Web-search activity is spread across the agent's messages — one search may
1097
+ // be reported by the message that ran it and cited by a later one.
1098
+ const webSearchUsage = createWebSearchUsage();
1099
+ for (const message of response?.messages || []) {
1100
+ collectWebSearchUsage(message, webSearchUsage);
1101
+ }
1102
+ updateWebSearchUsageTracker(usageTracker, modelName, webSearchUsage, config);
830
1103
  const endTime = Date.now();
831
1104
  const duration = endTime - startTime;
832
1105
  logger.log(null, logger.levels.info, `Agent "${agent.options?.name}" completed in ${Math.round(duration / 1000)} seconds.`);
@@ -1063,10 +1336,16 @@ const buildValidationRetryMessages = (priorMessages, rawContent, validationError
1063
1336
  *
1064
1337
  * When `expectsJsonResponse` is `true`, JSON escape instructions are prepended to the
1065
1338
  * system prompt and the parsed result is optionally validated against `schema`.
1339
+ *
1340
+ * Setting `config.webSearch` enables the provider's native web search tool in every
1341
+ * mode (see `buildWebSearchTool`). The provider runs the search server-side inside the
1342
+ * same request, so the return contract is unchanged; the searches performed and the
1343
+ * sources cited are recorded on `usageTracker`.
1066
1344
  * @param modelName - The model identifier, e.g. `"gpt-4o"`, `"claude-3-5-sonnet"`,
1067
1345
  * `"gemini-1.5-pro"`
1068
1346
  * @param config - Configuration object with API keys, `temperature`, optional `agentic`
1069
- * flag, and optional `recursionLimit`
1347
+ * flag, optional `recursionLimit`, and optional `webSearch` (`true` or a
1348
+ * `WebSearchConfig`)
1070
1349
  * @param prompt - The prompt to send; either a plain string (user message only) or an
1071
1350
  * array of `{ role, content }` message objects
1072
1351
  * @param onProgressReport - Optional async callback invoked with `{ message, progress }`
@@ -1128,12 +1407,9 @@ const runPromptWithModel = async (modelName, config, prompt, onProgressReport, m
1128
1407
  throw errorHandlingHelper.create(constants.HTTP_CODES.INTERNAL_SERVER_ERROR, "Agent returned no messages");
1129
1408
  }
1130
1409
  const lastMessage = messages[messages.length - 1];
1131
- let rawContent = lastMessage?.content || "";
1132
- // Handle array content blocks (e.g., from Gemini/Claude agent responses)
1133
- if (Array.isArray(rawContent)) {
1134
- const textBlock = rawContent.find((block) => typeof block === "object" && block.type === "text");
1135
- rawContent = textBlock?.text || "";
1136
- }
1410
+ // Flattens the array content blocks that Gemini/Claude agent responses and
1411
+ // any server-tool turn (e.g. web search) return.
1412
+ const rawContent = extractTextContent(lastMessage?.content);
1137
1413
  // If not expecting JSON, return raw content directly
1138
1414
  if (!expectsJsonResponse) {
1139
1415
  return rawContent;
@@ -1255,6 +1531,7 @@ const runPromptWithModel = async (modelName, config, prompt, onProgressReport, m
1255
1531
  output_tokens: 0,
1256
1532
  total_tokens: 0,
1257
1533
  };
1534
+ let webSearchUsage = createWebSearchUsage();
1258
1535
  // Inner loop: wait + retry on 429 around stream setup and consumption.
1259
1536
  // Usage is only recorded on a successful stream — partial streams that
1260
1537
  // error out with a rate limit are not counted. A 429 fired mid-stream
@@ -1264,6 +1541,7 @@ const runPromptWithModel = async (modelName, config, prompt, onProgressReport, m
1264
1541
  rawContent = "";
1265
1542
  chunkCount = 0;
1266
1543
  streamUsage = { input_tokens: 0, output_tokens: 0, total_tokens: 0 };
1544
+ webSearchUsage = createWebSearchUsage();
1267
1545
  try {
1268
1546
  // Honour caller cancellation: passing the signal tears down the
1269
1547
  // upstream HTTP request so a cancelled call stops billing tokens.
@@ -1276,16 +1554,16 @@ const runPromptWithModel = async (modelName, config, prompt, onProgressReport, m
1276
1554
  throw errorHandlingHelper.create(constants.HTTP_CODES.BAD_REQUEST, "Request cancelled by caller");
1277
1555
  }
1278
1556
  accumulateChunkUsage(streamUsage, chunk);
1279
- const content = chunk?.content || chunk;
1280
- if (typeof content === "string") {
1281
- rawContent += content;
1282
- chunkCount++;
1283
- if (chunkCount % progressReportInterval === 0) {
1284
- await onProgressReport({
1285
- message: "Generating content...",
1286
- progress: Math.min(calcCurrentProgress(), maxPercent - 5),
1287
- });
1288
- }
1557
+ collectWebSearchUsage(chunk, webSearchUsage);
1558
+ // Counting every chunk (not just the ones carrying text) keeps
1559
+ // progress ticking through the pause while a search runs.
1560
+ chunkCount++;
1561
+ rawContent += extractTextContent(chunk?.content ?? chunk);
1562
+ if (chunkCount % progressReportInterval === 0) {
1563
+ await onProgressReport({
1564
+ message: "Generating content...",
1565
+ progress: Math.min(calcCurrentProgress(), maxPercent - 5),
1566
+ });
1289
1567
  }
1290
1568
  }
1291
1569
  break; // stream completed without 429
@@ -1308,6 +1586,7 @@ const runPromptWithModel = async (modelName, config, prompt, onProgressReport, m
1308
1586
  }
1309
1587
  }
1310
1588
  updateUsageTracker(usageTracker, modelName, streamUsage, config);
1589
+ updateWebSearchUsageTracker(usageTracker, modelName, webSearchUsage, config);
1311
1590
  if (!rawContent) {
1312
1591
  throw errorHandlingHelper.create(constants.HTTP_CODES.INTERNAL_SERVER_ERROR, "LLM returned no content");
1313
1592
  }
@@ -1412,7 +1691,11 @@ const runPromptWithModel = async (modelName, config, prompt, onProgressReport, m
1412
1691
  }
1413
1692
  }
1414
1693
  updateUsageTracker(usageTracker, modelName, extractUsageFromInvoke(response), config);
1415
- const rawContent = response?.content || response;
1694
+ const webSearchUsage = createWebSearchUsage();
1695
+ collectWebSearchUsage(response, webSearchUsage);
1696
+ updateWebSearchUsageTracker(usageTracker, modelName, webSearchUsage, config);
1697
+ // Flattened because a server-tool turn returns content blocks, not a string.
1698
+ const rawContent = extractTextContent(response?.content ?? response);
1416
1699
  // If not expecting JSON, return raw content directly
1417
1700
  if (!expectsJsonResponse) {
1418
1701
  return rawContent;
@@ -1 +1 @@
1
- {"version":3,"file":"serve.d.ts","sourceRoot":"","sources":["../../src/serve.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,IAAI,MAAM,eAAe,CAAC;AA8LtC;;;;;GAKG;AACH,MAAM,MAAM,SAAS,GAAG,CAAC,MAAM,CAAC,EAAE,WAAW,KAAK,OAAO,CAAC,GAAG,CAAC,CAAC;AAE/D;;;;GAIG;AACH,MAAM,MAAM,aAAa,GAAG,CAC1B,OAAO,EAAE,GAAG,KACT,OAAO,CAAC;IAAE,IAAI,EAAE,GAAG,CAAC;IAAC,MAAM,EAAE,GAAG,CAAA;CAAE,CAAC,CAAC;AAEzC,MAAM,WAAW,YAAY;IAC3B,4EAA4E;IAC5E,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,2CAA2C;IAC3C,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,wFAAwF;IACxF,YAAY,CAAC,EAAE,aAAa,CAAC;CAC9B;AAyHD;;;;;;;;;;GAUG;AACH,eAAO,MAAM,KAAK,GAChB,MAAM,SAAS,EACf,UAAS,YAAiB,KACzB,IAAI,CAAC,MA4KP,CAAC"}
1
+ {"version":3,"file":"serve.d.ts","sourceRoot":"","sources":["../../src/serve.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,IAAI,MAAM,eAAe,CAAC;AA+PtC;;;;;GAKG;AACH,MAAM,MAAM,SAAS,GAAG,CAAC,MAAM,CAAC,EAAE,WAAW,KAAK,OAAO,CAAC,GAAG,CAAC,CAAC;AAE/D;;;;GAIG;AACH,MAAM,MAAM,aAAa,GAAG,CAC1B,OAAO,EAAE,GAAG,KACT,OAAO,CAAC;IAAE,IAAI,EAAE,GAAG,CAAC;IAAC,MAAM,EAAE,GAAG,CAAA;CAAE,CAAC,CAAC;AAEzC,MAAM,WAAW,YAAY;IAC3B,4EAA4E;IAC5E,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,2CAA2C;IAC3C,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,wFAAwF;IACxF,YAAY,CAAC,EAAE,aAAa,CAAC;CAC9B;AAyHD;;;;;;;;;;GAUG;AACH,eAAO,MAAM,KAAK,GAChB,MAAM,SAAS,EACf,UAAS,YAAiB,KACzB,IAAI,CAAC,MA8MP,CAAC"}