@hue-run/sdk 0.8.0 → 0.8.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.
@@ -0,0 +1,222 @@
1
+ import { MAX_CONTENT_BYTES } from "./config.js";
2
+ const MAX_PROVIDER_ITEMS = 128;
3
+ const MAX_PROVIDER_DEFINITIONS = 512;
4
+ const MAX_PROVIDER_SERVERS = 512;
5
+ function isItem(value) {
6
+ return value !== null && typeof value === "object" && !Array.isArray(value);
7
+ }
8
+ function text(value) {
9
+ return typeof value === "string" &&
10
+ value.trim() !== "" &&
11
+ value.length <= 256 &&
12
+ !value.includes("\u0000") &&
13
+ value.isWellFormed()
14
+ ? value
15
+ : undefined;
16
+ }
17
+ /** MCP arguments arrive as a JSON string; record the structure when it parses, else the text. */
18
+ function jsonArguments(value) {
19
+ if (typeof value !== "string")
20
+ return value;
21
+ if (Buffer.byteLength(value, "utf8") > MAX_CONTENT_BYTES)
22
+ return undefined;
23
+ try {
24
+ return JSON.parse(value);
25
+ }
26
+ catch {
27
+ return value;
28
+ }
29
+ }
30
+ /** OpenAI Responses `output` items. Built-in tools are named by their kind; MCP calls by tool. */
31
+ function openaiCalls(items, activity) {
32
+ const count = Math.min(items.length, MAX_PROVIDER_ITEMS);
33
+ activity.skipped += items.length - count;
34
+ for (let index = 0; index < count; index++) {
35
+ const item = items[index];
36
+ if (!isItem(item))
37
+ continue;
38
+ const callId = text(item.id);
39
+ switch (item.type) {
40
+ case "mcp_call": {
41
+ const name = text(item.name);
42
+ if (!name) {
43
+ activity.skipped++;
44
+ break;
45
+ }
46
+ activity.calls.push({
47
+ name,
48
+ callId,
49
+ server: text(item.server_label),
50
+ arguments: jsonArguments(item.arguments),
51
+ ...(item.output !== undefined && item.output !== null ? { result: item.output } : {}),
52
+ ...(item.error !== undefined && item.error !== null ? { errorType: "mcp_error" } : {}),
53
+ });
54
+ break;
55
+ }
56
+ case "mcp_list_tools": {
57
+ const server = text(item.server_label);
58
+ if (!server || !Array.isArray(item.tools)) {
59
+ activity.skipped++;
60
+ break;
61
+ }
62
+ const definitions = [];
63
+ const definitionCount = Math.min(item.tools.length, MAX_PROVIDER_DEFINITIONS);
64
+ activity.skipped += item.tools.length - definitionCount;
65
+ for (let index = 0; index < definitionCount; index++) {
66
+ const tool = item.tools[index];
67
+ if (!isItem(tool))
68
+ continue;
69
+ definitions.push({
70
+ type: "function",
71
+ ...(tool.name !== undefined ? { name: tool.name } : {}),
72
+ ...(tool.description !== undefined ? { description: tool.description } : {}),
73
+ ...(tool.input_schema !== undefined ? { parameters: tool.input_schema } : {}),
74
+ ...(tool.annotations !== undefined ? { annotations: tool.annotations } : {}),
75
+ });
76
+ }
77
+ activity.listings.push({
78
+ server,
79
+ definitions,
80
+ ...(item.error !== undefined && item.error !== null ? { errorType: "mcp_error" } : {}),
81
+ });
82
+ break;
83
+ }
84
+ case "web_search_call":
85
+ case "file_search_call":
86
+ case "code_interpreter_call": {
87
+ const name = item.type.slice(0, -"_call".length);
88
+ const failed = item.status === "failed" ? { errorType: "failed" } : {};
89
+ if (item.type === "web_search_call")
90
+ activity.calls.push({ name, callId, arguments: item.action, ...failed });
91
+ else if (item.type === "file_search_call")
92
+ activity.calls.push({
93
+ name,
94
+ callId,
95
+ arguments: { queries: item.queries },
96
+ ...(item.results !== undefined && item.results !== null
97
+ ? { result: item.results }
98
+ : {}),
99
+ ...failed,
100
+ });
101
+ else
102
+ activity.calls.push({
103
+ name,
104
+ callId,
105
+ arguments: { code: item.code, container_id: item.container_id },
106
+ ...(item.outputs !== undefined && item.outputs !== null
107
+ ? { result: item.outputs }
108
+ : {}),
109
+ ...failed,
110
+ });
111
+ break;
112
+ }
113
+ default:
114
+ // Messages, reasoning, approval requests and other items are not executed tools.
115
+ break;
116
+ }
117
+ }
118
+ }
119
+ /** Anthropic Messages `content` blocks: a use block paired with the result block that names it. */
120
+ function anthropicCalls(blocks, activity) {
121
+ const results = new Map();
122
+ const count = Math.min(blocks.length, MAX_PROVIDER_ITEMS);
123
+ const truncated = blocks.length > MAX_PROVIDER_ITEMS;
124
+ activity.skipped += blocks.length - count;
125
+ for (let index = 0; index < count; index++) {
126
+ const block = blocks[index];
127
+ if (isItem(block) &&
128
+ typeof block.type === "string" &&
129
+ block.type.endsWith("_tool_result") &&
130
+ typeof block.tool_use_id === "string")
131
+ results.set(block.tool_use_id, block);
132
+ }
133
+ for (let index = 0; index < count; index++) {
134
+ const block = blocks[index];
135
+ if (!isItem(block) || (block.type !== "mcp_tool_use" && block.type !== "server_tool_use"))
136
+ continue;
137
+ const name = text(block.name);
138
+ const callId = text(block.id);
139
+ if (!name) {
140
+ activity.skipped++;
141
+ continue;
142
+ }
143
+ const result = callId === undefined ? undefined : results.get(callId);
144
+ // When the response was truncated, an unmatched use block may have its result outside the
145
+ // bounded prefix. Do not export it as a successful call with a missing result.
146
+ if (truncated && result === undefined) {
147
+ activity.skipped++;
148
+ continue;
149
+ }
150
+ const content = result?.content;
151
+ let errorType;
152
+ if (result?.is_error === true)
153
+ errorType = "mcp_error";
154
+ else if (isItem(content) && typeof content.type === "string" && content.type.endsWith("_error"))
155
+ errorType = text(content.error_code) ?? "error";
156
+ activity.calls.push({
157
+ name,
158
+ callId,
159
+ ...(block.type === "mcp_tool_use" ? { server: text(block.server_name) } : {}),
160
+ arguments: block.input,
161
+ ...(content !== undefined ? { result: content } : {}),
162
+ ...(errorType ? { errorType } : {}),
163
+ });
164
+ }
165
+ }
166
+ /**
167
+ * Reads the hosted tool calls out of a provider response: the `output` items of an OpenAI
168
+ * Responses API response, or the `content` blocks of an Anthropic Messages API response. An array
169
+ * is taken as those items directly. Anything else yields no calls.
170
+ */
171
+ export function hostedToolActivity(provider, response) {
172
+ const activity = { calls: [], listings: [], skipped: 0 };
173
+ const items = Array.isArray(response)
174
+ ? response
175
+ : isItem(response)
176
+ ? response[provider === "openai" ? "output" : "content"]
177
+ : undefined;
178
+ if (!Array.isArray(items))
179
+ return activity;
180
+ if (provider === "openai")
181
+ openaiCalls(items, activity);
182
+ else
183
+ anthropicCalls(items, activity);
184
+ return activity;
185
+ }
186
+ /**
187
+ * The host of each hosted MCP server's URL, by label, read from the request that produced the
188
+ * response: OpenAI `tools[].server_url` by `server_label`, Anthropic `mcp_servers[].url` by `name`.
189
+ * Nothing else in the request is read.
190
+ */
191
+ export function hostedServerAddresses(provider, request) {
192
+ const addresses = new Map();
193
+ if (!isItem(request))
194
+ return addresses;
195
+ const entries = request[provider === "openai" ? "tools" : "mcp_servers"];
196
+ if (!Array.isArray(entries))
197
+ return addresses;
198
+ const count = Math.min(entries.length, MAX_PROVIDER_SERVERS);
199
+ for (let index = 0; index < count; index++) {
200
+ const entry = entries[index];
201
+ if (!isItem(entry))
202
+ continue;
203
+ const label = text(provider === "openai" ? entry.server_label : entry.name);
204
+ const url = provider === "openai" ? entry.server_url : entry.url;
205
+ if (!label || typeof url !== "string")
206
+ continue;
207
+ try {
208
+ const { hostname } = new URL(url);
209
+ if (hostname)
210
+ addresses.set(label, hostname);
211
+ }
212
+ catch {
213
+ // Not a URL; there is no address to record.
214
+ }
215
+ }
216
+ return addresses;
217
+ }
218
+ /** The provider a `model()` call named, when this module can read its responses. */
219
+ export function hostedToolProvider(value) {
220
+ const provider = typeof value === "string" ? value.toLowerCase() : undefined;
221
+ return provider === "openai" || provider === "anthropic" ? provider : undefined;
222
+ }
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Removes hosted-tool credentials from an exported attribute value, before the caller's `redact`.
3
+ * Tool definitions come from OpenTelemetry GenAI (`gen_ai.tool.definitions`), AI SDK 6
4
+ * (`ai.prompt.tools`, one JSON string per tool) and OpenInference (`llm.tools.{i}.tool.json_schema`,
5
+ * plus the raw request in `input.value`). Other attributes, and values that are not JSON, are
6
+ * returned unchanged. Metadata-only export removes all of these attributes anyway.
7
+ *
8
+ * @throws Error when a tool definition is nested too deeply to inspect; the record is then
9
+ * rejected rather than exported with credentials.
10
+ */
11
+ export declare function scrubToolCredentials(key: string, value: unknown): unknown;
12
+ /**
13
+ * Metadata-only summary of the tool definitions a record carries, which export then removes:
14
+ * `hue.tool.names` lists each definition's name in order, and `hue.tool.definitions.sha256` is
15
+ * the lowercase hex SHA-256 of the RFC 8785 canonical JSON of the credential-scrubbed definition
16
+ * list, so the same catalog has the same digest in both SDKs and across credential rotation.
17
+ * Returns the source unchanged when it has no parseable definitions; attributes the source
18
+ * already sets are kept.
19
+ */
20
+ export declare function withToolCatalogSummary<T extends Record<string, unknown>>(source: T): T;
@@ -0,0 +1,274 @@
1
+ import { createHash } from "node:crypto";
2
+ import { MAX_BODY_BYTES } from "./config.js";
3
+ /** Replaces a hosted-tool credential found in an exported tool definition. */
4
+ const REDACTED = "[redacted]";
5
+ /**
6
+ * Credential keys, compared case-insensitively and ignoring `-` and `_`: OpenAI hosted MCP
7
+ * `authorization` and `headers`, Anthropic MCP `authorization_token`, and common API key fields.
8
+ * Keep this list deliberately broad: provider tool schemas are untrusted input and providers use
9
+ * generic names such as `token`, `secret`, and `password` for hosted credentials.
10
+ */
11
+ const credentialKeys = new Set([
12
+ "authorization",
13
+ "authorizationtoken",
14
+ "headers",
15
+ "apikey",
16
+ "accesstoken",
17
+ "xapikey",
18
+ "token",
19
+ "refreshtoken",
20
+ "clientsecret",
21
+ "password",
22
+ "secret",
23
+ "credential",
24
+ "credentials",
25
+ ]);
26
+ /** URL-valued fields in hosted tool and MCP-server definitions can carry credentials in userinfo
27
+ * or query parameters. Query values are all replaced because a provider may use an arbitrary key
28
+ * for its credential and guessing which names are sensitive would leave a leak. */
29
+ function isUrlKey(key) {
30
+ const normalized = key.toLowerCase().replace(/[-_]/g, "");
31
+ return normalized === "serverurl" || normalized === "url";
32
+ }
33
+ function scrubUrl(value, state) {
34
+ let url;
35
+ try {
36
+ url = new URL(value);
37
+ }
38
+ catch {
39
+ // A malformed URL may still contain a credential. Do not export an opaque URL-valued
40
+ // string when it cannot be parsed safely.
41
+ state.changed = true;
42
+ return REDACTED;
43
+ }
44
+ let changed = false;
45
+ if (url.username || url.password) {
46
+ url.username = "";
47
+ url.password = "";
48
+ changed = true;
49
+ }
50
+ if (url.search) {
51
+ const keys = Array.from(url.searchParams.keys());
52
+ const scrubbed = new URLSearchParams();
53
+ for (const key of keys)
54
+ scrubbed.append(key, REDACTED);
55
+ url.search = scrubbed.toString();
56
+ changed = true;
57
+ }
58
+ if (url.hash) {
59
+ url.hash = "";
60
+ changed = true;
61
+ }
62
+ if (changed)
63
+ state.changed = true;
64
+ return changed ? url.toString() : value;
65
+ }
66
+ function isCredentialKey(key) {
67
+ const normalized = key.toLowerCase().replace(/[-_]/g, "");
68
+ return (credentialKeys.has(normalized) ||
69
+ normalized.endsWith("token") ||
70
+ normalized.endsWith("secret") ||
71
+ normalized.endsWith("password") ||
72
+ normalized.endsWith("apikey") ||
73
+ normalized.endsWith("credential"));
74
+ }
75
+ /** OpenInference records each tool as `llm.tools.{index}.tool.json_schema`. */
76
+ const openInferenceTool = /^llm\.tools\.(\d+)\.tool\.json_schema$/;
77
+ /**
78
+ * Replaces the value of every credential key at any depth. Keys directly inside a JSON Schema
79
+ * `properties` object name tool parameters (a tool may take a `headers` argument), so their
80
+ * schemas are kept and scrubbed like any other value.
81
+ */
82
+ function scrubNode(value, state, depth, parameters, credentialParameter = false) {
83
+ if (depth > 256)
84
+ throw new Error("Tool definition exceeds the supported nesting limit");
85
+ if (Array.isArray(value))
86
+ return value.map((item) => scrubNode(item, state, depth + 1, false, credentialParameter));
87
+ if (value === null || typeof value !== "object")
88
+ return value;
89
+ return Object.fromEntries(Object.entries(value).map(([key, item]) => {
90
+ if (credentialParameter && ["examples", "enum"].includes(key)) {
91
+ state.changed = true;
92
+ return [key, Array.isArray(item) ? item.map(() => REDACTED) : REDACTED];
93
+ }
94
+ if (credentialParameter && ["default", "const"].includes(key)) {
95
+ state.changed = true;
96
+ return [key, REDACTED];
97
+ }
98
+ if (!parameters && item !== null && isCredentialKey(key)) {
99
+ state.changed = true;
100
+ return [key, REDACTED];
101
+ }
102
+ if (!parameters && typeof item === "string" && isUrlKey(key))
103
+ return [key, scrubUrl(item, state)];
104
+ return [
105
+ key,
106
+ scrubNode(item, state, depth + 1, key === "properties", key === "properties"
107
+ ? false
108
+ : credentialParameter || (parameters && isCredentialKey(key))),
109
+ ];
110
+ }));
111
+ }
112
+ /**
113
+ * Parses JSON text, returning `undefined` for text that is not JSON. Oversized text is parsed too:
114
+ * the caller's `redact` runs afterwards and may shorten it enough to export.
115
+ */
116
+ function parse(text) {
117
+ try {
118
+ return JSON.parse(text);
119
+ }
120
+ catch (error) {
121
+ if (error instanceof SyntaxError)
122
+ return undefined;
123
+ throw error;
124
+ }
125
+ }
126
+ /** A JSON-encoded tool definition, or list of them, with its credentials replaced. */
127
+ function scrubDefinitionText(text) {
128
+ const parsed = parse(text);
129
+ if (parsed === null || typeof parsed !== "object")
130
+ return text;
131
+ const state = { changed: false };
132
+ const scrubbed = scrubNode(parsed, state, 0, false);
133
+ return state.changed ? JSON.stringify(scrubbed) : text;
134
+ }
135
+ /**
136
+ * A raw provider request or response recorded as JSON (OpenInference `input.value`,
137
+ * `output.value` and `llm.invocation_parameters`), with credentials replaced in its `tools` and
138
+ * `mcp_servers` entries only; nothing else in the value changes.
139
+ */
140
+ function scrubRequestText(text) {
141
+ if (!text.includes('"tools"') && !text.includes('"mcp_servers"'))
142
+ return text;
143
+ const parsed = parse(text);
144
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed))
145
+ return text;
146
+ const state = { changed: false };
147
+ const request = { ...parsed };
148
+ for (const field of ["tools", "mcp_servers"])
149
+ if (request[field] !== null && typeof request[field] === "object")
150
+ request[field] = scrubNode(request[field], state, 0, false);
151
+ return state.changed ? JSON.stringify(request) : text;
152
+ }
153
+ /**
154
+ * Removes hosted-tool credentials from an exported attribute value, before the caller's `redact`.
155
+ * Tool definitions come from OpenTelemetry GenAI (`gen_ai.tool.definitions`), AI SDK 6
156
+ * (`ai.prompt.tools`, one JSON string per tool) and OpenInference (`llm.tools.{i}.tool.json_schema`,
157
+ * plus the raw request in `input.value`). Other attributes, and values that are not JSON, are
158
+ * returned unchanged. Metadata-only export removes all of these attributes anyway.
159
+ *
160
+ * @throws Error when a tool definition is nested too deeply to inspect; the record is then
161
+ * rejected rather than exported with credentials.
162
+ */
163
+ export function scrubToolCredentials(key, value) {
164
+ const scrub = key === "gen_ai.tool.definitions" || key === "ai.prompt.tools" || openInferenceTool.test(key)
165
+ ? scrubDefinitionText
166
+ : key === "input.value" || key === "output.value" || key === "llm.invocation_parameters"
167
+ ? scrubRequestText
168
+ : undefined;
169
+ if (!scrub)
170
+ return value;
171
+ if (typeof value === "string")
172
+ return scrub(value);
173
+ if (Array.isArray(value))
174
+ return value.map((item) => (typeof item === "string" ? scrub(item) : item));
175
+ return value;
176
+ }
177
+ /** A usable tool name: non-blank, at most 256 characters, well-formed and free of NUL. */
178
+ function isName(value) {
179
+ return (typeof value === "string" &&
180
+ value.trim() !== "" &&
181
+ value.length <= 256 &&
182
+ !value.includes("\u0000") &&
183
+ value.isWellFormed());
184
+ }
185
+ /**
186
+ * A definition's name: `name` (GenAI, AI SDK, Responses and Anthropic tools), else Chat
187
+ * Completions' `function.name`, else the `type` of an unnamed built-in tool such as `mcp`.
188
+ */
189
+ function toolName(definition) {
190
+ if (definition === null || typeof definition !== "object" || Array.isArray(definition))
191
+ return undefined;
192
+ const { name, function: fn, type } = definition;
193
+ const nested = fn !== null && typeof fn === "object" ? fn.name : undefined;
194
+ return [name, nested, type].find(isName);
195
+ }
196
+ /**
197
+ * Parses JSON-encoded definitions; `undefined` unless every element is a JSON object or array.
198
+ * Like the Python SDK, which parses on the application thread, definitions longer than one
199
+ * export request are not parsed, so both SDKs summarize the same records.
200
+ */
201
+ function parseDefinitions(texts) {
202
+ const length = texts.reduce((total, text) => total + (typeof text === "string" ? Buffer.byteLength(text, "utf8") : 0), 0);
203
+ if (length > MAX_BODY_BYTES)
204
+ return undefined;
205
+ const parsed = texts.map((text) => (typeof text === "string" ? parse(text) : undefined));
206
+ return parsed.every((item) => item !== null && typeof item === "object") ? parsed : undefined;
207
+ }
208
+ /**
209
+ * The tool definitions a record carries, in order: `gen_ai.tool.definitions` (one JSON list),
210
+ * else AI SDK 6 `ai.prompt.tools` (one JSON string per tool), else OpenInference
211
+ * `llm.tools.{i}.tool.json_schema` ordered by index.
212
+ */
213
+ function definitionsOf(source) {
214
+ const definitions = source["gen_ai.tool.definitions"];
215
+ if (definitions !== undefined) {
216
+ const parsed = parseDefinitions([definitions])?.[0];
217
+ return parsed === undefined ? undefined : Array.isArray(parsed) ? parsed : [parsed];
218
+ }
219
+ const promptTools = source["ai.prompt.tools"];
220
+ if (promptTools !== undefined)
221
+ return parseDefinitions(Array.isArray(promptTools) ? promptTools : [promptTools]);
222
+ const indexed = Object.entries(source)
223
+ .flatMap(([key, value]) => {
224
+ const match = openInferenceTool.exec(key);
225
+ return match ? [[Number(match[1]), value]] : [];
226
+ })
227
+ .sort(([left], [right]) => left - right);
228
+ return indexed.length ? parseDefinitions(indexed.map(([, value]) => value)) : undefined;
229
+ }
230
+ /**
231
+ * RFC 8785 (JCS) canonical JSON: object keys sorted by UTF-16 code units, no whitespace, and
232
+ * ECMAScript number and string serialization. The Python SDK produces the same text.
233
+ */
234
+ function canonicalJson(value) {
235
+ if (Array.isArray(value))
236
+ return `[${value.map(canonicalJson).join(",")}]`;
237
+ if (value !== null && typeof value === "object") {
238
+ const record = value;
239
+ const members = Object.keys(record)
240
+ .sort()
241
+ .map((key) => `${JSON.stringify(key)}:${canonicalJson(record[key])}`);
242
+ return `{${members.join(",")}}`;
243
+ }
244
+ return JSON.stringify(value);
245
+ }
246
+ /**
247
+ * Metadata-only summary of the tool definitions a record carries, which export then removes:
248
+ * `hue.tool.names` lists each definition's name in order, and `hue.tool.definitions.sha256` is
249
+ * the lowercase hex SHA-256 of the RFC 8785 canonical JSON of the credential-scrubbed definition
250
+ * list, so the same catalog has the same digest in both SDKs and across credential rotation.
251
+ * Returns the source unchanged when it has no parseable definitions; attributes the source
252
+ * already sets are kept.
253
+ */
254
+ export function withToolCatalogSummary(source) {
255
+ let summary;
256
+ try {
257
+ const definitions = definitionsOf(source);
258
+ if (definitions === undefined)
259
+ return source;
260
+ const scrubbed = scrubNode(definitions, { changed: false }, 0, false);
261
+ const names = definitions.map(toolName).filter((name) => name !== undefined);
262
+ summary = {
263
+ ...(names.length ? { "hue.tool.names": names } : {}),
264
+ "hue.tool.definitions.sha256": createHash("sha256")
265
+ .update(canonicalJson(scrubbed), "utf8")
266
+ .digest("hex"),
267
+ };
268
+ }
269
+ catch {
270
+ // Metadata-only export removes the definitions whether or not they can be summarized.
271
+ return source;
272
+ }
273
+ return { ...summary, ...source };
274
+ }
package/dist/transport.js CHANGED
@@ -339,8 +339,10 @@ export class HueTransport {
339
339
  }
340
340
  }
341
341
  /** @internal Counts a helper capture or instrumentation failure that preserved application execution. */
342
- instrumentationFailure(signal = "traces", message = "Telemetry capture or instrumentation failed; application execution was preserved") {
343
- this.instrumentationFailures++;
342
+ instrumentationFailure(signal = "traces", message = "Telemetry capture or instrumentation failed; application execution was preserved", count = 1) {
343
+ if (!Number.isSafeInteger(count) || count < 1)
344
+ return;
345
+ this.instrumentationFailures += count;
344
346
  this.issue(signal, "invalid", 0, message);
345
347
  }
346
348
  /** Cumulative counters and current queue gauges. */
package/dist/types.d.ts CHANGED
@@ -139,20 +139,30 @@ export interface SpanOptions {
139
139
  sessionId?: string;
140
140
  /** Recorded as `user.id` on this span and inherited by nested helper spans. */
141
141
  userId?: string;
142
+ /**
143
+ * The application workspace or tenant the work runs in, recorded as `hue.workspace.id` on this
144
+ * span and inherited by nested helper spans.
145
+ */
146
+ workspaceId?: string;
142
147
  /** Recorded as `input.value` when `captureContent` is true; any JSON-encodable value. */
143
148
  input?: unknown;
144
149
  /** Explicit parent context, for example from {@link HueClient.extract}. */
145
150
  parentContext?: Context;
146
151
  }
147
152
  /**
148
- * MCP `initialize` `serverInfo` for {@link HueClient.tool}. Pass
149
- * `client.getServerVersion()` after connect; any MCP server works.
153
+ * MCP `initialize` `serverInfo` for {@link HueClient.tool}, plus the Hue provider and surface
154
+ * when the tool came from one. Pass `client.getServerVersion()` after connect; any MCP server
155
+ * works.
150
156
  */
151
157
  export interface McpServerInfo {
152
158
  /** `serverInfo.name` from MCP initialize, recorded as `mcp.server.name`. */
153
159
  name?: string;
154
160
  /** `serverInfo.version` from MCP initialize, recorded as `mcp.server.version`. */
155
161
  version?: string;
162
+ /** Hue provider id such as `google.gmail`, recorded as `hue.mcp.provider`. */
163
+ provider?: string;
164
+ /** Hue surface such as `google.gmail/mcp`, recorded as `hue.mcp.surface`. */
165
+ surface?: string;
156
166
  }
157
167
  /**
158
168
  * Options for {@link HueClient.tool}. `callId` is the provider-issued tool-call
@@ -168,6 +178,57 @@ export interface ToolOptions extends Pick<SpanOptions, "parentContext"> {
168
178
  */
169
179
  mcp?: McpServerInfo;
170
180
  }
181
+ /** Providers whose hosted tool calls {@link HueClient.recordProviderToolCalls} can read. */
182
+ export type HostedToolProvider = "openai" | "anthropic";
183
+ /** Identity of a hosted MCP server, keyed by the label the provider uses for it. */
184
+ export interface HostedServerInfo {
185
+ /** Server name recorded as `mcp.server.name`; defaults to the provider's label. */
186
+ name?: string;
187
+ /** Server version recorded as `mcp.server.version`. */
188
+ version?: string;
189
+ /** Hue provider id such as `google.gmail`, recorded as `hue.mcp.provider`. */
190
+ provider?: string;
191
+ /** Hue surface such as `google.gmail/mcp`, recorded as `hue.mcp.surface`. */
192
+ surface?: string;
193
+ }
194
+ /** Options for {@link HueClient.recordProviderToolCalls}. */
195
+ export interface ProviderToolCallOptions extends Pick<SpanOptions, "parentContext"> {
196
+ /**
197
+ * `openai` (Responses API) or `anthropic` (Messages API). Defaults to the enclosing `model()`
198
+ * call's `provider` when it is one of these.
199
+ */
200
+ provider?: HostedToolProvider;
201
+ /**
202
+ * The request the response answers. Only hosted MCP server URLs are read from it (OpenAI
203
+ * `tools[].server_url` by `server_label`, Anthropic `mcp_servers[].url` by `name`), to record
204
+ * each server's host as `server.address`.
205
+ */
206
+ request?: unknown;
207
+ /** Identity of each hosted MCP server, by the label the provider uses for it. */
208
+ servers?: Record<string, HostedServerInfo>;
209
+ }
210
+ /**
211
+ * A file the traced work read, received or produced, for {@link HueClient.recordFile}. Files are
212
+ * linked by content hash; their bytes are never exported.
213
+ */
214
+ export interface FileRecord {
215
+ /**
216
+ * `input` was given to the agent, `attachment` arrived from a tool or message, and `output` was
217
+ * produced by the agent.
218
+ */
219
+ role: "input" | "attachment" | "output";
220
+ /** Media type, for example `application/pdf`. */
221
+ mediaType: string;
222
+ /** Hex SHA-256 of the file's bytes; computed from `data` when omitted. */
223
+ sha256?: string;
224
+ /** The file's bytes, only hashed and measured, never exported; a string is hashed as UTF-8.
225
+ * Data larger than 25 MiB is omitted and counted as an instrumentation failure. */
226
+ data?: Uint8Array | string;
227
+ /** Size in bytes; computed from `data` when omitted. */
228
+ byteSize?: number;
229
+ /** File name, recorded as `hue.file.name` only when `captureContent` is true. */
230
+ name?: string;
231
+ }
171
232
  /** Provider-reported token counts for {@link HueSpan.setUsage}. */
172
233
  export interface TokenUsage {
173
234
  /** Provider-reported prompt tokens (`gen_ai.usage.input_tokens`). */
@@ -179,13 +240,25 @@ export interface TokenUsage {
179
240
  * Options for {@link HueClient.model}: GenAI request metadata plus the {@link SpanOptions} that
180
241
  * apply to a client span. `input` is recorded as `gen_ai.input.messages`.
181
242
  */
182
- export interface ModelOptions extends Pick<SpanOptions, "sessionId" | "userId" | "input" | "parentContext"> {
243
+ export interface ModelOptions extends Pick<SpanOptions, "sessionId" | "userId" | "workspaceId" | "input" | "parentContext"> {
183
244
  /** Provider identifier recorded as `gen_ai.provider.name`, for example "openai". */
184
245
  provider: string;
185
246
  /** Recorded as `gen_ai.operation.name`; defaults to "chat". */
186
247
  operation?: string;
187
248
  /** Span name; defaults to "{operation} {model}". */
188
249
  name?: string;
250
+ /**
251
+ * System instructions sent separately from the chat history, recorded as
252
+ * `gen_ai.system_instructions` when `captureContent` is true. Any JSON-encodable value, ideally
253
+ * GenAI semantic-convention parts such as `[{ type: "text", content: "..." }]`.
254
+ */
255
+ systemInstructions?: unknown;
256
+ /**
257
+ * Tool definitions offered to the model, recorded as `gen_ai.tool.definitions` when
258
+ * `captureContent` is true. Any JSON-encodable value, ideally the GenAI shape
259
+ * `[{ type: "function", name, description, parameters }]`.
260
+ */
261
+ tools?: unknown;
189
262
  }
190
263
  /** Value for AI SDK 6's `experimental_telemetry` option; AI SDK 7 uses `hueTelemetry` instead. */
191
264
  export interface ExperimentalTelemetrySettings {
package/dist/version.d.ts CHANGED
@@ -1,2 +1,2 @@
1
1
  /** Package version shared by the instrumentation scope and the export User-Agent. */
2
- export declare const sdkVersion = "0.8.0";
2
+ export declare const sdkVersion = "0.8.1";
package/dist/version.js CHANGED
@@ -1,3 +1,3 @@
1
1
  // Generated by scripts/write-version.mjs from package.json; do not edit by hand.
2
2
  /** Package version shared by the instrumentation scope and the export User-Agent. */
3
- export const sdkVersion = "0.8.0";
3
+ export const sdkVersion = "0.8.1";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hue-run/sdk",
3
- "version": "0.8.0",
3
+ "version": "0.8.1",
4
4
  "private": false,
5
5
  "license": "MIT",
6
6
  "publishConfig": {