@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/CHANGELOG.md CHANGED
@@ -6,6 +6,55 @@ All notable changes to `@combycode/llm-sdk` are documented here. The format foll
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [1.3.0] - 2026-07-06
10
+
11
+ ### Added
12
+ - **Streaming file parity.** `stream()` now surfaces hosted code-execution output files with the
13
+ same coverage as `complete()`: a new `{ type: 'file', file }` `StreamEvent` is emitted as each
14
+ file finalizes mid-stream, and the files are collected onto the streamed final response's
15
+ `files` (via `onCompletion` / the agent final response). New `ProviderAdapter.createStreamParser()`
16
+ returns a per-stream parser so adapters can hold per-stream state — used by Google to route an
17
+ inline code-execution artifact (whose "code ran" marker and bytes arrive in separate SSE events)
18
+ to `files` rather than conversational `media`. Verified live on Anthropic, OpenAI, and Google.
19
+ - **File-content retrieval** for hosted-tool output files. `CompleteResult`, `LLMClient`, and
20
+ the agent result now expose `retrieveFile(file)` → `{ blob, name, mimeType, size }` and
21
+ `streamFile(file)` → `{ stream, name, mimeType, size }`, resolving a `FileOutput`'s inline
22
+ `data`, `url`, or provider file `id` through the same model + key the call used — no
23
+ re-passing credentials. `streamFile` pipes large files to a sink without buffering. Name /
24
+ mime / size come from the download response headers (Content-Disposition / Content-Type /
25
+ Content-Length, with a filename-extension mime fallback). New network `responseType: 'stream'`
26
+ returns the raw body and releases the queue slot immediately. Exports `RetrievedFile`,
27
+ `FileStream`. Auth is sent only to the provider's own host.
28
+
29
+ ## [1.2.0] - 2026-07-05
30
+
31
+ ### Fixed
32
+ - Anthropic hosted code-execution **file outputs** now surface on `response.files` (verified
33
+ live). Three fixes, found by real-key testing: (1) the producer parsed the outdated
34
+ `code_execution_tool_result` shape, but the current `code_execution_20260521` tool emits
35
+ `bash_code_execution_tool_result` → `bash_code_execution_output.file_id` (now both are
36
+ handled); (2) code execution needs the beta endpoint — requests using `code_interpreter` now
37
+ hit `/v1/messages?beta=true`, without which no file outputs are returned; (3) `AgentLoop`'s
38
+ final response dropped `files` (and `moderation`) — both are now propagated from the final
39
+ LLM response in `complete()` and `stream()`.
40
+
41
+ ### Added
42
+ - `AgentLoopConfig.toolInputGuardrails` (`ToolInputGuardrail[]`) — per-tool-call input
43
+ guardrails that validate a call's arguments before the permission/approval check. A trip
44
+ denies just that call (error result to the model) without halting the run or invoking the
45
+ HITL approver; a pass runs the normal permission/approval/execution path.
46
+ - `AgentTool.customDataExtractor` — optional hook to derive out-of-band metadata from a
47
+ successful tool result, attached to that call's `ToolCallReport.customData`. The model never
48
+ sees it (for your own telemetry/routing/audit); a throwing extractor is swallowed.
49
+ - Code-execution **file outputs** now surface on `response.files` across **all** providers
50
+ (completes the channel shipped in 1.1, which had only the Anthropic producer):
51
+ - OpenAI Responses: code-interpreter image outputs (by URL) and downloadable container
52
+ files (`container_file_citation` → file id + name).
53
+ - Google: hosted code-execution `inlineData` artifacts (base64), routed to `files`
54
+ instead of `media` when the turn used code execution.
55
+ - xAI: inherited from the OpenAI Responses adapter.
56
+ - `FileOutput` gains a `url` field (for providers that return a fetchable URL).
57
+
9
58
  ## [1.1.0] - 2026-06-30
10
59
 
11
60
  ### Added
@@ -53,5 +102,6 @@ First public release.
53
102
  - Service tiers end to end (request → bill → cost).
54
103
  - Cross-environment: runs on Node, Bun, and the browser. ESM, zero runtime deps.
55
104
 
105
+ [1.2.0]: https://github.com/combycode/llm-sdk-ts/releases/tag/v1.2.0
56
106
  [1.1.0]: https://github.com/combycode/llm-sdk-ts/releases/tag/v1.1.0
57
107
  [1.0.0]: https://github.com/combycode/llm-sdk-ts/releases/tag/v1.0.0
package/README.md CHANGED
@@ -64,6 +64,28 @@ for await (const ev of llm.stream('Count to 5.')) {
64
64
  }
65
65
  ```
66
66
 
67
+ ### Hosted code execution + output files
68
+
69
+ Run the provider's code interpreter and pull the files it produces (charts, CSVs) — one
70
+ interface, any provider. `retrieveFile` / `streamFile` fetch the bytes plus `name` / `mimeType`
71
+ / `size`, bound to the same model + key:
72
+
73
+ ```ts
74
+ import { complete } from '@combycode/llm-sdk';
75
+
76
+ const { response, retrieveFile } = await complete({
77
+ model: 'anthropic/claude-haiku-4.5',
78
+ apiKey: process.env.ANTHROPIC_API_KEY,
79
+ prompt: 'Plot y = x**2 for x in 1..5, save a PNG, and return the file.',
80
+ tools: [{ type: 'code_interpreter' }],
81
+ maxTokens: 6000,
82
+ });
83
+
84
+ for (const file of response.files ?? []) {
85
+ const { blob, name, mimeType, size } = await retrieveFile(file); // or streamFile for large files
86
+ }
87
+ ```
88
+
67
89
  ## Documentation
68
90
 
69
91
  Full guide pages covering all export groups:
@@ -77,6 +99,8 @@ Full guide pages covering all export groups:
77
99
  - [Cost Tracking + estimate()](./docs/guide/cost.md)
78
100
  - [Observability / Telemetry](./docs/guide/telemetry.md)
79
101
  - [Media / Files / Batch](./docs/guide/media-files-batch.md)
102
+ - [Hosted Code Execution](./docs/guide/code-execution.md)
103
+ - [Retrieving Output Files](./docs/guide/retrieving-files.md)
80
104
  - [MCP (Model Context Protocol)](./docs/guide/mcp.md)
81
105
  - [Context Guard + Permissions + Persistence + Cache](./docs/guide/context-guard.md)
82
106
  - [OpenAI-Compatible Server](./docs/guide/server.md)
@@ -46,6 +46,31 @@ export interface Guardrail {
46
46
  /** Return a decision; throw only for unexpected infrastructure errors. */
47
47
  check(ctx: GuardrailCheckContext): Promise<GuardrailDecision>;
48
48
  }
49
+ /** Context for a tool-input guardrail: the specific tool call about to run. */
50
+ export interface ToolInputGuardrailContext {
51
+ toolName: string;
52
+ arguments: Record<string, unknown>;
53
+ callId: string;
54
+ step: number;
55
+ trace: TraceContext;
56
+ }
57
+ /** A tool-input guardrail decision. Unlike message-level guardrails it does NOT
58
+ * halt the run — a trip denies just this one tool call. */
59
+ export type ToolInputGuardrailDecision = {
60
+ pass: true;
61
+ } | {
62
+ pass: false;
63
+ reason: string;
64
+ };
65
+ /** Validates a tool call's arguments BEFORE it executes (and before any HITL
66
+ * approval interruption). On a trip the call is denied — the model receives the
67
+ * denial reason as an error tool result; the run continues and the approver is
68
+ * never consulted. Arguments are immutable once the model emits them, so a single
69
+ * pre-execution check is sufficient. */
70
+ export interface ToolInputGuardrail {
71
+ name: string;
72
+ check(ctx: ToolInputGuardrailContext): Promise<ToolInputGuardrailDecision> | ToolInputGuardrailDecision;
73
+ }
49
74
  export interface GuardrailTriggeredContext {
50
75
  runId: string;
51
76
  agentId: string;
@@ -5,7 +5,7 @@ import type { CacheConfig, ThinkingConfig } from '../llm/types/request';
5
5
  import type { ConversationHistory } from './history';
6
6
  import type { HistorySnapshot } from './history-types';
7
7
  import type { AgentTool } from './types';
8
- import type { Guardrail } from './guardrail-types';
8
+ import type { Guardrail, ToolInputGuardrail } from './guardrail-types';
9
9
  import type { PermissionPolicy } from '../plugins/permissions/policy';
10
10
  import type { ApprovalRequest, ApprovalDecision } from './approval-types';
11
11
  import type { Persistence } from '../plugins/persistence/types';
@@ -48,6 +48,10 @@ export interface AgentLoopConfig {
48
48
  * output guardrails run after each step's response is produced.
49
49
  * A tripwire decision halts the run with finishReason 'guardrail'. */
50
50
  guardrails?: Guardrail[];
51
+ /** Per-tool-call input guardrails. Each runs against a tool call's arguments
52
+ * BEFORE the permission/approval check and execution. A trip denies just that
53
+ * call (error result to the model) without halting the run or invoking `approve`. */
54
+ toolInputGuardrails?: ToolInputGuardrail[];
51
55
  /** Permission policy wired into the tool-execution path.
52
56
  * Called after lookup, before execution.
53
57
  * 'allow' -> proceed; 'deny' -> tool is blocked (error result to model);
@@ -13,7 +13,8 @@
13
13
  import { HookBus } from '../bus/hook-bus';
14
14
  import { type ContentPart, type Message } from '../llm/types/messages';
15
15
  import type { ExecuteOptions } from '../llm/types/options';
16
- import { type CompletionResponse } from '../llm/types/response';
16
+ import { type CompletionResponse, type FileOutput } from '../llm/types/response';
17
+ import type { FileStream, RetrievedFile } from '../llm/files/retrieve';
17
18
  import type { LLMClient } from '../llm/client';
18
19
  import { ConversationHistory } from './history';
19
20
  import type { AgentLoopSnapshot, AgentRunReport, AgentStreamEvent, AgentTool } from './types';
@@ -39,6 +40,7 @@ export declare class AgentLoop {
39
40
  private _toolTimeout;
40
41
  private _maxSteps;
41
42
  private _guardrails;
43
+ private _toolInputGuardrails;
42
44
  private _policy;
43
45
  private _approve;
44
46
  private _checkpoint;
@@ -51,6 +53,14 @@ export declare class AgentLoop {
51
53
  destroy(): void;
52
54
  /** Model is owned by client. */
53
55
  get model(): string;
56
+ /** Fetch a hosted-tool output file from the run's `response.files` — its bytes
57
+ * as a `Blob` plus `name` / `mimeType` / `size`. Delegates to the underlying
58
+ * client, so it uses this agent's provider + key (same as `retrieveFile` on a
59
+ * `complete()` result). */
60
+ retrieveFile(file: FileOutput): Promise<RetrievedFile>;
61
+ /** Stream a hosted-tool output file (large files) — a `ReadableStream` plus
62
+ * best-effort `name` / `mimeType` / `size`. Delegates to the underlying client. */
63
+ streamFile(file: FileOutput): Promise<FileStream>;
54
64
  get system(): string;
55
65
  set system(v: string);
56
66
  get context(): string;
@@ -39,6 +39,11 @@ export interface AgentTool {
39
39
  definition: Tool;
40
40
  /** Execute the tool. Return string or structured content. */
41
41
  execute: (args: Record<string, unknown>, context: ToolExecutionContext) => Promise<string | ContentPart[]>;
42
+ /** Optional: derive out-of-band metadata from a successful tool result, attached
43
+ * to this call's `ToolCallReport.customData`. The MODEL NEVER SEES IT — it is for
44
+ * your own telemetry/routing/audit. Runs after `execute`; may be async. Errors are
45
+ * swallowed (opt-in convenience must never break the tool result). */
46
+ customDataExtractor?: (result: string | ContentPart[], args: Record<string, unknown>, context: Omit<ToolExecutionContext, 'signal'>) => unknown | Promise<unknown>;
42
47
  }
43
48
  export interface ToolExecutionContext {
44
49
  step: number;
@@ -65,6 +70,9 @@ export interface ToolCallReport {
65
70
  value: number | string | boolean;
66
71
  type: string;
67
72
  }>;
73
+ /** Out-of-band metadata from the tool's `customDataExtractor`, if any. Never sent
74
+ * to the model; present only when an extractor returned a value. */
75
+ customData?: unknown;
68
76
  }
69
77
  export interface StepReport {
70
78
  index: number;
@@ -18,7 +18,8 @@ import type { AgentTool } from '../agent/types';
18
18
  import type { LLMClientConfig } from '../llm/client-config';
19
19
  import type { AudioOptions } from '../llm/types/audio';
20
20
  import type { ContentPart, Message } from '../llm/types/messages';
21
- import type { CompletionResponse } from '../llm/types/response';
21
+ import type { CompletionResponse, FileOutput } from '../llm/types/response';
22
+ import type { FileStream, RetrievedFile } from '../llm/files/retrieve';
22
23
  import type { ProviderName } from '../llm/types/provider';
23
24
  import type { ServiceTier } from '../llm/types/tiers';
24
25
  import type { BuiltinTool } from '../llm/types/tools';
@@ -82,5 +83,11 @@ export interface CompleteResult<T = unknown> {
82
83
  * otherwise `undefined`. The generic on `complete<T>(...)` types this. */
83
84
  parsed?: T;
84
85
  response: CompletionResponse;
86
+ /** Fetch a hosted-tool output file (from `response.files`): bytes (`Blob`) +
87
+ * `name` / `mimeType` / `size` — bound to the SAME model + key this call used. */
88
+ retrieveFile(file: FileOutput): Promise<RetrievedFile>;
89
+ /** Stream a hosted-tool output file (`ReadableStream` + `name` / `mimeType` /
90
+ * `size`) — for large files piped straight to a sink. Same model + key. */
91
+ streamFile(file: FileOutput): Promise<FileStream>;
85
92
  }
86
93
  export declare function complete<T = unknown>(opts: CompleteOptions): Promise<CompleteResult<T>>;