@agent-api/sdk 1.2.1 → 1.2.3

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
@@ -1,5 +1,18 @@
1
1
  # Changelog — @agent-api/sdk
2
2
 
3
+ ## 1.2.3
4
+
5
+ ### Fixed
6
+
7
+ - Hardened local workdir scans so broken symlinks and files that disappear during recursive list, grep, and summarize operations do not abort the scan.
8
+
9
+ ## 1.2.2
10
+
11
+ ### Added
12
+
13
+ - Added `RequestOptions.signal` so CLI, TUI, and desktop integrations can abort in-flight HTTP requests and response streams before a response ID is available.
14
+ - Documented and tested response cancellation behavior alongside the existing `responses.cancel(responseID)` backend cancellation API.
15
+
3
16
  ## 1.2.1
4
17
 
5
18
  ### Added
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  Production JavaScript/TypeScript SDK for the Managed Agent API.
4
4
 
5
- **Published on npm:** [`@agent-api/sdk`](https://www.npmjs.com/package/@agent-api/sdk) (v1.2.1)
5
+ **Published on npm:** [`@agent-api/sdk`](https://www.npmjs.com/package/@agent-api/sdk) (v1.2.2)
6
6
 
7
7
  ## Install
8
8
 
@@ -257,6 +257,7 @@ const context = await createLocalContextPackage(project, {
257
257
 
258
258
  - **Retries:** automatic exponential backoff for network failures, 429, and 5xx (default 2 retries).
259
259
  - **Timeouts:** 10 minute default for requests; 1 hour for streaming agent runs (configurable via `timeout` / `streamTimeout`).
260
+ - **Cancellation:** pass `signal` in request options to abort local HTTP waiting, and call `client.responses.cancel(responseID)` for backend best-effort cancellation after a response ID exists.
260
261
  - **Typed errors:** `AuthenticationError`, `RateLimitError`, `NotFoundError`, `BadRequestError`, etc.
261
262
  - **Pagination:** `listPage` returns a cursor page; `listIterator` auto-fetches all pages.
262
263
 
@@ -273,7 +274,7 @@ const stream = await client.responses.create({
273
274
  preset: "fast-search",
274
275
  input: "Summarize today's AI news.",
275
276
  stream: true,
276
- });
277
+ }, { signal: abortController.signal });
277
278
 
278
279
  for await (const event of stream) {
279
280
  if (event.type === "response.output_text.delta") {
@@ -33,6 +33,9 @@ export class HTTPClient {
33
33
  return await this.fetchOnce(method, path, body, options, stream, rawBody);
34
34
  }
35
35
  catch (error) {
36
+ if (options.signal?.aborted) {
37
+ throw error;
38
+ }
36
39
  if (!(error instanceof APIError) || attempt >= maxRetries) {
37
40
  throw error;
38
41
  }
@@ -43,14 +46,29 @@ export class HTTPClient {
43
46
  }
44
47
  attempt += 1;
45
48
  const delayMs = retryDelayMs(error, attempt);
46
- await sleep(delayMs);
49
+ await sleep(delayMs, options.signal);
47
50
  }
48
51
  }
49
52
  }
50
53
  async fetchOnce(method, path, body, options, stream, rawBody) {
51
54
  const controller = new AbortController();
52
55
  const timeout = options.timeout ?? (stream ? this.options.streamTimeout : this.options.timeout);
53
- const timeoutID = setTimeout(() => controller.abort(), timeout);
56
+ let timedOut = false;
57
+ let callerAborted = options.signal?.aborted ?? false;
58
+ const timeoutID = setTimeout(() => {
59
+ timedOut = true;
60
+ controller.abort();
61
+ }, timeout);
62
+ const abortFromCaller = () => {
63
+ callerAborted = true;
64
+ controller.abort();
65
+ };
66
+ if (options.signal?.aborted) {
67
+ controller.abort();
68
+ }
69
+ else {
70
+ options.signal?.addEventListener("abort", abortFromCaller, { once: true });
71
+ }
54
72
  try {
55
73
  const headers = {
56
74
  Accept: stream ? "text/event-stream" : "application/json",
@@ -80,12 +98,19 @@ export class HTTPClient {
80
98
  throw error;
81
99
  }
82
100
  if (error instanceof Error && error.name === "AbortError") {
83
- throw new APIConnectionError(`Request timed out after ${timeout}ms`, error);
101
+ if (callerAborted || options.signal?.aborted) {
102
+ throw new APIConnectionError("Request aborted", error);
103
+ }
104
+ if (timedOut) {
105
+ throw new APIConnectionError(`Request timed out after ${timeout}ms`, error);
106
+ }
107
+ throw new APIConnectionError("Request aborted", error);
84
108
  }
85
109
  throw new APIConnectionError("Request failed", error);
86
110
  }
87
111
  finally {
88
112
  clearTimeout(timeoutID);
113
+ options.signal?.removeEventListener("abort", abortFromCaller);
89
114
  }
90
115
  }
91
116
  }
@@ -99,6 +124,23 @@ function retryDelayMs(error, attempt) {
99
124
  const jitter = Math.floor(Math.random() * 100);
100
125
  return exponential + jitter;
101
126
  }
102
- function sleep(ms) {
103
- return new Promise((resolve) => setTimeout(resolve, ms));
127
+ function sleep(ms, signal) {
128
+ if (!signal) {
129
+ return new Promise((resolve) => setTimeout(resolve, ms));
130
+ }
131
+ if (signal.aborted) {
132
+ return Promise.reject(new APIConnectionError("Request aborted"));
133
+ }
134
+ return new Promise((resolve, reject) => {
135
+ const timeout = setTimeout(() => {
136
+ signal.removeEventListener("abort", abort);
137
+ resolve();
138
+ }, ms);
139
+ const abort = () => {
140
+ clearTimeout(timeout);
141
+ signal.removeEventListener("abort", abort);
142
+ reject(new APIConnectionError("Request aborted"));
143
+ };
144
+ signal.addEventListener("abort", abort, { once: true });
145
+ });
104
146
  }
@@ -1,6 +1,6 @@
1
1
  import { createHash, randomUUID } from "node:crypto";
2
2
  import { constants as fsConstants, watch as watchFS } from "node:fs";
3
- import { access, copyFile, mkdir, readdir, readFile, rename, rm, stat, writeFile } from "node:fs/promises";
3
+ import { access, copyFile, lstat, mkdir, readdir, readFile, rename, rm, stat, writeFile } from "node:fs/promises";
4
4
  import { homedir, tmpdir } from "node:os";
5
5
  import path from "node:path";
6
6
  import { localSkillFromDirectory } from "../local-skills.js";
@@ -322,12 +322,15 @@ export class LocalFileStore {
322
322
  if (item.type !== "file" || item.size > maxBytesPerFile || !isLikelyTextFile(item.path)) {
323
323
  continue;
324
324
  }
325
- const raw = await readFile(item.fullPath);
325
+ const raw = await readOptionalFile(item.fullPath);
326
+ if (!raw) {
327
+ continue;
328
+ }
326
329
  if (looksBinary(raw)) {
327
330
  continue;
328
331
  }
329
332
  filesScanned++;
330
- const lines = splitLines(raw.toString("utf8"));
333
+ const lines = splitLines(Buffer.from(raw).toString("utf8"));
331
334
  for (let i = 0; i < lines.length; i++) {
332
335
  if (!lines[i].includes(pattern)) {
333
336
  continue;
@@ -363,7 +366,10 @@ export class LocalFileStore {
363
366
  if (!isLikelyTextFile(item.path) || item.size > previewBytes * 4) {
364
367
  continue;
365
368
  }
366
- const raw = await readFile(item.fullPath);
369
+ const raw = await readOptionalFile(item.fullPath);
370
+ if (!raw) {
371
+ continue;
372
+ }
367
373
  if (looksBinary(raw)) {
368
374
  continue;
369
375
  }
@@ -371,7 +377,7 @@ export class LocalFileStore {
371
377
  previews.push({
372
378
  path: item.path,
373
379
  size: item.size,
374
- preview: raw.subarray(0, previewBytes).toString("utf8"),
380
+ preview: Buffer.from(raw.subarray(0, previewBytes)).toString("utf8"),
375
381
  preview_truncated: truncated || undefined,
376
382
  });
377
383
  }
@@ -393,7 +399,10 @@ export class LocalFileStore {
393
399
  if (ignored(relativePath, options.ignore)) {
394
400
  continue;
395
401
  }
396
- const info = await stat(fullPath);
402
+ const info = await lstatOptional(fullPath);
403
+ if (!info) {
404
+ continue;
405
+ }
397
406
  const item = {
398
407
  path: relativePath,
399
408
  fullPath,
@@ -401,7 +410,7 @@ export class LocalFileStore {
401
410
  size: info.size,
402
411
  modifiedAt: info.mtime,
403
412
  };
404
- if (entry.isDirectory()) {
413
+ if (info.isDirectory()) {
405
414
  if (options.includeDirectories) {
406
415
  out.push(item);
407
416
  }
@@ -813,6 +822,28 @@ async function fileExists(fullPath) {
813
822
  return false;
814
823
  }
815
824
  }
825
+ async function lstatOptional(fullPath) {
826
+ try {
827
+ return await lstat(fullPath);
828
+ }
829
+ catch (error) {
830
+ if (error?.code === "ENOENT" || error?.code === "ENOTDIR") {
831
+ return null;
832
+ }
833
+ throw error;
834
+ }
835
+ }
836
+ async function readOptionalFile(fullPath) {
837
+ try {
838
+ return await readFile(fullPath);
839
+ }
840
+ catch (error) {
841
+ if (error?.code === "ENOENT" || error?.code === "ENOTDIR" || error?.code === "EISDIR") {
842
+ return null;
843
+ }
844
+ throw error;
845
+ }
846
+ }
816
847
  function normalizeAppName(appName) {
817
848
  const trimmed = appName.trim();
818
849
  if (!trimmed) {
@@ -20,6 +20,8 @@ export interface ClientOptions {
20
20
  export interface RequestOptions {
21
21
  headers?: Record<string, string>;
22
22
  timeout?: number;
23
+ /** Abort the underlying HTTP request or stream from caller-controlled UI/runtime state. */
24
+ signal?: AbortSignal;
23
25
  /** Override automatic retries for this request. */
24
26
  maxRetries?: number;
25
27
  }
package/dist/version.d.ts CHANGED
@@ -1,2 +1,2 @@
1
- export declare const VERSION = "1.2.1";
2
- export declare const USER_AGENT = "@agent-api/sdk/1.2.1";
1
+ export declare const VERSION = "1.2.3";
2
+ export declare const USER_AGENT = "@agent-api/sdk/1.2.3";
package/dist/version.js CHANGED
@@ -1,2 +1,2 @@
1
- export const VERSION = "1.2.1";
1
+ export const VERSION = "1.2.3";
2
2
  export const USER_AGENT = `@agent-api/sdk/${VERSION}`;
@@ -36,6 +36,9 @@ class HTTPClient {
36
36
  return await this.fetchOnce(method, path, body, options, stream, rawBody);
37
37
  }
38
38
  catch (error) {
39
+ if (options.signal?.aborted) {
40
+ throw error;
41
+ }
39
42
  if (!(error instanceof errors_js_1.APIError) || attempt >= maxRetries) {
40
43
  throw error;
41
44
  }
@@ -46,14 +49,29 @@ class HTTPClient {
46
49
  }
47
50
  attempt += 1;
48
51
  const delayMs = retryDelayMs(error, attempt);
49
- await sleep(delayMs);
52
+ await sleep(delayMs, options.signal);
50
53
  }
51
54
  }
52
55
  }
53
56
  async fetchOnce(method, path, body, options, stream, rawBody) {
54
57
  const controller = new AbortController();
55
58
  const timeout = options.timeout ?? (stream ? this.options.streamTimeout : this.options.timeout);
56
- const timeoutID = setTimeout(() => controller.abort(), timeout);
59
+ let timedOut = false;
60
+ let callerAborted = options.signal?.aborted ?? false;
61
+ const timeoutID = setTimeout(() => {
62
+ timedOut = true;
63
+ controller.abort();
64
+ }, timeout);
65
+ const abortFromCaller = () => {
66
+ callerAborted = true;
67
+ controller.abort();
68
+ };
69
+ if (options.signal?.aborted) {
70
+ controller.abort();
71
+ }
72
+ else {
73
+ options.signal?.addEventListener("abort", abortFromCaller, { once: true });
74
+ }
57
75
  try {
58
76
  const headers = {
59
77
  Accept: stream ? "text/event-stream" : "application/json",
@@ -83,12 +101,19 @@ class HTTPClient {
83
101
  throw error;
84
102
  }
85
103
  if (error instanceof Error && error.name === "AbortError") {
86
- throw new errors_js_1.APIConnectionError(`Request timed out after ${timeout}ms`, error);
104
+ if (callerAborted || options.signal?.aborted) {
105
+ throw new errors_js_1.APIConnectionError("Request aborted", error);
106
+ }
107
+ if (timedOut) {
108
+ throw new errors_js_1.APIConnectionError(`Request timed out after ${timeout}ms`, error);
109
+ }
110
+ throw new errors_js_1.APIConnectionError("Request aborted", error);
87
111
  }
88
112
  throw new errors_js_1.APIConnectionError("Request failed", error);
89
113
  }
90
114
  finally {
91
115
  clearTimeout(timeoutID);
116
+ options.signal?.removeEventListener("abort", abortFromCaller);
92
117
  }
93
118
  }
94
119
  }
@@ -103,6 +128,23 @@ function retryDelayMs(error, attempt) {
103
128
  const jitter = Math.floor(Math.random() * 100);
104
129
  return exponential + jitter;
105
130
  }
106
- function sleep(ms) {
107
- return new Promise((resolve) => setTimeout(resolve, ms));
131
+ function sleep(ms, signal) {
132
+ if (!signal) {
133
+ return new Promise((resolve) => setTimeout(resolve, ms));
134
+ }
135
+ if (signal.aborted) {
136
+ return Promise.reject(new errors_js_1.APIConnectionError("Request aborted"));
137
+ }
138
+ return new Promise((resolve, reject) => {
139
+ const timeout = setTimeout(() => {
140
+ signal.removeEventListener("abort", abort);
141
+ resolve();
142
+ }, ms);
143
+ const abort = () => {
144
+ clearTimeout(timeout);
145
+ signal.removeEventListener("abort", abort);
146
+ reject(new errors_js_1.APIConnectionError("Request aborted"));
147
+ };
148
+ signal.addEventListener("abort", abort, { once: true });
149
+ });
108
150
  }
@@ -337,12 +337,15 @@ class LocalFileStore {
337
337
  if (item.type !== "file" || item.size > maxBytesPerFile || !isLikelyTextFile(item.path)) {
338
338
  continue;
339
339
  }
340
- const raw = await (0, promises_1.readFile)(item.fullPath);
340
+ const raw = await readOptionalFile(item.fullPath);
341
+ if (!raw) {
342
+ continue;
343
+ }
341
344
  if (looksBinary(raw)) {
342
345
  continue;
343
346
  }
344
347
  filesScanned++;
345
- const lines = splitLines(raw.toString("utf8"));
348
+ const lines = splitLines(Buffer.from(raw).toString("utf8"));
346
349
  for (let i = 0; i < lines.length; i++) {
347
350
  if (!lines[i].includes(pattern)) {
348
351
  continue;
@@ -378,7 +381,10 @@ class LocalFileStore {
378
381
  if (!isLikelyTextFile(item.path) || item.size > previewBytes * 4) {
379
382
  continue;
380
383
  }
381
- const raw = await (0, promises_1.readFile)(item.fullPath);
384
+ const raw = await readOptionalFile(item.fullPath);
385
+ if (!raw) {
386
+ continue;
387
+ }
382
388
  if (looksBinary(raw)) {
383
389
  continue;
384
390
  }
@@ -386,7 +392,7 @@ class LocalFileStore {
386
392
  previews.push({
387
393
  path: item.path,
388
394
  size: item.size,
389
- preview: raw.subarray(0, previewBytes).toString("utf8"),
395
+ preview: Buffer.from(raw.subarray(0, previewBytes)).toString("utf8"),
390
396
  preview_truncated: truncated || undefined,
391
397
  });
392
398
  }
@@ -408,7 +414,10 @@ class LocalFileStore {
408
414
  if (ignored(relativePath, options.ignore)) {
409
415
  continue;
410
416
  }
411
- const info = await (0, promises_1.stat)(fullPath);
417
+ const info = await lstatOptional(fullPath);
418
+ if (!info) {
419
+ continue;
420
+ }
412
421
  const item = {
413
422
  path: relativePath,
414
423
  fullPath,
@@ -416,7 +425,7 @@ class LocalFileStore {
416
425
  size: info.size,
417
426
  modifiedAt: info.mtime,
418
427
  };
419
- if (entry.isDirectory()) {
428
+ if (info.isDirectory()) {
420
429
  if (options.includeDirectories) {
421
430
  out.push(item);
422
431
  }
@@ -833,6 +842,28 @@ async function fileExists(fullPath) {
833
842
  return false;
834
843
  }
835
844
  }
845
+ async function lstatOptional(fullPath) {
846
+ try {
847
+ return await (0, promises_1.lstat)(fullPath);
848
+ }
849
+ catch (error) {
850
+ if (error?.code === "ENOENT" || error?.code === "ENOTDIR") {
851
+ return null;
852
+ }
853
+ throw error;
854
+ }
855
+ }
856
+ async function readOptionalFile(fullPath) {
857
+ try {
858
+ return await (0, promises_1.readFile)(fullPath);
859
+ }
860
+ catch (error) {
861
+ if (error?.code === "ENOENT" || error?.code === "ENOTDIR" || error?.code === "EISDIR") {
862
+ return null;
863
+ }
864
+ throw error;
865
+ }
866
+ }
836
867
  function normalizeAppName(appName) {
837
868
  const trimmed = appName.trim();
838
869
  if (!trimmed) {
@@ -1,5 +1,5 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.USER_AGENT = exports.VERSION = void 0;
4
- exports.VERSION = "1.2.1";
4
+ exports.VERSION = "1.2.3";
5
5
  exports.USER_AGENT = `@agent-api/sdk/${exports.VERSION}`;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-api/sdk",
3
- "version": "1.2.1",
3
+ "version": "1.2.3",
4
4
  "description": "Production JavaScript SDK for the Managed Agent API",
5
5
  "license": "MIT",
6
6
  "repository": {