@inferencesh/sdk 0.21.0 → 0.22.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.
Files changed (57) hide show
  1. package/CHANGELOG.md +53 -0
  2. package/dist/agent/api.d.ts +35 -1
  3. package/dist/agent/api.js +43 -7
  4. package/dist/agent/reducer.js +23 -7
  5. package/dist/agent/types.d.ts +29 -4
  6. package/dist/api/agents.d.ts +9 -3
  7. package/dist/api/agents.js +87 -63
  8. package/dist/api/api-keys.d.ts +2 -5
  9. package/dist/api/apps.d.ts +9 -2
  10. package/dist/api/apps.js +9 -1
  11. package/dist/api/chats.d.ts +13 -3
  12. package/dist/api/chats.js +13 -2
  13. package/dist/api/engines.d.ts +4 -2
  14. package/dist/api/engines.js +3 -2
  15. package/dist/api/flow-runs.d.ts +7 -2
  16. package/dist/api/flow-runs.js +7 -2
  17. package/dist/api/flows.d.ts +13 -2
  18. package/dist/api/flows.js +15 -1
  19. package/dist/api/knowledge.d.ts +7 -4
  20. package/dist/api/knowledge.js +6 -4
  21. package/dist/api/mcp-servers.d.ts +2 -1
  22. package/dist/api/projects.d.ts +5 -3
  23. package/dist/api/projects.js +3 -2
  24. package/dist/api/secrets.d.ts +2 -2
  25. package/dist/api/tasks.js +38 -56
  26. package/dist/api/teams.d.ts +4 -2
  27. package/dist/api/teams.js +3 -2
  28. package/dist/api/write-bodies.d.ts +110 -0
  29. package/dist/api/write-bodies.js +1 -0
  30. package/dist/http/client.d.ts +1 -1
  31. package/dist/http/client.js +2 -1
  32. package/dist/http/loopback.d.ts +8 -0
  33. package/dist/http/loopback.js +18 -0
  34. package/dist/http/stream-until-terminal.d.ts +55 -0
  35. package/dist/http/stream-until-terminal.js +88 -0
  36. package/dist/http/stream.d.ts +1 -0
  37. package/dist/http/stream.js +7 -3
  38. package/dist/index.d.ts +2 -0
  39. package/dist/index.js +1 -0
  40. package/dist/proxy/endpoints.json +156 -0
  41. package/dist/proxy/express.d.ts +5 -6
  42. package/dist/proxy/express.js +18 -2
  43. package/dist/proxy/hono.d.ts +5 -3
  44. package/dist/proxy/hono.js +4 -3
  45. package/dist/proxy/index.d.ts +165 -6
  46. package/dist/proxy/index.js +409 -25
  47. package/dist/proxy/nextjs.d.ts +29 -17
  48. package/dist/proxy/nextjs.js +72 -62
  49. package/dist/proxy/remix.d.ts +5 -3
  50. package/dist/proxy/remix.js +4 -3
  51. package/dist/proxy/svelte.d.ts +5 -4
  52. package/dist/proxy/svelte.js +4 -4
  53. package/dist/types.d.ts +423 -6
  54. package/dist/types.js +111 -0
  55. package/dist/version.d.ts +1 -0
  56. package/dist/version.js +7 -0
  57. package/package.json +2 -1
package/CHANGELOG.md CHANGED
@@ -7,6 +7,59 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.22.1] - 2026-10-10
11
+
12
+ ### Fixed
13
+
14
+ - `sendMessage` sends its attachments: it posted only the text after uploading the files, so attachments never reached the agent. It now posts the file refs as `attachments` on POST /chats/{id}/messages. Needs api-v1227+; an older API ignores the field.
15
+
16
+ ## [0.22.0] - 2026-10-10
17
+
18
+ Breaking: the server proxy forwards only app runs and agent chats and refuses every other API call with 403 (see Security); `update()` on projects, knowledge, skills, chats, engines and teams sends PATCH and needs an API that answers PATCH (see Changed); the Next.js proxy handler has no `PUT`.
19
+
20
+ ### Added
21
+
22
+ - Server proxy options (`@inferencesh/sdk/proxy` and every adapter): `allowedEndpoints` (`namespace/name` globs, e.g. `"ana/helper"`, `"ana/*"`) limits which apps and agents visitors may run; `isAuthenticated(request)` decides per request whether the API key may be used (401 otherwise) and receives the framework's request; `allowUnauthorizedRequests` (default `true`) set to `false` refuses every request unless `isAuthenticated` is given. `@inferencesh/sdk/proxy/nextjs` gains `createHandler(options)` (App Router) and `createPageHandler(options)` (Page Router); `handlers` and `pageHandler` are those with the default options. `PROXY_ENDPOINTS` lists what the proxy forwards. `apiUrl` (default `https://api.inference.sh`) names the API the frontend's client calls; `maxUploadBytes` (default 100 MiB) caps a file uploaded through the proxy.
23
+ - Server proxy: `endpoints` forwards a subset of `PROXY_ENDPOINTS`, built with `pickProxyEndpoint("POST /chats/{id}/messages", { onlyFields, validate })`, which can narrow an entry (fewer body fields, an extra body check) but never widen it; `matchProxyEndpoint(method, path)` finds the entry a request takes. `exactRefs: true` matches `allowedEndpoints` entries against the whole ref, version included ("ana/helper@v2" lets only that version through). `isLoopbackHost` / `isHttpsOrLoopback` export the proxy's https-or-loopback rule.
24
+ - Server proxy: `PROXY_ENDPOINTS` is built from the API's proxy manifest (`src/proxy/endpoints.json`, generated by go/api from its route table for the run-only preset and copied by its `make types`), exported as `PROXY_MANIFEST`. Each entry gains `route` ("POST /chats/{id}/messages") and `scopes` (what the site's key must hold for it); `PROXY_ENDPOINT_RULES` holds the proxy's own body checks by route. `pickProxyEndpoint` takes the manifest route, any placeholder names.
25
+ - `AppsAPI.patch(appId, AppPatchBody)`: PATCH /apps/{id}, changes the app's copy fields sent and keeps the others. `update()` stays POST, since it also edits the version in place.
26
+ - `ChatsAPI.updateSettings()` (POST /chats/{id}/settings: name, visibility, tool approval, hooks, memory), `FlowsAPI.patch()` (PATCH /flows/{id}, only the fields sent) and `FlowsAPI.saveViewport()` (POST /flows/{id}/viewport). `HttpClient` takes `'patch'`.
27
+ - Write body types (`AgentUpdateBody`, `AppUpdateBody`, `ChatUpdateBody`, `EngineUpdateBody`, `FlowUpdateBody`, `FlowPatchBody`, `FlowViewportBody`, `ProjectWriteBody`, `KnowledgeUpdateBody`, `TeamUpdateBody`, `MCPServerUpdateBody`) name the fields each route writes. The update methods take them in place of `Partial<DTO>`: a loaded DTO spread into the call still type-checks; an explicit field the route drops does not.
28
+ - `ApiKeysAPI.create()` takes the generated `CreateApiKeyRequest`, so `scope` (personal or workspace key) and `expires_at` type-check. Existing `{ name, scopes }` calls are unchanged.
29
+ - `ProxyAdapter` takes an optional `signal` that the proxy passes to the upstream fetch.
30
+ - Types: `ScopeRefusedMeta` (`required_scope`, the meta of an `insufficient_scope` or `admin_scopes_not_approved` error), `ErrorCodeAdminScopesNotApproved` and the other admin session error codes, `AgentVersionDTO.visitor_reads` (the scope groups a publication of the agent answers), `ScopesResponse.account_scopes` and `login_preset`, `AppSandbox` on `AppVersionDTO`.
31
+
32
+ ### Changed
33
+
34
+ - `update()` on `ProjectsAPI`, `KnowledgeAPI`, `SkillsAPI`, `ChatsAPI`, `EnginesAPI` and `TeamsAPI` sends PATCH instead of POST: a field left out of the body keeps its value. With POST the API wrote every field left out as empty. Needs an API that answers PATCH on these routes; an older API answers 405.
35
+ - `@inferencesh/sdk/proxy/nextjs` `createHandler()` (and `handlers`) return `{ GET, POST }`: the `PUT` handler is gone, since the proxy forwards no PUT call and answered every one with 403. Drop `PUT` from `export const { GET, POST, PUT } = ...`; Next.js now answers PUT with 405.
36
+
37
+ - Tool approvals (`approveTool`, `rejectTool`, `alwaysAllowTool`, `resolveInterrupt`) and widening a chat's approval settings document that they need the account holder's own sign-in: with an API key, an app's token or the server proxy the API answers 403 `requires_sign_in` (`ErrorCodeRequiresSignIn`).
38
+ - Types regenerated from the API: admin elevation types (`AdminElevate*`, `AdminElevation*`), `MeResponse.platform_power` and `scopes`, scope metadata (`sign_in_only`, `not_for_apps`, `costs_credits`, `automation`, `public_rows`, `staff_only`), `ScopePreset.default` and `grants`, `ApiKeyDTO.grants`, the approvals, secrets:reveal and admin scopes (`admin:pricing` included), and error codes including `requires_sign_in`, `insufficient_scope`, `invalid_ttl`, `invalid_code`, `already_processed` and `expired`.
39
+
40
+ ### Fixed
41
+
42
+ - `@inferencesh/sdk/proxy/express` cancels the upstream request when the client disconnects. Before, a disconnected chat or task stream kept its upstream connection open, which kept the host process alive after `server.close()`.
43
+ - `tasks.watch().done`, `client.run({ wait: true })` and the agent's stream-until-idle no longer hang when a stream closes cleanly before a terminal status (load balancer idle timeout, deploy): the SDK reads the task or chat once and settles from it, rejecting when it is still not terminal.
44
+ - Agent chat state always holds `content` as an array: a message the API serializes with `content: null`, or a partial update without content, no longer crashes views that read it. A partial update keeps the content the message had.
45
+ - `SDK_VERSION` reads `package.json`, so it cannot drift from the published version.
46
+
47
+ ### Deprecated
48
+
49
+ - `FlowRunsAPI.update()`: the API refuses it, because a flow run has no caller-writable fields. Use `FlowRunsAPI.updateVisibility()` to change who can see a run. The method stays for compile compatibility.
50
+
51
+ ### Security
52
+
53
+ - The server proxy forwards only what a frontend needs to run things, listed in `PROXY_ENDPOINTS`: `POST /apps/run`, `POST /files` (uploads of run inputs), `POST /agents/run`, `POST /chats`, `POST /chats/{id}/messages`, `POST /chats/{id}/agent`, `GET /agents/{namespace}/{name}`, reading, polling, streaming, stopping and cancelling the resulting task or chat, and client tool results (`POST /tools/{id}`). Every other call (secrets, API keys, billing, account, file listings, app/agent/flow management, admin, task and chat listings and feeds) answers 403 before the API key is attached, whatever the options. Before, it forwarded any path on `*.inference.sh` with the site's key. A server that proxied other calls for its frontend must make them from its own backend.
54
+ - Ids in proxied paths must have the shape the API mints (a lowercase 26-character ULID), so a literal route beside an id is never taken for one: `GET /tasks/stream` (the live feed of every task the key can read), `/tasks/featured`, `/tasks/queue-stats`, `/chats/list` and the like answer 403.
55
+ - Tool approvals are not forwarded: approve, reject, always allow, explain, resolving an interrupt, listing a run's interrupts, and chat settings (`allow_all_tools`). The API takes an approval only from the account holder's own sign-in, never an API key, so these could not work through a proxy, and "allow all tools" would have let any visitor holding a chat id approve every call in it. Agents served through a proxy should not use approval-gated tools; use a published agent (embed) for that.
56
+ - A target URL with credentials (`https://user:pass@...`) is refused with 400.
57
+ - The proxy forwards only to the configured API origin: `apiBaseUrl` / `INFERENCE_API_BASE_URL` when set, else `apiUrl` (default `https://api.inference.sh`). Other `*.inference.sh` hosts are refused with 412. `allowedDomains` patterns must match the whole host.
58
+ - `POST /apps/run` through the proxy takes only `app`, `app_id`, `version_id`, `function`, `input`, `stream` and `wait`: a visitor can no longer set `workers`, `infra`, `session`, `session_timeout`, `webhook`, `run_at`, `setup` or `metadata` (403).
59
+ - `POST /files` through the proxy takes one file per request, with no `category`, and a declared `size` up to `maxUploadBytes` (413 above it). Uploads count against the API key owner's storage quota.
60
+ - With `allowedEndpoints` set, a request that names an app or agent must name an allowed one by its `namespace/name` ref: `app_id`/`version_id` runs and ad-hoc `agent_config` runs are refused, and body keys are matched case-insensitively as the API reads them. Requests about an existing task or chat name it by id and are not checked against `allowedEndpoints`: whoever holds a task or chat id can read, stream, cancel or stop it and send messages in the chat. All visitors share the key, so the id is the only boundary between them.
61
+ - The server proxy (`@inferencesh/sdk/proxy`) refuses a target that is not `https:` with 412 before attaching the API key, so a client can no longer make it send `INFERENCE_API_KEY` over plain http. Plain `http:` is allowed only to a loopback host (`localhost`, `*.localhost`, `127.0.0.0/8`, `[::1]`), for a local API in development. A malformed target with `apiBaseUrl` set answers 400 instead of throwing.
62
+
10
63
  ## [0.21.0] - 2026-10-08
11
64
 
12
65
  Breaking: `AgentClient.http` requires `fetch`, the `AppTag*` constants are removed, and `StreamableManagerOptions` is a type alias. See Changed and Removed.
@@ -46,7 +46,23 @@ export declare function submitToolResult(client: AgentClient, toolInvocationId:
46
46
  };
47
47
  form_data?: Record<string, unknown>;
48
48
  }): Promise<void>;
49
+ /**
50
+ * Approve a call awaiting approval; the api then runs it.
51
+ *
52
+ * Needs approvals:write, which only the account holder's own sign-in holds
53
+ * (the web app, or a `belt login` session). An API key, an app's OAuth token
54
+ * or the server proxy is refused with 403 `requires_sign_in`
55
+ * (ErrorCodeRequiresSignIn); the error's message says what to do.
56
+ */
49
57
  export declare function approveTool(client: AgentClient, toolInvocationId: string): Promise<void>;
58
+ /**
59
+ * Reject a call awaiting approval, with an optional reason the agent sees.
60
+ *
61
+ * Needs approvals:write, which only the account holder's own sign-in holds
62
+ * (the web app, or a `belt login` session). An API key, an app's OAuth token
63
+ * or the server proxy is refused with 403 `requires_sign_in`
64
+ * (ErrorCodeRequiresSignIn); the error's message says what to do.
65
+ */
50
66
  export declare function rejectTool(client: AgentClient, toolInvocationId: string, reason?: string): Promise<void>;
51
67
  /**
52
68
  * What "always allow" can save for a call awaiting approval, narrowest first.
@@ -57,6 +73,11 @@ export declare function getAlwaysAllowOptions(client: AgentClient, chatId: strin
57
73
  * Save an "always allow" option (a key from getAlwaysAllowOptions) as chat
58
74
  * rules and approve the call once. Without an option the api saves its old
59
75
  * default: the command exactly for remote_exec, the whole tool otherwise.
76
+ *
77
+ * Needs approvals:write, which only the account holder's own sign-in holds
78
+ * (the web app, or a `belt login` session). An API key, an app's OAuth token
79
+ * or the server proxy is refused with 403 `requires_sign_in`
80
+ * (ErrorCodeRequiresSignIn); the error's message says what to do.
60
81
  */
61
82
  export declare function alwaysAllowTool(client: AgentClient, chatId: string, toolInvocationId: string, option?: string): Promise<AlwaysAllowResultDTO>;
62
83
  /**
@@ -65,8 +86,21 @@ export declare function alwaysAllowTool(client: AgentClient, chatId: string, too
65
86
  * for the call, so asking again is free.
66
87
  */
67
88
  export declare function explainTool(client: AgentClient, chatId: string, toolInvocationId: string): Promise<ToolExplanationDTO>;
68
- /** Change the chat's settings. Answers with the settings as they now are. */
89
+ /**
90
+ * Change the chat's settings. Answers with the settings as they now are.
91
+ * Widening what runs without approval (allow_all_tools) needs approvals:write,
92
+ * held only by the account holder's own sign-in: other credentials are
93
+ * refused with 403 `requires_sign_in` (ErrorCodeRequiresSignIn).
94
+ */
69
95
  export declare function updateChatSettings(client: AgentClient, chatId: string, settings: ChatSettingsRequest): Promise<ChatSettingsDTO>;
96
+ /**
97
+ * Allow or deny an interrupt gate on an agent run.
98
+ *
99
+ * Needs approvals:write, which only the account holder's own sign-in holds
100
+ * (the web app, or a `belt login` session). An API key, an app's OAuth token
101
+ * or the server proxy is refused with 403 `requires_sign_in`
102
+ * (ErrorCodeRequiresSignIn); the error's message says what to do.
103
+ */
70
104
  export declare function resolveInterrupt(client: AgentClient, interruptId: string, decision: 'allow' | 'deny'): Promise<InterruptDTO>;
71
105
  export declare function listRunInterrupts(client: AgentClient, runId: string): Promise<InterruptDTO[]>;
72
106
  export declare function uploadFile(client: AgentClient, file: globalThis.File): Promise<FileRef>;
package/dist/agent/api.js CHANGED
@@ -21,7 +21,9 @@ async function processFiles(client, files) {
21
21
  if (isFileRef(file))
22
22
  return file;
23
23
  try {
24
- return await client.files.upload(file);
24
+ // The upload answers with the whole file record; the message takes a ref.
25
+ const { id, uri, filename, content_type, size } = await client.files.upload(file);
26
+ return { id, uri, filename, content_type, size };
25
27
  }
26
28
  catch (error) {
27
29
  console.error('[AgentSDK] Failed to upload file:', error);
@@ -45,9 +47,9 @@ async function createChat(client, config) {
45
47
  /**
46
48
  * Send a message in a chat — POST /chats/{id}/messages
47
49
  */
48
- async function sendChatMessage(client, chatId, text) {
50
+ async function sendChatMessage(client, chatId, text, attachments) {
49
51
  const resp = await client.http.request('post', `/chats/${chatId}/messages`, {
50
- data: { message: text },
52
+ data: attachments ? { message: text, attachments } : { message: text },
51
53
  });
52
54
  return {
53
55
  chatId,
@@ -58,14 +60,14 @@ async function sendChatMessage(client, chatId, text) {
58
60
  * Send a message — creates chat if needed, then sends message
59
61
  */
60
62
  export async function sendMessage(client, config, chatId, text, files) {
61
- await processFiles(client, files);
63
+ const attachments = await processFiles(client, files);
62
64
  // Existing chat — just send the message
63
65
  if (chatId) {
64
- return sendChatMessage(client, chatId, text);
66
+ return sendChatMessage(client, chatId, text, attachments);
65
67
  }
66
68
  // New chat — create it, then send the first message
67
69
  const chat = await createChat(client, config);
68
- return sendChatMessage(client, chat.id, text);
70
+ return sendChatMessage(client, chat.id, text, attachments);
69
71
  }
70
72
  // =========================================================================
71
73
  // Agent info
@@ -146,9 +148,25 @@ export async function submitToolResult(client, toolInvocationId, resultOrAction)
146
148
  const data = typeof resultOrAction === 'string' ? { result: resultOrAction } : resultOrAction;
147
149
  await client.http.request('post', `/tools/${toolInvocationId}`, { data });
148
150
  }
151
+ /**
152
+ * Approve a call awaiting approval; the api then runs it.
153
+ *
154
+ * Needs approvals:write, which only the account holder's own sign-in holds
155
+ * (the web app, or a `belt login` session). An API key, an app's OAuth token
156
+ * or the server proxy is refused with 403 `requires_sign_in`
157
+ * (ErrorCodeRequiresSignIn); the error's message says what to do.
158
+ */
149
159
  export async function approveTool(client, toolInvocationId) {
150
160
  await client.http.request('post', `/tools/${toolInvocationId}/invoke`);
151
161
  }
162
+ /**
163
+ * Reject a call awaiting approval, with an optional reason the agent sees.
164
+ *
165
+ * Needs approvals:write, which only the account holder's own sign-in holds
166
+ * (the web app, or a `belt login` session). An API key, an app's OAuth token
167
+ * or the server proxy is refused with 403 `requires_sign_in`
168
+ * (ErrorCodeRequiresSignIn); the error's message says what to do.
169
+ */
152
170
  export async function rejectTool(client, toolInvocationId, reason) {
153
171
  await client.http.request('post', `/tools/${toolInvocationId}/reject`, { data: { reason } });
154
172
  }
@@ -164,6 +182,11 @@ export async function getAlwaysAllowOptions(client, chatId, toolInvocationId) {
164
182
  * Save an "always allow" option (a key from getAlwaysAllowOptions) as chat
165
183
  * rules and approve the call once. Without an option the api saves its old
166
184
  * default: the command exactly for remote_exec, the whole tool otherwise.
185
+ *
186
+ * Needs approvals:write, which only the account holder's own sign-in holds
187
+ * (the web app, or a `belt login` session). An API key, an app's OAuth token
188
+ * or the server proxy is refused with 403 `requires_sign_in`
189
+ * (ErrorCodeRequiresSignIn); the error's message says what to do.
167
190
  */
168
191
  export async function alwaysAllowTool(client, chatId, toolInvocationId, option) {
169
192
  const data = option ? { option } : {};
@@ -179,7 +202,12 @@ export async function explainTool(client, chatId, toolInvocationId) {
179
202
  const resp = await client.http.request('post', `/chats/${chatId}/tools/${toolInvocationId}/explain`);
180
203
  return resp.data;
181
204
  }
182
- /** Change the chat's settings. Answers with the settings as they now are. */
205
+ /**
206
+ * Change the chat's settings. Answers with the settings as they now are.
207
+ * Widening what runs without approval (allow_all_tools) needs approvals:write,
208
+ * held only by the account holder's own sign-in: other credentials are
209
+ * refused with 403 `requires_sign_in` (ErrorCodeRequiresSignIn).
210
+ */
183
211
  export async function updateChatSettings(client, chatId, settings) {
184
212
  const resp = await client.http.request('post', `/chats/${chatId}/settings`, { data: settings });
185
213
  return resp.data;
@@ -187,6 +215,14 @@ export async function updateChatSettings(client, chatId, settings) {
187
215
  // =========================================================================
188
216
  // Interrupt operations
189
217
  // =========================================================================
218
+ /**
219
+ * Allow or deny an interrupt gate on an agent run.
220
+ *
221
+ * Needs approvals:write, which only the account holder's own sign-in holds
222
+ * (the web app, or a `belt login` session). An API key, an app's OAuth token
223
+ * or the server proxy is refused with 403 `requires_sign_in`
224
+ * (ErrorCodeRequiresSignIn); the error's message says what to do.
225
+ */
190
226
  export async function resolveInterrupt(client, interruptId, decision) {
191
227
  const resp = await client.http.request('post', `/interrupts/${interruptId}/resolve`, {
192
228
  data: { decision }
@@ -14,6 +14,20 @@ function deriveChatStatus(run) {
14
14
  return ChatStatusAwaitingInput;
15
15
  return ChatStatusIdle;
16
16
  }
17
+ /**
18
+ * Every message enters state through here. The API serializes a message with
19
+ * no content as `content: null` (a nil Go slice), and a partial stream update
20
+ * carries the whole DTO with `content: null` when content is not among its
21
+ * fields. State always holds an array, so ChatMessageDTO.content is what its
22
+ * type says for every consumer; a partial update with null content keeps the
23
+ * content the message already had.
24
+ */
25
+ function normalizeMessage(message, existing) {
26
+ if (message.content != null)
27
+ return message;
28
+ return { ...message, content: existing?.content ?? [] };
29
+ }
30
+ const normalizeMessages = (messages) => messages.map((m) => normalizeMessage(m));
17
31
  // =============================================================================
18
32
  // Initial State
19
33
  // =============================================================================
@@ -38,7 +52,7 @@ export function chatReducer(state, action) {
38
52
  if (!chat) {
39
53
  return { ...state, chat: null, messages: [], connectionStatus: 'idle', messageCursor: undefined, hasOlderMessages: undefined };
40
54
  }
41
- const messages = [...(chat.chat_messages || [])].sort((a, b) => a.order - b.order);
55
+ const messages = normalizeMessages(chat.chat_messages || []).sort((a, b) => a.order - b.order);
42
56
  const cursor = chat?._messageCursor;
43
57
  const hasOlder = chat?._hasOlderMessages;
44
58
  return { ...state, chat, messages, messageCursor: cursor, hasOlderMessages: hasOlder };
@@ -93,11 +107,11 @@ export function chatReducer(state, action) {
93
107
  return { ...state, chat: { ...state.chat, active_run: action.payload, status: deriveChatStatus(action.payload) } };
94
108
  }
95
109
  case 'SET_MESSAGES':
96
- return { ...state, messages: action.payload };
110
+ return { ...state, messages: normalizeMessages(action.payload) };
97
111
  case 'PREPEND_MESSAGES': {
98
112
  const { messages: older, cursor, hasMore } = action.payload;
99
113
  const existingIds = new Set(state.messages.map(m => m.id));
100
- const deduped = older.filter(m => !existingIds.has(m.id));
114
+ const deduped = normalizeMessages(older.filter(m => !existingIds.has(m.id)));
101
115
  const merged = [...deduped, ...state.messages].sort((a, b) => a.order - b.order);
102
116
  return { ...state, messages: merged, messageCursor: cursor, hasOlderMessages: hasMore };
103
117
  }
@@ -106,7 +120,9 @@ export function chatReducer(state, action) {
106
120
  const existingIndex = state.messages.findIndex((m) => m.id === message.id);
107
121
  if (existingIndex !== -1) {
108
122
  const existing = state.messages[existingIndex];
109
- const updated = action.partial ? { ...existing, ...message } : message;
123
+ const updated = action.partial
124
+ ? normalizeMessage({ ...existing, ...message }, existing)
125
+ : normalizeMessage(message);
110
126
  if (existing === updated)
111
127
  return state;
112
128
  const newMessages = [...state.messages];
@@ -115,12 +131,12 @@ export function chatReducer(state, action) {
115
131
  }
116
132
  if (action.partial)
117
133
  return state;
118
- return { ...state, messages: [...state.messages, message].sort((a, b) => a.order - b.order) };
134
+ return { ...state, messages: [...state.messages, normalizeMessage(message)].sort((a, b) => a.order - b.order) };
119
135
  }
120
136
  case 'ADD_MESSAGE':
121
137
  return {
122
138
  ...state,
123
- messages: [...state.messages, action.payload].sort((a, b) => a.order - b.order),
139
+ messages: [...state.messages, normalizeMessage(action.payload)].sort((a, b) => a.order - b.order),
124
140
  };
125
141
  case 'DELTA_TOKEN': {
126
142
  const { messageId, output } = action.payload;
@@ -131,7 +147,7 @@ export function chatReducer(state, action) {
131
147
  const target = msgs.find(m => m.id === messageId);
132
148
  if (!target)
133
149
  return state;
134
- const textBlock = target.content?.find(c => c.type === 'text');
150
+ const textBlock = target.content.find(c => c.type === 'text');
135
151
  const newContent = textBlock
136
152
  ? target.content.map(c => c.type === 'text' ? { ...c, text: output.response } : c)
137
153
  : [{ type: 'text', text: output.response }, ...target.content];
@@ -132,9 +132,21 @@ export interface AgentChatActions {
132
132
  submitToolResult: (toolInvocationId: string, result: string) => Promise<void>;
133
133
  /** Answer an MCP tool call's input requests (AwaitingInput status with MCPInputState data) */
134
134
  submitMCPInput: (toolInvocationId: string, responses: Record<string, ElicitResult>) => Promise<void>;
135
- /** Approve a tool (for HIL approval - AwaitingApproval status) */
135
+ /**
136
+ * Approve a tool (for HIL approval - AwaitingApproval status)
137
+ *
138
+ * Needs the account holder's own sign-in (approvals:write): with an API
139
+ * key, an app's token or the server proxy it rejects with 403
140
+ * `requires_sign_in` (ErrorCodeRequiresSignIn).
141
+ */
136
142
  approveTool: (toolInvocationId: string) => Promise<void>;
137
- /** Reject a tool (for HIL approval - AwaitingApproval status) */
143
+ /**
144
+ * Reject a tool (for HIL approval - AwaitingApproval status)
145
+ *
146
+ * Needs the account holder's own sign-in (approvals:write): with an API
147
+ * key, an app's token or the server proxy it rejects with 403
148
+ * `requires_sign_in` (ErrorCodeRequiresSignIn).
149
+ */
138
150
  rejectTool: (toolInvocationId: string, reason?: string) => Promise<void>;
139
151
  /**
140
152
  * What "always allow" can save for a call awaiting approval, narrowest
@@ -154,15 +166,28 @@ export interface AgentChatActions {
154
166
  * api saves its default. A string is accepted from older callers (it was the
155
167
  * tool name) and means the default. Rejects with a 409 when the option is
156
168
  * stale: read the options again.
169
+ *
170
+ * Needs the account holder's own sign-in (approvals:write): with an API
171
+ * key, an app's token or the server proxy it rejects with 403
172
+ * `requires_sign_in` (ErrorCodeRequiresSignIn).
157
173
  */
158
174
  alwaysAllowTool: (toolInvocationId: string, choice?: AlwaysAllowChoice | string) => Promise<AlwaysAllowResultDTO | undefined>;
159
- /** Change this chat's settings, e.g. allow_all_tools (switching it on approves the calls waiting) */
175
+ /**
176
+ * Change this chat's settings, e.g. allow_all_tools (switching it on approves the calls waiting).
177
+ * Widening approvals needs the account holder's own sign-in: other credentials get 403 `requires_sign_in`.
178
+ */
160
179
  updateChatSettings: (settings: ChatSettingsRequest) => Promise<void>;
161
180
  /** Hand this chat to another agent (namespace/name); the next message goes to it. Agents our loop runs only. */
162
181
  switchAgent: (agentRef: string) => Promise<void>;
163
182
  /** Cancel a queued message before the agent processes it */
164
183
  cancelMessage: (messageId: string) => Promise<void>;
165
- /** Resolve an interrupt gate (allow or deny) */
184
+ /**
185
+ * Resolve an interrupt gate (allow or deny)
186
+ *
187
+ * Needs the account holder's own sign-in (approvals:write): with an API
188
+ * key, an app's token or the server proxy it rejects with 403
189
+ * `requires_sign_in` (ErrorCodeRequiresSignIn).
190
+ */
166
191
  resolveInterrupt: (interruptId: string, decision: 'allow' | 'deny') => Promise<void>;
167
192
  /** Load older messages (scroll-up pagination). Returns true if more exist. */
168
193
  loadOlderMessages: () => Promise<boolean>;
@@ -1,5 +1,6 @@
1
1
  import { HttpClient } from '../http/client';
2
2
  import type { Response } from '../http/response';
3
+ import type { AgentUpdateBody } from './write-bodies';
3
4
  import { FilesAPI } from './files';
4
5
  import { ChatDTO, ChatMessageDTO, ChannelContext, LLMDelta, LLMOutput, AgentConfigInput as AgentConfig, AgentDTO, AgentVersionDTO, CreateAgentRequest, FileDTO as File, InterruptDTO, CursorListRequest, CursorListResponse } from '../types';
5
6
  /** Internal tool definition returned by getInternalTools */
@@ -159,9 +160,10 @@ export declare class AgentsAPI {
159
160
  */
160
161
  createAgent(data: CreateAgentRequest): Promise<Response<AgentDTO>>;
161
162
  /**
162
- * Update an agent template
163
+ * Update an agent template: its visibility and images, and a new version
164
+ * when `version` is set (see AgentUpdateBody).
163
165
  */
164
- update(agentId: string, data: Partial<AgentDTO>): Promise<Response<AgentDTO>>;
166
+ update(agentId: string, data: AgentUpdateBody): Promise<Response<AgentDTO>>;
165
167
  /**
166
168
  * Delete an agent template
167
169
  */
@@ -215,7 +217,11 @@ export declare class AgentsAPI {
215
217
  form_data?: Record<string, unknown>;
216
218
  }): Promise<void>;
217
219
  /**
218
- * Resolve an interrupt gate on an agent run
220
+ * Resolve an interrupt gate on an agent run.
221
+ *
222
+ * Needs approvals:write, which only the account holder's own sign-in holds:
223
+ * with an API key, an app's OAuth token or the server proxy it rejects with
224
+ * 403 `requires_sign_in` (ErrorCodeRequiresSignIn).
219
225
  */
220
226
  resolveInterrupt(interruptId: string, decision: 'allow' | 'deny'): Promise<Response<InterruptDTO>>;
221
227
  /**
@@ -1,4 +1,4 @@
1
- import { StreamableManager } from '../http/streamable';
1
+ import { streamUntilTerminal } from '../http/stream-until-terminal';
2
2
  import { PollManager } from '../http/poll';
3
3
  import { createLLMDeltaAccumulator } from '../delta';
4
4
  import { ToolTypeClient, ToolInvocationStatusAwaitingInput, ToolInvocationStatusInProgress, } from '../types';
@@ -108,6 +108,10 @@ export class Agent {
108
108
  let waitPromise = null;
109
109
  if (this.chatId && shouldWait) {
110
110
  waitPromise = waitFn(options);
111
+ // The stream can fail while the POST is in flight, before anything
112
+ // awaits waitPromise; mark the rejection handled so Node does not treat
113
+ // it as unhandled. The await below still sees it.
114
+ waitPromise.catch(() => { });
111
115
  }
112
116
  // Make the POST request
113
117
  let response;
@@ -145,8 +149,11 @@ export class Agent {
145
149
  this.disconnect();
146
150
  reject(signal.reason);
147
151
  };
148
- if (signal.aborted)
149
- return onAbort();
152
+ if (signal.aborted) {
153
+ onAbort();
154
+ wait.catch(() => { });
155
+ return;
156
+ }
150
157
  signal.addEventListener('abort', onAbort, { once: true });
151
158
  wait.then(resolve, reject).finally(() => signal.removeEventListener('abort', onAbort));
152
159
  });
@@ -203,89 +210,101 @@ export class Agent {
203
210
  startStreaming(options = {}) {
204
211
  if (!this.chatId)
205
212
  return;
206
- this.streamUntilIdle(options, new TurnGate(false));
213
+ // Fire-and-forget: a stream failure must not become an unhandled rejection.
214
+ this.streamUntilIdle(options, new TurnGate(false)).catch((error) => {
215
+ console.warn('[Agent] Stream error:', error);
216
+ });
207
217
  }
208
218
  /** Stream events until chat becomes idle */
209
219
  streamUntilIdle(options, gate) {
210
220
  if (!this.chatId)
211
221
  return Promise.resolve();
212
- const endpoint = `/chats/${this.chatId}/stream`;
213
- return new Promise((resolve) => {
214
- this.stream?.stop();
215
- this.stream = new StreamableManager({
216
- request: (init) => this.http.fetch(endpoint, init),
217
- });
222
+ const chatId = this.chatId;
223
+ this.stream?.stop();
224
+ const handle = streamUntilTerminal((settle) => {
218
225
  // Last chat/run observation was idle but the gate wasn't settled yet;
219
226
  // a terminal message for this turn can settle it.
220
227
  let idlePending = false;
221
228
  const onIdle = () => {
222
229
  if (gate.settled)
223
- resolve();
230
+ settle.resolve();
224
231
  else
225
232
  idlePending = true;
226
233
  };
227
- this.stream.addEventListener('chats', (chat) => {
234
+ const observeChat = (chat) => {
228
235
  options.onChat?.(chat);
229
236
  gate.observeChat(chat);
230
237
  if (!isChatBusy(chat))
231
238
  onIdle();
232
239
  else
233
240
  idlePending = false;
234
- });
235
- this.stream.addEventListener('agent_runs', (run) => {
236
- const asChat = { active_run: run };
237
- options.onChat?.(asChat);
238
- gate.observeChat(asChat);
239
- if (!isChatBusy(asChat))
240
- onIdle();
241
- else
242
- idlePending = false;
243
- });
241
+ };
244
242
  // One accumulator per message being streamed, keyed by the message id
245
243
  // the delta names, so a tool-call-only turn never shows the previous
246
244
  // message's text. Same attribution rule as the React hooks.
247
245
  const deltaAccums = new Map();
248
- this.stream.addEventListener('delta', (evt) => {
249
- if (!options.onDelta || !evt?.delta || !evt.resource_id)
250
- return;
251
- let accum = deltaAccums.get(evt.resource_id);
252
- if (!accum) {
253
- accum = createLLMDeltaAccumulator();
254
- deltaAccums.set(evt.resource_id, accum);
255
- }
256
- accum.apply(evt.delta);
257
- options.onDelta({
258
- messageId: evt.resource_id,
259
- delta: evt.delta,
260
- output: accum.toOutput(),
261
- seq: evt.seq,
262
- });
263
- });
264
- this.stream.addEventListener('chat_messages', (message) => {
265
- // A terminal message receives no further deltas.
266
- if (isMessageTerminal(message.status))
267
- deltaAccums.delete(message.id);
268
- gate.observeMessage(message);
269
- options.onMessage?.(message);
270
- if (idlePending && gate.settled)
271
- resolve();
272
- if (message.tool_invocations && options.onToolCall) {
273
- for (const inv of message.tool_invocations) {
274
- if (this.dispatchedToolCalls.has(inv.id))
275
- continue;
276
- if (inv.type === ToolTypeClient && (inv.status === ToolInvocationStatusInProgress || inv.status === ToolInvocationStatusAwaitingInput)) {
277
- this.dispatchedToolCalls.add(inv.id);
278
- options.onToolCall({
279
- id: inv.id,
280
- name: inv.function?.name || '',
281
- args: inv.function?.arguments || {},
282
- });
246
+ return {
247
+ request: (init) => this.http.fetch(`/chats/${chatId}/stream`, init),
248
+ events: {
249
+ chats: (chat) => observeChat(chat),
250
+ agent_runs: (run) => observeChat({ active_run: run }),
251
+ delta: (evt) => {
252
+ if (!options.onDelta || !evt?.delta || !evt.resource_id)
253
+ return;
254
+ let accum = deltaAccums.get(evt.resource_id);
255
+ if (!accum) {
256
+ accum = createLLMDeltaAccumulator();
257
+ deltaAccums.set(evt.resource_id, accum);
283
258
  }
284
- }
285
- }
286
- });
287
- this.stream.start();
259
+ accum.apply(evt.delta);
260
+ options.onDelta({
261
+ messageId: evt.resource_id,
262
+ delta: evt.delta,
263
+ output: accum.toOutput(),
264
+ seq: evt.seq,
265
+ });
266
+ },
267
+ chat_messages: (message) => {
268
+ // A terminal message receives no further deltas.
269
+ if (isMessageTerminal(message.status))
270
+ deltaAccums.delete(message.id);
271
+ gate.observeMessage(message);
272
+ options.onMessage?.(message);
273
+ if (idlePending && gate.settled)
274
+ settle.resolve();
275
+ if (message.tool_invocations && options.onToolCall) {
276
+ for (const inv of message.tool_invocations) {
277
+ if (this.dispatchedToolCalls.has(inv.id))
278
+ continue;
279
+ if (inv.type === ToolTypeClient && (inv.status === ToolInvocationStatusInProgress || inv.status === ToolInvocationStatusAwaitingInput)) {
280
+ this.dispatchedToolCalls.add(inv.id);
281
+ options.onToolCall({
282
+ id: inv.id,
283
+ name: inv.function?.name || '',
284
+ args: inv.function?.arguments || {},
285
+ });
286
+ }
287
+ }
288
+ }
289
+ },
290
+ },
291
+ // The stream closed before the chat was seen idle: the chat itself
292
+ // says whether the turn is over.
293
+ reconcile: async () => {
294
+ const { data: chat } = await this.http.request('get', `/chats/${chatId}`);
295
+ observeChat(chat);
296
+ },
297
+ closedMessage: 'Chat stream closed before the chat became idle',
298
+ // Stopped by disconnect()/abort or replaced by a newer stream: the
299
+ // caller stopped waiting on purpose.
300
+ onStopped: () => settle.resolve(),
301
+ // Once the turn is over the stream stays attached, so onChat/onMessage
302
+ // keep reporting until the next turn or disconnect() replaces it.
303
+ keepOpen: true,
304
+ };
288
305
  });
306
+ this.stream = handle;
307
+ return handle.done;
289
308
  }
290
309
  /** Poll until chat becomes idle, dispatching callbacks on changes */
291
310
  pollUntilIdle(options, gate) {
@@ -394,7 +413,8 @@ export class AgentsAPI {
394
413
  return this.http.request('post', '/agents', { data });
395
414
  }
396
415
  /**
397
- * Update an agent template
416
+ * Update an agent template: its visibility and images, and a new version
417
+ * when `version` is set (see AgentUpdateBody).
398
418
  */
399
419
  async update(agentId, data) {
400
420
  return this.http.request('post', `/agents/${agentId}`, { data });
@@ -472,7 +492,11 @@ export class AgentsAPI {
472
492
  await this.http.request('post', `/tools/${toolInvocationId}`, { data: { result } });
473
493
  }
474
494
  /**
475
- * Resolve an interrupt gate on an agent run
495
+ * Resolve an interrupt gate on an agent run.
496
+ *
497
+ * Needs approvals:write, which only the account holder's own sign-in holds:
498
+ * with an API key, an app's OAuth token or the server proxy it rejects with
499
+ * 403 `requires_sign_in` (ErrorCodeRequiresSignIn).
476
500
  */
477
501
  async resolveInterrupt(interruptId, decision) {
478
502
  return this.http.request('post', `/interrupts/${interruptId}/resolve`, {
@@ -1,6 +1,6 @@
1
1
  import { HttpClient } from '../http/client';
2
2
  import type { Response } from '../http/response';
3
- import { ApiKeyDTO, CursorListRequest, CursorListResponse } from '../types';
3
+ import { ApiKeyDTO, CreateApiKeyRequest, CursorListRequest, CursorListResponse } from '../types';
4
4
  /**
5
5
  * API Keys API
6
6
  */
@@ -14,10 +14,7 @@ export declare class ApiKeysAPI {
14
14
  /**
15
15
  * Create an API key
16
16
  */
17
- create(data: {
18
- name: string;
19
- scopes?: string[];
20
- }): Promise<Response<ApiKeyDTO>>;
17
+ create(data: CreateApiKeyRequest): Promise<Response<ApiKeyDTO>>;
21
18
  /**
22
19
  * Delete an API key
23
20
  */