@combycode/llm-sdk 1.1.0 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -2451,7 +2451,7 @@ var QueueState = class {
2451
2451
  });
2452
2452
  this.processed++;
2453
2453
  if (response.ok) {
2454
- const body = await parseResponseBody(response, entry.request.responseType ?? "json");
2454
+ const body = entry.request.responseType === "stream" ? response.body : await parseResponseBody(response, entry.request.responseType ?? "json");
2455
2455
  this.semaphore.release();
2456
2456
  entry.resolve({ status: response.status, headers: resHeaders, body });
2457
2457
  return;
@@ -22354,6 +22354,160 @@ async function* wrapParallel(raw, interval, moderate2) {
22354
22354
  }
22355
22355
  }
22356
22356
 
22357
+ // src/llm/files/retrieve.ts
22358
+ var DEFAULT_BASE = {
22359
+ anthropic: "https://api.anthropic.com",
22360
+ openai: "https://api.openai.com",
22361
+ xai: "https://api.x.ai",
22362
+ google: "https://generativelanguage.googleapis.com",
22363
+ openrouter: "https://openrouter.ai/api"
22364
+ };
22365
+ var OCTET_STREAM = "application/octet-stream";
22366
+ function contentRequest(ctx, file) {
22367
+ const base = ctx.baseURL ?? DEFAULT_BASE[ctx.provider];
22368
+ const id = file.id;
22369
+ if (ctx.provider === "anthropic") {
22370
+ return {
22371
+ url: `${base}/v1/files/${id}/content?beta=true`,
22372
+ headers: {
22373
+ "x-api-key": ctx.apiKey,
22374
+ "anthropic-version": "2023-06-01",
22375
+ "anthropic-beta": "files-api-2025-04-14",
22376
+ accept: "application/binary"
22377
+ }
22378
+ };
22379
+ }
22380
+ if (ctx.provider === "openai" || ctx.provider === "xai" || ctx.provider === "openrouter") {
22381
+ const containerId = file.ref?.containerId;
22382
+ const path = containerId ? `/v1/containers/${containerId}/files/${id}/content` : `/v1/files/${id}/content`;
22383
+ return { url: `${base}${path}`, headers: { authorization: `Bearer ${ctx.apiKey}` } };
22384
+ }
22385
+ if (ctx.provider === "google") {
22386
+ return {
22387
+ url: `${base}/v1beta/files/${id}:download?alt=media`,
22388
+ headers: { "x-goog-api-key": ctx.apiKey }
22389
+ };
22390
+ }
22391
+ throw new Error(`retrieveFile: no file-content endpoint for provider "${ctx.provider}"`);
22392
+ }
22393
+ function providerAuth(ctx, url) {
22394
+ const base = ctx.baseURL ?? DEFAULT_BASE[ctx.provider];
22395
+ if (!url.startsWith(base)) return {};
22396
+ if (ctx.provider === "anthropic") {
22397
+ return { "x-api-key": ctx.apiKey, "anthropic-version": "2023-06-01" };
22398
+ }
22399
+ if (ctx.provider === "google") return { "x-goog-api-key": ctx.apiKey };
22400
+ return { authorization: `Bearer ${ctx.apiKey}` };
22401
+ }
22402
+ async function fetchFileResponse(ctx, file, responseType) {
22403
+ if (file.url) {
22404
+ return ctx.fetch({
22405
+ url: file.url,
22406
+ method: "GET",
22407
+ headers: providerAuth(ctx, file.url),
22408
+ body: void 0,
22409
+ provider: ctx.provider,
22410
+ model: "files",
22411
+ responseType
22412
+ });
22413
+ }
22414
+ if (file.id) {
22415
+ const { url, headers } = contentRequest(ctx, file);
22416
+ return ctx.fetch({
22417
+ url,
22418
+ method: "GET",
22419
+ headers,
22420
+ body: void 0,
22421
+ provider: ctx.provider,
22422
+ model: "files",
22423
+ responseType
22424
+ });
22425
+ }
22426
+ throw new Error("retrieveFile: FileOutput has neither `data`, `url`, nor `id`");
22427
+ }
22428
+ function bytesToBlob(bytes, type) {
22429
+ const ab = new ArrayBuffer(bytes.byteLength);
22430
+ new Uint8Array(ab).set(bytes);
22431
+ return new Blob([ab], { type });
22432
+ }
22433
+ function singleChunkStream(bytes) {
22434
+ return new ReadableStream({
22435
+ start(controller) {
22436
+ controller.enqueue(bytes);
22437
+ controller.close();
22438
+ }
22439
+ });
22440
+ }
22441
+ var EXT_MIME = {
22442
+ png: "image/png",
22443
+ jpg: "image/jpeg",
22444
+ jpeg: "image/jpeg",
22445
+ gif: "image/gif",
22446
+ webp: "image/webp",
22447
+ svg: "image/svg+xml",
22448
+ bmp: "image/bmp",
22449
+ csv: "text/csv",
22450
+ tsv: "text/tab-separated-values",
22451
+ txt: "text/plain",
22452
+ json: "application/json",
22453
+ xml: "application/xml",
22454
+ html: "text/html",
22455
+ md: "text/markdown",
22456
+ pdf: "application/pdf",
22457
+ zip: "application/zip",
22458
+ parquet: "application/vnd.apache.parquet",
22459
+ xlsx: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
22460
+ };
22461
+ function mimeFromName(name) {
22462
+ const ext = name?.includes(".") ? name.split(".").pop()?.toLowerCase() : void 0;
22463
+ return ext ? EXT_MIME[ext] : void 0;
22464
+ }
22465
+ function header(headers, name) {
22466
+ const lower = name.toLowerCase();
22467
+ for (const k in headers) if (k.toLowerCase() === lower) return headers[k];
22468
+ return void 0;
22469
+ }
22470
+ function filenameFromDisposition(cd) {
22471
+ if (!cd) return void 0;
22472
+ const ext = cd.match(/filename\*=[^']*''([^;]+)/i);
22473
+ if (ext) {
22474
+ try {
22475
+ return decodeURIComponent(ext[1].trim());
22476
+ } catch {
22477
+ }
22478
+ }
22479
+ const plain = cd.match(/filename="?([^";]+)"?/i);
22480
+ return plain ? plain[1].trim() : void 0;
22481
+ }
22482
+ async function retrieveFile(file, ctx) {
22483
+ if (file.data) {
22484
+ const blob2 = bytesToBlob(base64ToBytes(file.data), file.mimeType ?? OCTET_STREAM);
22485
+ return { blob: blob2, name: file.name, mimeType: blob2.type, size: blob2.size };
22486
+ }
22487
+ const res = await fetchFileResponse(ctx, file, "arraybuffer");
22488
+ const headers = res.headers ?? {};
22489
+ const name = filenameFromDisposition(header(headers, "content-disposition")) ?? file.name;
22490
+ const mimeType = header(headers, "content-type")?.split(";")[0].trim() || file.mimeType || mimeFromName(name) || OCTET_STREAM;
22491
+ const blob = new Blob([res.body], { type: mimeType });
22492
+ return { blob, name, mimeType, size: blob.size };
22493
+ }
22494
+ async function streamFile(file, ctx) {
22495
+ if (file.data) {
22496
+ const bytes = base64ToBytes(file.data);
22497
+ return { stream: singleChunkStream(bytes), name: file.name, mimeType: file.mimeType, size: bytes.byteLength };
22498
+ }
22499
+ const res = await fetchFileResponse(ctx, file, "stream");
22500
+ const headers = res.headers ?? {};
22501
+ const len = header(headers, "content-length");
22502
+ const name = filenameFromDisposition(header(headers, "content-disposition")) ?? file.name;
22503
+ return {
22504
+ stream: res.body,
22505
+ name,
22506
+ mimeType: header(headers, "content-type")?.split(";")[0].trim() || file.mimeType || mimeFromName(name),
22507
+ size: len ? Number(len) : void 0
22508
+ };
22509
+ }
22510
+
22357
22511
  // src/util/duration.ts
22358
22512
  var UNIT_MS = {
22359
22513
  s: 1e3,
@@ -22596,6 +22750,28 @@ var LLMClient = class {
22596
22750
  else next.output = result;
22597
22751
  return next;
22598
22752
  }
22753
+ // ─── File retrieval (hosted-tool output files) ─────────────────────────
22754
+ retrieveContext() {
22755
+ return {
22756
+ provider: this.provider,
22757
+ apiKey: this.apiKey,
22758
+ fetch: this.fetchFn,
22759
+ baseURL: this.adapter.baseURL()
22760
+ };
22761
+ }
22762
+ /** Fetch a hosted-tool output file (e.g. a code-execution file from
22763
+ * `response.files`): its bytes as a `Blob` plus `name` / `mimeType` / `size`.
22764
+ * Resolves inline `data`, a `url`, or a provider file `id` — all through this
22765
+ * client's provider + auth + engine. */
22766
+ retrieveFile(file) {
22767
+ return retrieveFile(file, this.retrieveContext());
22768
+ }
22769
+ /** Stream a hosted-tool output file: a `ReadableStream<Uint8Array>` plus
22770
+ * best-effort `name` / `mimeType` / `size` (from the response headers). For large
22771
+ * files piped straight to a file / GridFS / HTTP response without buffering. */
22772
+ streamFile(file) {
22773
+ return streamFile(file, this.retrieveContext());
22774
+ }
22599
22775
  /** Submit a request. Returns the parsed CompletionResponse. */
22600
22776
  async complete(input, options = {}) {
22601
22777
  const rawMessages = normalizeInput(input);
@@ -22807,17 +22983,18 @@ var LLMClient = class {
22807
22983
  let usage = emptyUsage();
22808
22984
  let finishReason = "stop";
22809
22985
  let moderationReport;
22986
+ const files = [];
22810
22987
  const fetchStream = this.fetchStreamFn;
22811
- const adapter = this.adapter;
22812
22988
  const queueName = this.queueName;
22813
22989
  const priority = this.priority;
22990
+ const parseStream = this.adapter.createStreamParser();
22814
22991
  async function* rawEvents() {
22815
22992
  for await (const sseEvent of fetchStream(httpReq, {
22816
22993
  queueName,
22817
22994
  priority,
22818
22995
  ctx
22819
22996
  })) {
22820
- for (const ev of adapter.parseStreamEvent(sseEvent)) yield ev;
22997
+ for (const ev of parseStream(sseEvent)) yield ev;
22821
22998
  }
22822
22999
  }
22823
23000
  const mod = options.moderation;
@@ -22855,6 +23032,9 @@ var LLMClient = class {
22855
23032
  case "done":
22856
23033
  finishReason = event.finishReason;
22857
23034
  break;
23035
+ case "file":
23036
+ files.push(event.file);
23037
+ break;
22858
23038
  case "moderation":
22859
23039
  moderationReport = this.mergeModeration(
22860
23040
  moderationReport,
@@ -22877,6 +23057,7 @@ var LLMClient = class {
22877
23057
  toolCalls: [],
22878
23058
  thinking: thinking || null,
22879
23059
  media: [],
23060
+ ...files.length ? { files } : {},
22880
23061
  ...moderationReport ? { moderation: moderationReport } : {},
22881
23062
  latencyMs: performance.now() - start,
22882
23063
  raw: null
@@ -23137,6 +23318,22 @@ function anthropicRequestTier(t) {
23137
23318
  function anthropicBilledTier(raw) {
23138
23319
  return typeof raw === "string" && raw ? { serviceTier: raw, pricingTier: raw } : {};
23139
23320
  }
23321
+ function filesFromCodeExecBlock(block) {
23322
+ if (block.type !== "bash_code_execution_tool_result" && block.type !== "code_execution_tool_result") {
23323
+ return [];
23324
+ }
23325
+ const result = block.content;
23326
+ if (!result || result.type !== "bash_code_execution_result" && result.type !== "code_execution_result" || !Array.isArray(result.content)) {
23327
+ return [];
23328
+ }
23329
+ const files = [];
23330
+ for (const out of result.content) {
23331
+ if ((out.type === "bash_code_execution_output" || out.type === "code_execution_output") && typeof out.file_id === "string") {
23332
+ files.push({ id: out.file_id, source: "code_execution" });
23333
+ }
23334
+ }
23335
+ return files;
23336
+ }
23140
23337
  var AnthropicAdapter = class {
23141
23338
  name = "anthropic";
23142
23339
  apiKey;
@@ -23232,7 +23429,10 @@ var AnthropicAdapter = class {
23232
23429
  });
23233
23430
  const headers = {};
23234
23431
  if (hasFileRef) headers["anthropic-beta"] = "files-api-2025-04-14";
23235
- return { body, headers };
23432
+ const usesCodeExec = req.tools?.some(
23433
+ (t) => !isFunctionTool(t) && t.type === "code_interpreter"
23434
+ );
23435
+ return { body, headers, ...usesCodeExec ? { path: "/v1/messages?beta=true" } : {} };
23236
23436
  }
23237
23437
  enableStreaming(providerReq, _req) {
23238
23438
  providerReq.body.stream = true;
@@ -23314,15 +23514,8 @@ var AnthropicAdapter = class {
23314
23514
  };
23315
23515
  content.push(tc);
23316
23516
  toolCalls.push(tc);
23317
- } else if (block.type === "code_execution_tool_result") {
23318
- const result = block.content;
23319
- if (result?.type === "code_execution_result" && Array.isArray(result.content)) {
23320
- for (const out of result.content) {
23321
- if (out.type === "code_execution_output" && typeof out.file_id === "string") {
23322
- files.push({ id: out.file_id, source: "code_execution" });
23323
- }
23324
- }
23325
- }
23517
+ } else {
23518
+ files.push(...filesFromCodeExecBlock(block));
23326
23519
  }
23327
23520
  }
23328
23521
  const finishReason = extractFinishReason(toolCalls.length > 0, r.stop_reason, {
@@ -23361,6 +23554,8 @@ var AnthropicAdapter = class {
23361
23554
  if (block.type === "tool_use") {
23362
23555
  return [{ type: "tool_call_start", id: block.id, name: block.name }];
23363
23556
  }
23557
+ const files = filesFromCodeExecBlock(block);
23558
+ if (files.length) return files.map((file) => ({ type: "file", file }));
23364
23559
  }
23365
23560
  if (type === "content_block_stop") {
23366
23561
  }
@@ -23384,6 +23579,11 @@ var AnthropicAdapter = class {
23384
23579
  }
23385
23580
  return [];
23386
23581
  }
23582
+ /** Stateless — every event is self-contained (code-execution result blocks
23583
+ * arrive complete in a single content_block_start). */
23584
+ createStreamParser() {
23585
+ return (event) => this.parseStreamEvent(event);
23586
+ }
23387
23587
  parseUsage(u) {
23388
23588
  if (!u) return emptyUsage();
23389
23589
  const inputTokens = u.input_tokens ?? 0;
@@ -23826,7 +24026,9 @@ var GoogleAdapter = class {
23826
24026
  const content = [];
23827
24027
  const toolCalls = [];
23828
24028
  const media = [];
24029
+ const files = [];
23829
24030
  let thinking = null;
24031
+ const hasCodeExec = parts.some((p) => p.executableCode || p.codeExecutionResult);
23830
24032
  for (const part of parts) {
23831
24033
  if (part.text !== void 0 && !part.thought) {
23832
24034
  content.push({ type: "text", text: part.text });
@@ -23837,7 +24039,9 @@ var GoogleAdapter = class {
23837
24039
  if (part.inlineData) {
23838
24040
  const inline = part.inlineData;
23839
24041
  const mime = inline.mimeType;
23840
- if (mime.startsWith("image/")) {
24042
+ if (hasCodeExec) {
24043
+ files.push({ data: inline.data, mimeType: mime, source: "code_execution" });
24044
+ } else if (mime.startsWith("image/")) {
23841
24045
  const p = {
23842
24046
  type: "image_output",
23843
24047
  mediaId: "",
@@ -23898,11 +24102,24 @@ var GoogleAdapter = class {
23898
24102
  toolCalls,
23899
24103
  thinking,
23900
24104
  media,
24105
+ ...files.length ? { files } : {},
23901
24106
  latencyMs,
23902
24107
  raw
23903
24108
  };
23904
24109
  }
23905
24110
  parseStreamEvent(event) {
24111
+ return this.streamEvents(event, { codeExec: false });
24112
+ }
24113
+ /** Stateful — Google splits the code-execution marker (`executableCode` /
24114
+ * `codeExecutionResult`) and the produced file (`inlineData`) across parts and
24115
+ * often across SSE events. The closure remembers "code execution began in this
24116
+ * stream" so a later `inlineData` blob is routed to `files` (a code-exec
24117
+ * artifact) rather than `media` (conversational output). */
24118
+ createStreamParser() {
24119
+ const state = { codeExec: false };
24120
+ return (event) => this.streamEvents(event, state);
24121
+ }
24122
+ streamEvents(event, state) {
23906
24123
  const data = JSON.parse(event.data);
23907
24124
  const candidates = data.candidates ?? [];
23908
24125
  const candidate = candidates[0];
@@ -23916,6 +24133,9 @@ var GoogleAdapter = class {
23916
24133
  const rawContent = candidate.content ?? {};
23917
24134
  const parts = rawContent.parts ?? [];
23918
24135
  const events = [];
24136
+ for (const part of parts) {
24137
+ if (part.executableCode || part.codeExecutionResult) state.codeExec = true;
24138
+ }
23919
24139
  for (const part of parts) {
23920
24140
  if (part.text !== void 0 && !part.thought)
23921
24141
  events.push({ type: "text", text: part.text });
@@ -23923,10 +24143,17 @@ var GoogleAdapter = class {
23923
24143
  if (part.inlineData) {
23924
24144
  const inline = part.inlineData;
23925
24145
  const mime = inline.mimeType;
23926
- const mediaType = mime.startsWith("image/") ? "image" : mime.startsWith("audio/") ? "audio" : "video";
23927
- events.push({ type: "media_start", mediaType, mimeType: mime });
23928
- events.push({ type: "media_chunk", data: inline.data });
23929
- events.push({ type: "media_end" });
24146
+ if (state.codeExec) {
24147
+ events.push({
24148
+ type: "file",
24149
+ file: { data: inline.data, mimeType: mime, source: "code_execution" }
24150
+ });
24151
+ } else {
24152
+ const mediaType = mime.startsWith("image/") ? "image" : mime.startsWith("audio/") ? "audio" : "video";
24153
+ events.push({ type: "media_start", mediaType, mimeType: mime });
24154
+ events.push({ type: "media_chunk", data: inline.data });
24155
+ events.push({ type: "media_end" });
24156
+ }
23930
24157
  }
23931
24158
  if (part.functionCall) {
23932
24159
  const fc = part.functionCall;
@@ -24221,6 +24448,10 @@ var GoogleInteractionsAdapter = class {
24221
24448
  }
24222
24449
  return events;
24223
24450
  }
24451
+ /** Stateless — the Interactions adapter surfaces no code-execution file outputs. */
24452
+ createStreamParser() {
24453
+ return (event) => this.parseStreamEvent(event);
24454
+ }
24224
24455
  parseUsage(u) {
24225
24456
  if (!u) return emptyUsage();
24226
24457
  const input = u.total_input_tokens ?? u.prompt_tokens ?? 0;
@@ -25186,6 +25417,10 @@ var OpenAIAdapter = class {
25186
25417
  }
25187
25418
  return events;
25188
25419
  }
25420
+ /** Stateless — Chat Completions has no hosted code-execution file outputs. */
25421
+ createStreamParser() {
25422
+ return (event) => this.parseStreamEvent(event);
25423
+ }
25189
25424
  parseUsage(u) {
25190
25425
  if (!u) return emptyUsage();
25191
25426
  const input = u.prompt_tokens ?? u.input_tokens ?? 0;
@@ -25701,6 +25936,33 @@ function filenameForMime(mimeType) {
25701
25936
  if (mimeType.startsWith("image/")) return `file.${mimeType.slice("image/".length)}`;
25702
25937
  return "file.bin";
25703
25938
  }
25939
+ function filesFromResponsesOutputItem(item) {
25940
+ const files = [];
25941
+ const type = item.type;
25942
+ if (type === "message") {
25943
+ for (const c of item.content ?? []) {
25944
+ if (c.type !== "output_text") continue;
25945
+ for (const a of c.annotations ?? []) {
25946
+ if (a.type === "container_file_citation" && typeof a.file_id === "string") {
25947
+ files.push({
25948
+ id: a.file_id,
25949
+ ...typeof a.filename === "string" ? { name: a.filename } : {},
25950
+ ...typeof a.container_id === "string" ? { ref: { containerId: a.container_id } } : {},
25951
+ source: "code_execution"
25952
+ });
25953
+ }
25954
+ }
25955
+ }
25956
+ }
25957
+ if (type === "code_interpreter_call") {
25958
+ for (const out of item.outputs ?? []) {
25959
+ if (out.type === "image" && typeof out.url === "string") {
25960
+ files.push({ url: out.url, source: "code_execution" });
25961
+ }
25962
+ }
25963
+ }
25964
+ return files;
25965
+ }
25704
25966
  var OpenAIResponsesAdapter = class {
25705
25967
  name = "openai";
25706
25968
  apiKey;
@@ -25876,10 +26138,12 @@ var OpenAIResponsesAdapter = class {
25876
26138
  const content = [];
25877
26139
  const toolCalls = [];
25878
26140
  const media = [];
26141
+ const files = [];
25879
26142
  let thinking = null;
25880
26143
  let text = "";
25881
26144
  for (const item of output) {
25882
26145
  const type = item.type;
26146
+ files.push(...filesFromResponsesOutputItem(item));
25883
26147
  if (type === "message") {
25884
26148
  const itemContent = item.content ?? [];
25885
26149
  for (const c of itemContent) {
@@ -25939,6 +26203,7 @@ var OpenAIResponsesAdapter = class {
25939
26203
  toolCalls,
25940
26204
  thinking,
25941
26205
  media,
26206
+ ...files.length ? { files } : {},
25942
26207
  ...moderation ? { moderation } : {},
25943
26208
  latencyMs,
25944
26209
  raw
@@ -25980,6 +26245,7 @@ var OpenAIResponsesAdapter = class {
25980
26245
  }
25981
26246
  if (type === "response.output_item.done") {
25982
26247
  const item = data.item;
26248
+ for (const file of filesFromResponsesOutputItem(item)) events.push({ type: "file", file });
25983
26249
  if (item?.type === "function_call") {
25984
26250
  events.push({ type: "tool_call_end", id: item.call_id ?? "" });
25985
26251
  }
@@ -26010,6 +26276,11 @@ var OpenAIResponsesAdapter = class {
26010
26276
  }
26011
26277
  return events;
26012
26278
  }
26279
+ /** Stateless — each output item finalizes with all its file annotations in a
26280
+ * single response.output_item.done event. */
26281
+ createStreamParser() {
26282
+ return (event) => this.parseStreamEvent(event);
26283
+ }
26013
26284
  parseUsage(u) {
26014
26285
  if (!u) return emptyUsage();
26015
26286
  const input = u.input_tokens ?? 0;
@@ -27665,6 +27936,7 @@ var AgentLoop = class _AgentLoop {
27665
27936
  _toolTimeout;
27666
27937
  _maxSteps;
27667
27938
  _guardrails;
27939
+ _toolInputGuardrails;
27668
27940
  _policy;
27669
27941
  _approve;
27670
27942
  _checkpoint;
@@ -27692,6 +27964,7 @@ var AgentLoop = class _AgentLoop {
27692
27964
  this._toolTimeout = config.toolTimeout ?? DEFAULT_TOOL_TIMEOUT_MS;
27693
27965
  this._maxSteps = config.maxSteps !== void 0 && config.maxSteps > 0 ? config.maxSteps : DEFAULT_MAX_STEPS;
27694
27966
  this._guardrails = config.guardrails ?? [];
27967
+ this._toolInputGuardrails = config.toolInputGuardrails ?? [];
27695
27968
  this._policy = config.policy ?? null;
27696
27969
  this._approve = config.approve ?? null;
27697
27970
  this._checkpoint = config.checkpoint ?? null;
@@ -27729,6 +28002,19 @@ var AgentLoop = class _AgentLoop {
27729
28002
  get model() {
27730
28003
  return this.client.model;
27731
28004
  }
28005
+ // ─── File retrieval (hosted-tool output files) ──────────────────────────
28006
+ /** Fetch a hosted-tool output file from the run's `response.files` — its bytes
28007
+ * as a `Blob` plus `name` / `mimeType` / `size`. Delegates to the underlying
28008
+ * client, so it uses this agent's provider + key (same as `retrieveFile` on a
28009
+ * `complete()` result). */
28010
+ retrieveFile(file) {
28011
+ return this.client.retrieveFile(file);
28012
+ }
28013
+ /** Stream a hosted-tool output file (large files) — a `ReadableStream` plus
28014
+ * best-effort `name` / `mimeType` / `size`. Delegates to the underlying client. */
28015
+ streamFile(file) {
28016
+ return this.client.streamFile(file);
28017
+ }
27732
28018
  get system() {
27733
28019
  return this._system;
27734
28020
  }
@@ -27910,6 +28196,10 @@ var AgentLoop = class _AgentLoop {
27910
28196
  toolCalls: lastResponse?.toolCalls ?? [],
27911
28197
  thinking: lastResponse?.thinking ?? null,
27912
28198
  media: lastResponse?.media ?? [],
28199
+ // Propagate hosted-tool file outputs + inline-moderation result from the final
28200
+ // LLM response (e.g. code-execution files produced during the run).
28201
+ ...lastResponse?.files ? { files: lastResponse.files } : {},
28202
+ ...lastResponse?.moderation ? { moderation: lastResponse.moderation } : {},
27913
28203
  latencyMs: performance.now() - startPerf,
27914
28204
  raw: lastResponse?.raw ?? null
27915
28205
  };
@@ -28104,6 +28394,8 @@ var AgentLoop = class _AgentLoop {
28104
28394
  toolCalls: lastResponse?.toolCalls ?? [],
28105
28395
  thinking: lastResponse?.thinking ?? null,
28106
28396
  media: [],
28397
+ ...lastResponse?.files ? { files: lastResponse.files } : {},
28398
+ ...lastResponse?.moderation ? { moderation: lastResponse.moderation } : {},
28107
28399
  latencyMs: performance.now() - startPerf,
28108
28400
  raw: null
28109
28401
  };
@@ -28190,6 +28482,18 @@ var AgentLoop = class _AgentLoop {
28190
28482
  runTrace
28191
28483
  );
28192
28484
  if (!lookup.found) return lookup.errorResult;
28485
+ for (const g of this._toolInputGuardrails) {
28486
+ const decision = await g.check({
28487
+ toolName: tc.name,
28488
+ arguments: tc.arguments,
28489
+ callId: tc.id,
28490
+ step,
28491
+ trace: { sessionId: runTrace.sessionId, requestId: runTrace.requestId, callId: tc.id }
28492
+ });
28493
+ if (!decision.pass) {
28494
+ return this.buildDeniedResult(tc, decision.reason, reports);
28495
+ }
28496
+ }
28193
28497
  if (this._policy !== null) {
28194
28498
  const target = {
28195
28499
  kind: "tool",
@@ -28206,7 +28510,7 @@ var AgentLoop = class _AgentLoop {
28206
28510
  try {
28207
28511
  const baseCtx = { step, callId: tc.id, metrics, trace: { sessionId: runTrace.sessionId, requestId: runTrace.requestId, callId: tc.id } };
28208
28512
  const result = await executeWithTimeout(lookup.tool, tc, baseCtx, this._toolTimeout);
28209
- return await this.buildSuccessResult(tc, result, runId, step, metrics, reports, toolStart, runTrace);
28513
+ return await this.buildSuccessResult(tc, result, runId, step, metrics, reports, toolStart, runTrace, lookup.tool, baseCtx);
28210
28514
  } catch (e) {
28211
28515
  return handleToolError(e, tc, this.hooks, runId, this.id, step, metrics, reports, toolStart, runTrace);
28212
28516
  }
@@ -28297,7 +28601,7 @@ var AgentLoop = class _AgentLoop {
28297
28601
  try {
28298
28602
  const baseCtx = { step, callId: tc.id, metrics, trace: { sessionId: runTrace.sessionId, requestId: runTrace.requestId, callId: tc.id } };
28299
28603
  const result = await executeWithTimeout(tool, tc, baseCtx, this._toolTimeout);
28300
- return await this.buildSuccessResult(tc, result, runId, step, metrics, reports, toolStart, runTrace);
28604
+ return await this.buildSuccessResult(tc, result, runId, step, metrics, reports, toolStart, runTrace, tool, baseCtx);
28301
28605
  } catch (e) {
28302
28606
  return handleToolError(e, tc, this.hooks, runId, this.id, step, metrics, reports, toolStart, runTrace);
28303
28607
  }
@@ -28309,9 +28613,16 @@ var AgentLoop = class _AgentLoop {
28309
28613
  return this.buildDeniedResult(tc, denyMsg, reports);
28310
28614
  }
28311
28615
  /** Emit onToolCallComplete, push report, and return success content part. */
28312
- async buildSuccessResult(tc, result, runId, step, metrics, reports, toolStart, runTrace) {
28616
+ async buildSuccessResult(tc, result, runId, step, metrics, reports, toolStart, runTrace, tool, context) {
28313
28617
  const latencyMs = performance.now() - toolStart;
28314
28618
  const resultStr = typeof result === "string" ? result : JSON.stringify(result);
28619
+ let customData;
28620
+ if (tool?.customDataExtractor && context) {
28621
+ try {
28622
+ customData = await tool.customDataExtractor(result, tc.arguments, context);
28623
+ } catch {
28624
+ }
28625
+ }
28315
28626
  await this.hooks.emit("onToolCallComplete", {
28316
28627
  runId,
28317
28628
  agentId: this.id,
@@ -28333,7 +28644,8 @@ var AgentLoop = class _AgentLoop {
28333
28644
  latencyMs,
28334
28645
  skipped: false,
28335
28646
  error: null,
28336
- metrics: Object.fromEntries(metrics)
28647
+ metrics: Object.fromEntries(metrics),
28648
+ ...customData !== void 0 ? { customData } : {}
28337
28649
  });
28338
28650
  return { type: "tool_result", id: tc.id, content: resultStr };
28339
28651
  }
@@ -34337,7 +34649,13 @@ async function complete(opts) {
34337
34649
  serviceTier
34338
34650
  });
34339
34651
  }
34340
- const result = { text: res.text, response: res };
34652
+ const result = {
34653
+ text: res.text,
34654
+ response: res,
34655
+ // Bound to this call's client (same provider/model/key/engine).
34656
+ retrieveFile: (file) => llm.retrieveFile(file),
34657
+ streamFile: (file) => llm.streamFile(file)
34658
+ };
34341
34659
  if (opts.structured?.schema) {
34342
34660
  result.parsed = parseStructured(res.text);
34343
34661
  }
@@ -18,10 +18,11 @@
18
18
  * Hooks emitted: onClientCreate (in ctor), onMessageResolve, onBeforeSubmit,
19
19
  * onCompletion, onClientDestroy. */
20
20
  import type { HookBus } from '../bus/hook-bus';
21
+ import type { FileStream, RetrievedFile } from './files/retrieve';
21
22
  import type { ContentPart, Message } from './types/messages';
22
23
  import type { ExecuteOptions } from './types/options';
23
24
  import type { ApiType, ProviderName } from './types/provider';
24
- import type { CompletionResponse } from './types/response';
25
+ import type { CompletionResponse, FileOutput } from './types/response';
25
26
  import type { StreamEvent } from './types/stream';
26
27
  import type { LLMClientConfig } from './client-config';
27
28
  export declare class LLMClient {
@@ -67,6 +68,16 @@ export declare class LLMClient {
67
68
  private moderationConfig;
68
69
  /** Fold a moderation stream event into the accumulating report. */
69
70
  private mergeModeration;
71
+ private retrieveContext;
72
+ /** Fetch a hosted-tool output file (e.g. a code-execution file from
73
+ * `response.files`): its bytes as a `Blob` plus `name` / `mimeType` / `size`.
74
+ * Resolves inline `data`, a `url`, or a provider file `id` — all through this
75
+ * client's provider + auth + engine. */
76
+ retrieveFile(file: FileOutput): Promise<RetrievedFile>;
77
+ /** Stream a hosted-tool output file: a `ReadableStream<Uint8Array>` plus
78
+ * best-effort `name` / `mimeType` / `size` (from the response headers). For large
79
+ * files piped straight to a file / GridFS / HTTP response without buffering. */
80
+ streamFile(file: FileOutput): Promise<FileStream>;
70
81
  /** Submit a request. Returns the parsed CompletionResponse. */
71
82
  complete(input: string | ContentPart[] | Message[], options?: ExecuteOptions): Promise<CompletionResponse>;
72
83
  /** Run `complete` with a JSON Schema enforced via `structured`. Strips any
@@ -0,0 +1,44 @@
1
+ /** Retrieve the bytes of a hosted-tool output file (e.g. code-execution files).
2
+ *
3
+ * A `FileOutput` may carry inline base64 `data` (Google), a `url` (OpenAI
4
+ * code-interpreter images), or an `id` fetched from the provider's files API
5
+ * (Anthropic, OpenAI container files). These helpers resolve all three into
6
+ * bytes — buffered (`retrieveFile` → Blob) or streamed (`streamFile` →
7
+ * ReadableStream, for large files piped straight to a sink).
8
+ *
9
+ * All HTTP flows through the injected `EngineFetch` (auth, queue, cost, traces). */
10
+ import type { EngineFetch } from '../../network/types';
11
+ import type { ProviderName } from '../types/provider';
12
+ import type { FileOutput } from '../types/response';
13
+ export interface RetrieveContext {
14
+ provider: ProviderName;
15
+ apiKey: string;
16
+ fetch: EngineFetch;
17
+ /** Provider API base URL; defaults per provider when omitted. */
18
+ baseURL?: string;
19
+ }
20
+ /** A retrieved file's bytes plus the metadata an end user needs to display and
21
+ * open it as an attachment (correct name + type). */
22
+ export interface RetrievedFile {
23
+ blob: Blob;
24
+ /** Filename (from Content-Disposition, else the FileOutput name). */
25
+ name?: string;
26
+ /** MIME type (from Content-Type, else the FileOutput mime). */
27
+ mimeType: string;
28
+ /** Size in bytes. */
29
+ size: number;
30
+ }
31
+ /** A streamed file: its byte stream plus best-effort metadata (from the response
32
+ * headers). The stream itself carries only bytes — name/type/size come from here. */
33
+ export interface FileStream {
34
+ stream: ReadableStream<Uint8Array>;
35
+ name?: string;
36
+ mimeType?: string;
37
+ /** From Content-Length, when the provider sends it. */
38
+ size?: number;
39
+ }
40
+ /** Fetch the whole file as bytes (buffered) plus its name/type/size. */
41
+ export declare function retrieveFile(file: FileOutput, ctx: RetrieveContext): Promise<RetrievedFile>;
42
+ /** Stream the file's bytes (un-buffered) plus best-effort name/type/size. Pipe the
43
+ * stream straight to a file, GridFS, or an HTTP response — nothing is buffered. */
44
+ export declare function streamFile(file: FileOutput, ctx: RetrieveContext): Promise<FileStream>;