@astralform/js 4.0.0 → 4.2.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.d.cts CHANGED
@@ -120,6 +120,19 @@ interface AstralformBaseConfig {
120
120
  baseURL?: string;
121
121
  /** Supply a custom fetch (SSR, testing, custom interceptors). */
122
122
  fetch?: typeof globalThis.fetch;
123
+ /**
124
+ * Abort a REST request — connect, headers, AND body read — after this many
125
+ * milliseconds. Defaults to 30_000. Does not apply to `uploadFile` (large
126
+ * files on slow uplinks) or to SSE streaming, which is long-lived by design
127
+ * and carries its own `AbortSignal`.
128
+ *
129
+ * Note that unlike axios and friends, `0` does NOT mean "no timeout": any
130
+ * non-positive or non-finite value falls back to the default. REST calls
131
+ * cannot opt out of the deadline — an unbounded REST request is the bug
132
+ * this exists to prevent, and it fails silently (a stalled body strands the
133
+ * caller forever with nothing to react to).
134
+ */
135
+ timeoutMs?: number;
123
136
  }
124
137
  interface AstralformApiKeyConfig extends AstralformBaseConfig {
125
138
  /** Agent API key (`sk_live_...` or `sk_test_...`). */
@@ -638,6 +651,12 @@ interface ChatStreamRequest {
638
651
  upload_ids?: string[];
639
652
  agent_name?: string;
640
653
  plan_mode?: boolean;
654
+ /**
655
+ * Start a durable long-horizon goal for this run (goal mode). The backend mints
656
+ * an agent_goal from this text and drives the run under a budget until the
657
+ * objective is genuinely complete. A blank/omitted value runs a normal turn.
658
+ */
659
+ goal?: string;
641
660
  /**
642
661
  * Per-request model choice (client-side model selection), wire shape.
643
662
  * `provider` and `model` are paired; omit to reuse the thread's last model.
@@ -723,6 +742,12 @@ interface SendOptions$1 extends ModelChoiceOptions {
723
742
  uploadIds?: string[];
724
743
  agentName?: string;
725
744
  planMode?: boolean;
745
+ /**
746
+ * Start a durable long-horizon goal for this run (goal mode). The text becomes
747
+ * the goal's objective; the backend keeps the agent working under a budget until
748
+ * it's complete. Omit for a normal turn.
749
+ */
750
+ goal?: string;
726
751
  }
727
752
  /**
728
753
  * A selectable model for one of the team's connected providers, from
@@ -761,6 +786,7 @@ interface ConversationAsset {
761
786
  declare class AstralformClient {
762
787
  private readonly baseURL;
763
788
  private readonly fetchFn;
789
+ private readonly timeoutMs;
764
790
  /**
765
791
  * Auth state is mutable so callers can rotate access tokens or switch
766
792
  * agent context without re-instantiating the client. API-key mode is
@@ -807,7 +833,23 @@ declare class AstralformClient {
807
833
  */
808
834
  private get authHeaders();
809
835
  private get headers();
836
+ /**
837
+ * Run one REST exchange under a single deadline covering connect, headers,
838
+ * AND the body read. The body read is the part that matters: `json()` used
839
+ * to sit outside every guard, so a response whose headers arrived but whose
840
+ * body stalled hung forever — silently stranding callers that await it
841
+ * (a stalled `getMessages` used to leave `StreamManager.restore()` parked
842
+ * before it ever fetched the events it renders from).
843
+ *
844
+ * The controller is created per request and is deliberately NOT the
845
+ * session's — that one means "the user cancelled this turn" and is null
846
+ * outside a live turn. Aborting frees the socket; the race guarantees a
847
+ * rejection even when an injected `fetch` ignores the signal.
848
+ */
849
+ private withDeadline;
810
850
  private request;
851
+ /** Fetch + status handling. Always called inside `withDeadline`. */
852
+ private send;
811
853
  get<T>(path: string): Promise<T>;
812
854
  post<T>(path: string, body: unknown): Promise<T>;
813
855
  private del;
@@ -1141,6 +1183,11 @@ interface SendOptions extends ModelChoiceOptions {
1141
1183
  agentName?: string;
1142
1184
  uploadIds?: string[];
1143
1185
  planMode?: boolean;
1186
+ /**
1187
+ * Start a durable long-horizon goal for this run (goal mode) — the text is the
1188
+ * goal objective the backend drives to completion. Omit for a normal turn.
1189
+ */
1190
+ goal?: string;
1144
1191
  }
1145
1192
  type StreamManagerEvent = {
1146
1193
  type: "stateChange";
package/dist/index.d.ts CHANGED
@@ -120,6 +120,19 @@ interface AstralformBaseConfig {
120
120
  baseURL?: string;
121
121
  /** Supply a custom fetch (SSR, testing, custom interceptors). */
122
122
  fetch?: typeof globalThis.fetch;
123
+ /**
124
+ * Abort a REST request — connect, headers, AND body read — after this many
125
+ * milliseconds. Defaults to 30_000. Does not apply to `uploadFile` (large
126
+ * files on slow uplinks) or to SSE streaming, which is long-lived by design
127
+ * and carries its own `AbortSignal`.
128
+ *
129
+ * Note that unlike axios and friends, `0` does NOT mean "no timeout": any
130
+ * non-positive or non-finite value falls back to the default. REST calls
131
+ * cannot opt out of the deadline — an unbounded REST request is the bug
132
+ * this exists to prevent, and it fails silently (a stalled body strands the
133
+ * caller forever with nothing to react to).
134
+ */
135
+ timeoutMs?: number;
123
136
  }
124
137
  interface AstralformApiKeyConfig extends AstralformBaseConfig {
125
138
  /** Agent API key (`sk_live_...` or `sk_test_...`). */
@@ -638,6 +651,12 @@ interface ChatStreamRequest {
638
651
  upload_ids?: string[];
639
652
  agent_name?: string;
640
653
  plan_mode?: boolean;
654
+ /**
655
+ * Start a durable long-horizon goal for this run (goal mode). The backend mints
656
+ * an agent_goal from this text and drives the run under a budget until the
657
+ * objective is genuinely complete. A blank/omitted value runs a normal turn.
658
+ */
659
+ goal?: string;
641
660
  /**
642
661
  * Per-request model choice (client-side model selection), wire shape.
643
662
  * `provider` and `model` are paired; omit to reuse the thread's last model.
@@ -723,6 +742,12 @@ interface SendOptions$1 extends ModelChoiceOptions {
723
742
  uploadIds?: string[];
724
743
  agentName?: string;
725
744
  planMode?: boolean;
745
+ /**
746
+ * Start a durable long-horizon goal for this run (goal mode). The text becomes
747
+ * the goal's objective; the backend keeps the agent working under a budget until
748
+ * it's complete. Omit for a normal turn.
749
+ */
750
+ goal?: string;
726
751
  }
727
752
  /**
728
753
  * A selectable model for one of the team's connected providers, from
@@ -761,6 +786,7 @@ interface ConversationAsset {
761
786
  declare class AstralformClient {
762
787
  private readonly baseURL;
763
788
  private readonly fetchFn;
789
+ private readonly timeoutMs;
764
790
  /**
765
791
  * Auth state is mutable so callers can rotate access tokens or switch
766
792
  * agent context without re-instantiating the client. API-key mode is
@@ -807,7 +833,23 @@ declare class AstralformClient {
807
833
  */
808
834
  private get authHeaders();
809
835
  private get headers();
836
+ /**
837
+ * Run one REST exchange under a single deadline covering connect, headers,
838
+ * AND the body read. The body read is the part that matters: `json()` used
839
+ * to sit outside every guard, so a response whose headers arrived but whose
840
+ * body stalled hung forever — silently stranding callers that await it
841
+ * (a stalled `getMessages` used to leave `StreamManager.restore()` parked
842
+ * before it ever fetched the events it renders from).
843
+ *
844
+ * The controller is created per request and is deliberately NOT the
845
+ * session's — that one means "the user cancelled this turn" and is null
846
+ * outside a live turn. Aborting frees the socket; the race guarantees a
847
+ * rejection even when an injected `fetch` ignores the signal.
848
+ */
849
+ private withDeadline;
810
850
  private request;
851
+ /** Fetch + status handling. Always called inside `withDeadline`. */
852
+ private send;
811
853
  get<T>(path: string): Promise<T>;
812
854
  post<T>(path: string, body: unknown): Promise<T>;
813
855
  private del;
@@ -1141,6 +1183,11 @@ interface SendOptions extends ModelChoiceOptions {
1141
1183
  agentName?: string;
1142
1184
  uploadIds?: string[];
1143
1185
  planMode?: boolean;
1186
+ /**
1187
+ * Start a durable long-horizon goal for this run (goal mode) — the text is the
1188
+ * goal objective the backend drives to completion. Omit for a normal turn.
1189
+ */
1190
+ goal?: string;
1144
1191
  }
1145
1192
  type StreamManagerEvent = {
1146
1193
  type: "stateChange";
package/dist/index.js CHANGED
@@ -252,6 +252,7 @@ async function* streamJobSSE(options) {
252
252
 
253
253
  // src/client.ts
254
254
  var DEFAULT_BASE_URL = "https://api.astralform.ai";
255
+ var DEFAULT_TIMEOUT_MS = 3e4;
255
256
  function validateBaseURL(url) {
256
257
  const cleaned = url.replace(/\/+$/, "");
257
258
  try {
@@ -302,6 +303,7 @@ var AstralformClient = class {
302
303
  }
303
304
  this.baseURL = validateBaseURL(config.baseURL ?? DEFAULT_BASE_URL);
304
305
  this.fetchFn = config.fetch ?? globalThis.fetch.bind(globalThis);
306
+ this.timeoutMs = typeof config.timeoutMs === "number" && Number.isFinite(config.timeoutMs) && config.timeoutMs > 0 ? config.timeoutMs : DEFAULT_TIMEOUT_MS;
305
307
  }
306
308
  /**
307
309
  * Replace the current OIDC access token without reconstructing the client.
@@ -392,11 +394,48 @@ var AstralformClient = class {
392
394
  "Content-Type": "application/json"
393
395
  };
394
396
  }
397
+ /**
398
+ * Run one REST exchange under a single deadline covering connect, headers,
399
+ * AND the body read. The body read is the part that matters: `json()` used
400
+ * to sit outside every guard, so a response whose headers arrived but whose
401
+ * body stalled hung forever — silently stranding callers that await it
402
+ * (a stalled `getMessages` used to leave `StreamManager.restore()` parked
403
+ * before it ever fetched the events it renders from).
404
+ *
405
+ * The controller is created per request and is deliberately NOT the
406
+ * session's — that one means "the user cancelled this turn" and is null
407
+ * outside a live turn. Aborting frees the socket; the race guarantees a
408
+ * rejection even when an injected `fetch` ignores the signal.
409
+ */
410
+ async withDeadline(run) {
411
+ const controller = new AbortController();
412
+ const timedOut = () => new ConnectionError(`Request timed out after ${this.timeoutMs}ms`);
413
+ let timer;
414
+ const deadline = new Promise((_resolve, reject) => {
415
+ timer = setTimeout(() => {
416
+ controller.abort();
417
+ reject(timedOut());
418
+ }, this.timeoutMs);
419
+ });
420
+ try {
421
+ return await Promise.race([run(controller.signal), deadline]);
422
+ } catch (err) {
423
+ if (controller.signal.aborted) throw timedOut();
424
+ throw err;
425
+ } finally {
426
+ clearTimeout(timer);
427
+ }
428
+ }
395
429
  async request(method, path, body) {
430
+ return this.withDeadline((signal) => this.send(method, path, body, signal));
431
+ }
432
+ /** Fetch + status handling. Always called inside `withDeadline`. */
433
+ async send(method, path, body, signal) {
396
434
  const response = await this.fetchFn(`${this.baseURL}${path}`, {
397
435
  method,
398
436
  headers: this.headers,
399
- body: body !== void 0 ? JSON.stringify(body) : void 0
437
+ body: body !== void 0 ? JSON.stringify(body) : void 0,
438
+ signal
400
439
  }).catch((err) => {
401
440
  throw new ConnectionError(
402
441
  err instanceof Error ? err.message : "Failed to connect"
@@ -405,13 +444,21 @@ var AstralformClient = class {
405
444
  await this.handleError(response);
406
445
  return response;
407
446
  }
447
+ // DO NOT refactor these back into `request()` + `.json()`. Parsing the body
448
+ // INSIDE the raced callback is the entire fix: `json()` outside the deadline
449
+ // is the original bug (headers arrive, body stalls, caller hangs forever).
450
+ // `request()` survives for `del()`, which never reads the body.
408
451
  async get(path) {
409
- const response = await this.request("GET", path);
410
- return response.json();
452
+ return this.withDeadline(async (signal) => {
453
+ const response = await this.send("GET", path, void 0, signal);
454
+ return await response.json();
455
+ });
411
456
  }
412
457
  async post(path, body) {
413
- const response = await this.request("POST", path, body);
414
- return response.json();
458
+ return this.withDeadline(async (signal) => {
459
+ const response = await this.send("POST", path, body, signal);
460
+ return await response.json();
461
+ });
415
462
  }
416
463
  async del(path) {
417
464
  await this.request("DELETE", path);
@@ -1302,6 +1349,7 @@ var ChatSession = class {
1302
1349
  upload_ids: options?.uploadIds,
1303
1350
  agent_name: options?.agentName,
1304
1351
  plan_mode: options?.planMode,
1352
+ goal: options?.goal,
1305
1353
  // Per-request model choice (client-side model selection).
1306
1354
  provider: options?.provider,
1307
1355
  model: options?.model,
@@ -1865,6 +1913,7 @@ var StreamManager = class {
1865
1913
  agentName: options?.agentName,
1866
1914
  uploadIds: options?.uploadIds,
1867
1915
  planMode: options?.planMode,
1916
+ goal: options?.goal,
1868
1917
  provider: options?.provider,
1869
1918
  model: options?.model,
1870
1919
  reasoningEffort: options?.reasoningEffort,