@astralform/js 1.0.0 → 1.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
@@ -641,6 +641,27 @@ interface ToolApprovalRequest {
641
641
  decision: ToolApprovalDecision;
642
642
  scope: ToolApprovalScope;
643
643
  }
644
+ /**
645
+ * A remembered tool-permission grant belonging to the current end user.
646
+ * Only `conversation`/`always` grants are ever stored (`once` is consumed at
647
+ * approval time and never persisted).
648
+ */
649
+ interface ToolGrant {
650
+ id: string;
651
+ toolName: string;
652
+ decision: ToolApprovalDecision;
653
+ scope: Exclude<ToolApprovalScope, "once">;
654
+ /** Set for `conversation`-scoped grants; `null` for `always`. */
655
+ conversationId: string | null;
656
+ createdAt: string;
657
+ }
658
+ /** A page of the current end user's own tool grants. */
659
+ interface MyToolGrantsPage {
660
+ grants: ToolGrant[];
661
+ total: number;
662
+ limit: number;
663
+ offset: number;
664
+ }
644
665
  interface ToolDefinition {
645
666
  name: string;
646
667
  description: string;
@@ -759,6 +780,21 @@ declare class AstralformClient {
759
780
  getConversationEvents(conversationId: string, jobId?: string): Promise<ConversationEvent[]>;
760
781
  submitToolResult(request: ToolResultRequest): Promise<void>;
761
782
  submitToolApproval(request: ToolApprovalRequest): Promise<void>;
783
+ /**
784
+ * List the current end user's own remembered tool-permission grants.
785
+ * Only `conversation`/`always` grants exist (`once` is never persisted).
786
+ * Paginated via `limit` (default 100, max 200) / `offset`; `total` lets you
787
+ * page through all of them.
788
+ */
789
+ getMyToolPermissions(options?: {
790
+ limit?: number;
791
+ offset?: number;
792
+ }): Promise<MyToolGrantsPage>;
793
+ /**
794
+ * Revoke one of the current end user's remembered grants by id. The agent
795
+ * will ask again the next time that tool is used.
796
+ */
797
+ revokeToolPermission(id: string): Promise<void>;
762
798
  private mapAsset;
763
799
  uploadFile(conversationId: string, file: Blob, filename?: string): Promise<ConversationAsset>;
764
800
  listUploads(conversationId: string): Promise<ConversationAsset[]>;
@@ -881,6 +917,13 @@ declare class ChatSession {
881
917
  private processStream;
882
918
  /** Last received sequence number for resumable reconnection */
883
919
  private lastSeq;
920
+ /**
921
+ * Client-tool call_ids whose result was already submitted this turn. On a
922
+ * reconnect the resumed stream can replay a tool request we already handled;
923
+ * this dedups so each is executed + submitted at most once (but a request we
924
+ * never submitted still runs). Cleared at the start of each turn.
925
+ */
926
+ private submittedToolCallIds;
884
927
  /** Current job ID for cancellation */
885
928
  currentJobId: string | null;
886
929
  private consumeJobStream;
@@ -889,6 +932,16 @@ declare class ChatSession {
889
932
  * minimal session state, and emits typed ChatEvents to consumers.
890
933
  */
891
934
  private consumeEventStream;
935
+ /**
936
+ * Consume a single SSE stream to exhaustion. Returns whether a terminal
937
+ * event (``message_stop`` / ``error``) was seen, so the caller can decide
938
+ * whether an ended stream means "turn done" vs "dropped, reconnect".
939
+ */
940
+ private pumpStream;
941
+ /** Sleep for ``ms``, resolving early if the turn is aborted mid-backoff. */
942
+ private sleepUnlessAborted;
943
+ /** POST a client-tool result, retrying transient failures a few times. */
944
+ private submitToolResultWithRetry;
892
945
  private dispatchWireEvent;
893
946
  /**
894
947
  * State mutations driven by wire events. Kept separate from translation so
@@ -1122,4 +1175,4 @@ declare function isEmbeddedResource(value: unknown): value is {
1122
1175
  */
1123
1176
  declare function parseEmbeddedResource(value: unknown): EmbeddedResource | null;
1124
1177
 
1125
- export { type ActiveJob, type AgentIdentity, type AgentInfo, type AssetCreatedPayload, type AstralformApiKeyConfig, AstralformClient, type AstralformConfig, AstralformError, type AstralformUserTokenConfig, type AttachmentStagedPayload, AuthenticationError, type BlockDeltaPayload, type ChatEvent, ChatEventType, type ChatEventTypeValue, ChatSession, type ChatStorage, type ChatStreamEvent, type ChatStreamRequest, ConnectionError, type ContextUpdatePayload, type ContextWarningPayload, type Conversation, type ConversationAsset, type ConversationEvent, type DesktopStreamPayload, type EmbeddedResource, type FeedbackRequest, type FeedbackResponse, InMemoryStorage, type JobCreateResponse, type JobStatus, type JobSummary, LLMNotConfiguredError, type MemoryRecallPayload, type MemoryRecord, type MemoryUpdatePayload, type Message, type ProjectStatus, type ProjectSummary, type PromptSuggestionPayload, type ProtocolAdapter, ProtocolRegistry, RateLimitError, type RateLimitErrorDetails, type RawSseEvent, type SendOptions, ServerError, type SkillInfo, StreamAbortedError, type StreamJobSSEOptions, StreamManager, type StreamManagerEvent, type StreamState, type SubagentStartPayload, type SubagentStopPayload, type TaskStatus, type TeamSummary, type TitleGeneratedPayload, type TodoItem, type TodoUpdatePayload, type ToolApprovalDecision, type ToolApprovalGrantedPayload, type ToolApprovalRequest, type ToolApprovalRequestedPayload, type ToolApprovalScope, type ToolCallRequest, type ToolDefinition, type ToolHandler, type ToolHarnessWarningPayload, type ToolPermissionDeniedPayload, ToolRegistry, type ToolResult, type ToolResultRequest, type TurnUsage, type UIComponentsConfig, type UserUnavailablePayload, type WireBlockDelta, type WireBlockDeltaPayload, type WireBlockKind, type WireBlockStart, type WireBlockStatus, type WireBlockStop, type WireCustomEvent, type WireErrorEvent, type WireEvent, type WireInputArgDelta, type WireInputDelta, type WireKeepalive, type WireMessageStart, type WireMessageStop, type WireOutputDelta, type WireRetryEvent, type WireSignatureDelta, type WireStallWarning, type WireStatusDelta, type WireStopReason, type WireTextDelta, type WireThinkingDelta, type WorkspaceReadyPayload, generateId, isEmbeddedResource, mapSseToChat, parseEmbeddedResource, replayEvents, streamJobSSE, translateDelta };
1178
+ export { type ActiveJob, type AgentIdentity, type AgentInfo, type AssetCreatedPayload, type AstralformApiKeyConfig, AstralformClient, type AstralformConfig, AstralformError, type AstralformUserTokenConfig, type AttachmentStagedPayload, AuthenticationError, type BlockDeltaPayload, type ChatEvent, ChatEventType, type ChatEventTypeValue, ChatSession, type ChatStorage, type ChatStreamEvent, type ChatStreamRequest, ConnectionError, type ContextUpdatePayload, type ContextWarningPayload, type Conversation, type ConversationAsset, type ConversationEvent, type DesktopStreamPayload, type EmbeddedResource, type FeedbackRequest, type FeedbackResponse, InMemoryStorage, type JobCreateResponse, type JobStatus, type JobSummary, LLMNotConfiguredError, type MemoryRecallPayload, type MemoryRecord, type MemoryUpdatePayload, type Message, type MyToolGrantsPage, type ProjectStatus, type ProjectSummary, type PromptSuggestionPayload, type ProtocolAdapter, ProtocolRegistry, RateLimitError, type RateLimitErrorDetails, type RawSseEvent, type SendOptions, ServerError, type SkillInfo, StreamAbortedError, type StreamJobSSEOptions, StreamManager, type StreamManagerEvent, type StreamState, type SubagentStartPayload, type SubagentStopPayload, type TaskStatus, type TeamSummary, type TitleGeneratedPayload, type TodoItem, type TodoUpdatePayload, type ToolApprovalDecision, type ToolApprovalGrantedPayload, type ToolApprovalRequest, type ToolApprovalRequestedPayload, type ToolApprovalScope, type ToolCallRequest, type ToolDefinition, type ToolGrant, type ToolHandler, type ToolHarnessWarningPayload, type ToolPermissionDeniedPayload, ToolRegistry, type ToolResult, type ToolResultRequest, type TurnUsage, type UIComponentsConfig, type UserUnavailablePayload, type WireBlockDelta, type WireBlockDeltaPayload, type WireBlockKind, type WireBlockStart, type WireBlockStatus, type WireBlockStop, type WireCustomEvent, type WireErrorEvent, type WireEvent, type WireInputArgDelta, type WireInputDelta, type WireKeepalive, type WireMessageStart, type WireMessageStop, type WireOutputDelta, type WireRetryEvent, type WireSignatureDelta, type WireStallWarning, type WireStatusDelta, type WireStopReason, type WireTextDelta, type WireThinkingDelta, type WorkspaceReadyPayload, generateId, isEmbeddedResource, mapSseToChat, parseEmbeddedResource, replayEvents, streamJobSSE, translateDelta };
package/dist/index.d.ts CHANGED
@@ -641,6 +641,27 @@ interface ToolApprovalRequest {
641
641
  decision: ToolApprovalDecision;
642
642
  scope: ToolApprovalScope;
643
643
  }
644
+ /**
645
+ * A remembered tool-permission grant belonging to the current end user.
646
+ * Only `conversation`/`always` grants are ever stored (`once` is consumed at
647
+ * approval time and never persisted).
648
+ */
649
+ interface ToolGrant {
650
+ id: string;
651
+ toolName: string;
652
+ decision: ToolApprovalDecision;
653
+ scope: Exclude<ToolApprovalScope, "once">;
654
+ /** Set for `conversation`-scoped grants; `null` for `always`. */
655
+ conversationId: string | null;
656
+ createdAt: string;
657
+ }
658
+ /** A page of the current end user's own tool grants. */
659
+ interface MyToolGrantsPage {
660
+ grants: ToolGrant[];
661
+ total: number;
662
+ limit: number;
663
+ offset: number;
664
+ }
644
665
  interface ToolDefinition {
645
666
  name: string;
646
667
  description: string;
@@ -759,6 +780,21 @@ declare class AstralformClient {
759
780
  getConversationEvents(conversationId: string, jobId?: string): Promise<ConversationEvent[]>;
760
781
  submitToolResult(request: ToolResultRequest): Promise<void>;
761
782
  submitToolApproval(request: ToolApprovalRequest): Promise<void>;
783
+ /**
784
+ * List the current end user's own remembered tool-permission grants.
785
+ * Only `conversation`/`always` grants exist (`once` is never persisted).
786
+ * Paginated via `limit` (default 100, max 200) / `offset`; `total` lets you
787
+ * page through all of them.
788
+ */
789
+ getMyToolPermissions(options?: {
790
+ limit?: number;
791
+ offset?: number;
792
+ }): Promise<MyToolGrantsPage>;
793
+ /**
794
+ * Revoke one of the current end user's remembered grants by id. The agent
795
+ * will ask again the next time that tool is used.
796
+ */
797
+ revokeToolPermission(id: string): Promise<void>;
762
798
  private mapAsset;
763
799
  uploadFile(conversationId: string, file: Blob, filename?: string): Promise<ConversationAsset>;
764
800
  listUploads(conversationId: string): Promise<ConversationAsset[]>;
@@ -881,6 +917,13 @@ declare class ChatSession {
881
917
  private processStream;
882
918
  /** Last received sequence number for resumable reconnection */
883
919
  private lastSeq;
920
+ /**
921
+ * Client-tool call_ids whose result was already submitted this turn. On a
922
+ * reconnect the resumed stream can replay a tool request we already handled;
923
+ * this dedups so each is executed + submitted at most once (but a request we
924
+ * never submitted still runs). Cleared at the start of each turn.
925
+ */
926
+ private submittedToolCallIds;
884
927
  /** Current job ID for cancellation */
885
928
  currentJobId: string | null;
886
929
  private consumeJobStream;
@@ -889,6 +932,16 @@ declare class ChatSession {
889
932
  * minimal session state, and emits typed ChatEvents to consumers.
890
933
  */
891
934
  private consumeEventStream;
935
+ /**
936
+ * Consume a single SSE stream to exhaustion. Returns whether a terminal
937
+ * event (``message_stop`` / ``error``) was seen, so the caller can decide
938
+ * whether an ended stream means "turn done" vs "dropped, reconnect".
939
+ */
940
+ private pumpStream;
941
+ /** Sleep for ``ms``, resolving early if the turn is aborted mid-backoff. */
942
+ private sleepUnlessAborted;
943
+ /** POST a client-tool result, retrying transient failures a few times. */
944
+ private submitToolResultWithRetry;
892
945
  private dispatchWireEvent;
893
946
  /**
894
947
  * State mutations driven by wire events. Kept separate from translation so
@@ -1122,4 +1175,4 @@ declare function isEmbeddedResource(value: unknown): value is {
1122
1175
  */
1123
1176
  declare function parseEmbeddedResource(value: unknown): EmbeddedResource | null;
1124
1177
 
1125
- export { type ActiveJob, type AgentIdentity, type AgentInfo, type AssetCreatedPayload, type AstralformApiKeyConfig, AstralformClient, type AstralformConfig, AstralformError, type AstralformUserTokenConfig, type AttachmentStagedPayload, AuthenticationError, type BlockDeltaPayload, type ChatEvent, ChatEventType, type ChatEventTypeValue, ChatSession, type ChatStorage, type ChatStreamEvent, type ChatStreamRequest, ConnectionError, type ContextUpdatePayload, type ContextWarningPayload, type Conversation, type ConversationAsset, type ConversationEvent, type DesktopStreamPayload, type EmbeddedResource, type FeedbackRequest, type FeedbackResponse, InMemoryStorage, type JobCreateResponse, type JobStatus, type JobSummary, LLMNotConfiguredError, type MemoryRecallPayload, type MemoryRecord, type MemoryUpdatePayload, type Message, type ProjectStatus, type ProjectSummary, type PromptSuggestionPayload, type ProtocolAdapter, ProtocolRegistry, RateLimitError, type RateLimitErrorDetails, type RawSseEvent, type SendOptions, ServerError, type SkillInfo, StreamAbortedError, type StreamJobSSEOptions, StreamManager, type StreamManagerEvent, type StreamState, type SubagentStartPayload, type SubagentStopPayload, type TaskStatus, type TeamSummary, type TitleGeneratedPayload, type TodoItem, type TodoUpdatePayload, type ToolApprovalDecision, type ToolApprovalGrantedPayload, type ToolApprovalRequest, type ToolApprovalRequestedPayload, type ToolApprovalScope, type ToolCallRequest, type ToolDefinition, type ToolHandler, type ToolHarnessWarningPayload, type ToolPermissionDeniedPayload, ToolRegistry, type ToolResult, type ToolResultRequest, type TurnUsage, type UIComponentsConfig, type UserUnavailablePayload, type WireBlockDelta, type WireBlockDeltaPayload, type WireBlockKind, type WireBlockStart, type WireBlockStatus, type WireBlockStop, type WireCustomEvent, type WireErrorEvent, type WireEvent, type WireInputArgDelta, type WireInputDelta, type WireKeepalive, type WireMessageStart, type WireMessageStop, type WireOutputDelta, type WireRetryEvent, type WireSignatureDelta, type WireStallWarning, type WireStatusDelta, type WireStopReason, type WireTextDelta, type WireThinkingDelta, type WorkspaceReadyPayload, generateId, isEmbeddedResource, mapSseToChat, parseEmbeddedResource, replayEvents, streamJobSSE, translateDelta };
1178
+ export { type ActiveJob, type AgentIdentity, type AgentInfo, type AssetCreatedPayload, type AstralformApiKeyConfig, AstralformClient, type AstralformConfig, AstralformError, type AstralformUserTokenConfig, type AttachmentStagedPayload, AuthenticationError, type BlockDeltaPayload, type ChatEvent, ChatEventType, type ChatEventTypeValue, ChatSession, type ChatStorage, type ChatStreamEvent, type ChatStreamRequest, ConnectionError, type ContextUpdatePayload, type ContextWarningPayload, type Conversation, type ConversationAsset, type ConversationEvent, type DesktopStreamPayload, type EmbeddedResource, type FeedbackRequest, type FeedbackResponse, InMemoryStorage, type JobCreateResponse, type JobStatus, type JobSummary, LLMNotConfiguredError, type MemoryRecallPayload, type MemoryRecord, type MemoryUpdatePayload, type Message, type MyToolGrantsPage, type ProjectStatus, type ProjectSummary, type PromptSuggestionPayload, type ProtocolAdapter, ProtocolRegistry, RateLimitError, type RateLimitErrorDetails, type RawSseEvent, type SendOptions, ServerError, type SkillInfo, StreamAbortedError, type StreamJobSSEOptions, StreamManager, type StreamManagerEvent, type StreamState, type SubagentStartPayload, type SubagentStopPayload, type TaskStatus, type TeamSummary, type TitleGeneratedPayload, type TodoItem, type TodoUpdatePayload, type ToolApprovalDecision, type ToolApprovalGrantedPayload, type ToolApprovalRequest, type ToolApprovalRequestedPayload, type ToolApprovalScope, type ToolCallRequest, type ToolDefinition, type ToolGrant, type ToolHandler, type ToolHarnessWarningPayload, type ToolPermissionDeniedPayload, ToolRegistry, type ToolResult, type ToolResultRequest, type TurnUsage, type UIComponentsConfig, type UserUnavailablePayload, type WireBlockDelta, type WireBlockDeltaPayload, type WireBlockKind, type WireBlockStart, type WireBlockStatus, type WireBlockStop, type WireCustomEvent, type WireErrorEvent, type WireEvent, type WireInputArgDelta, type WireInputDelta, type WireKeepalive, type WireMessageStart, type WireMessageStop, type WireOutputDelta, type WireRetryEvent, type WireSignatureDelta, type WireStallWarning, type WireStatusDelta, type WireStopReason, type WireTextDelta, type WireThinkingDelta, type WorkspaceReadyPayload, generateId, isEmbeddedResource, mapSseToChat, parseEmbeddedResource, replayEvents, streamJobSSE, translateDelta };
package/dist/index.js CHANGED
@@ -508,6 +508,49 @@ var AstralformClient = class {
508
508
  async submitToolApproval(request) {
509
509
  await this.post("/v1/tool-approval", request);
510
510
  }
511
+ // --- End-user tool-permission self-service ---
512
+ /**
513
+ * List the current end user's own remembered tool-permission grants.
514
+ * Only `conversation`/`always` grants exist (`once` is never persisted).
515
+ * Paginated via `limit` (default 100, max 200) / `offset`; `total` lets you
516
+ * page through all of them.
517
+ */
518
+ async getMyToolPermissions(options) {
519
+ const params = new URLSearchParams();
520
+ if (options?.limit != null) {
521
+ const safeLimit = Math.max(
522
+ 1,
523
+ Math.min(200, Math.floor(Number(options.limit)))
524
+ );
525
+ params.set("limit", String(safeLimit));
526
+ }
527
+ if (options?.offset != null) {
528
+ const safeOffset = Math.max(0, Math.floor(Number(options.offset)));
529
+ params.set("offset", String(safeOffset));
530
+ }
531
+ const qs = params.toString();
532
+ const raw = await this.get(`/v1/me/tool-permissions${qs ? `?${qs}` : ""}`);
533
+ return {
534
+ grants: raw.grants.map((g) => ({
535
+ id: g.id,
536
+ toolName: g.tool_name,
537
+ decision: g.decision,
538
+ scope: g.scope,
539
+ conversationId: g.conversation_id,
540
+ createdAt: g.created_at
541
+ })),
542
+ total: raw.total,
543
+ limit: raw.limit,
544
+ offset: raw.offset
545
+ };
546
+ }
547
+ /**
548
+ * Revoke one of the current end user's remembered grants by id. The agent
549
+ * will ask again the next time that tool is used.
550
+ */
551
+ async revokeToolPermission(id) {
552
+ await this.del(`/v1/me/tool-permissions/${encodeURIComponent(id)}`);
553
+ }
511
554
  // --- Conversation Assets ---
512
555
  mapAsset(raw) {
513
556
  return {
@@ -1101,6 +1144,11 @@ function translateWireEvent(wire) {
1101
1144
  }
1102
1145
 
1103
1146
  // src/session.ts
1147
+ var SSE_MAX_RECONNECTS = 6;
1148
+ var TOOL_RESULT_MAX_RETRIES = 3;
1149
+ function sseReconnectDelayMs(attempt) {
1150
+ return Math.min(500 * 2 ** (attempt - 1), 5e3);
1151
+ }
1104
1152
  function pathEquals(a, b) {
1105
1153
  if (a.length !== b.length) return false;
1106
1154
  for (let i = 0; i < a.length; i++) {
@@ -1137,6 +1185,13 @@ var ChatSession = class {
1137
1185
  this.abortController = null;
1138
1186
  /** Last received sequence number for resumable reconnection */
1139
1187
  this.lastSeq = -1;
1188
+ /**
1189
+ * Client-tool call_ids whose result was already submitted this turn. On a
1190
+ * reconnect the resumed stream can replay a tool request we already handled;
1191
+ * this dedups so each is executed + submitted at most once (but a request we
1192
+ * never submitted still runs). Cleared at the start of each turn.
1193
+ */
1194
+ this.submittedToolCallIds = /* @__PURE__ */ new Set();
1140
1195
  /** Current job ID for cancellation */
1141
1196
  this.currentJobId = null;
1142
1197
  this.client = new AstralformClient(config);
@@ -1271,13 +1326,9 @@ var ChatSession = class {
1271
1326
  }
1272
1327
  const messageId = job.message_id;
1273
1328
  this.lastSeq = -1;
1274
- const stream = this.client.streamJobEvents(
1275
- job.job_id,
1276
- this.lastSeq,
1277
- this.abortController?.signal
1278
- );
1329
+ this.submittedToolCallIds.clear();
1279
1330
  await this.consumeEventStream(
1280
- stream,
1331
+ job.job_id,
1281
1332
  conversationId,
1282
1333
  messageId,
1283
1334
  true
@@ -1288,7 +1339,41 @@ var ChatSession = class {
1288
1339
  * Shared event consumption loop. Parses each wire event, updates
1289
1340
  * minimal session state, and emits typed ChatEvents to consumers.
1290
1341
  */
1291
- async consumeEventStream(stream, conversationId, messageId, executeClientTools) {
1342
+ async consumeEventStream(jobId, conversationId, messageId, executeClientTools) {
1343
+ const signal = this.abortController?.signal;
1344
+ for (let attempt = 0; ; attempt++) {
1345
+ const stream = this.client.streamJobEvents(jobId, this.lastSeq, signal);
1346
+ let sawTerminal;
1347
+ try {
1348
+ sawTerminal = await this.pumpStream(
1349
+ stream,
1350
+ conversationId,
1351
+ messageId,
1352
+ executeClientTools
1353
+ );
1354
+ } catch (err) {
1355
+ if (signal?.aborted) return;
1356
+ if (err instanceof AuthenticationError || err instanceof RateLimitError) {
1357
+ throw err;
1358
+ }
1359
+ if (attempt >= SSE_MAX_RECONNECTS) throw err;
1360
+ await this.sleepUnlessAborted(sseReconnectDelayMs(attempt + 1), signal);
1361
+ continue;
1362
+ }
1363
+ if (sawTerminal || signal?.aborted) return;
1364
+ if (attempt >= SSE_MAX_RECONNECTS) {
1365
+ throw new ConnectionError("Lost connection to the response stream.");
1366
+ }
1367
+ await this.sleepUnlessAborted(sseReconnectDelayMs(attempt + 1), signal);
1368
+ }
1369
+ }
1370
+ /**
1371
+ * Consume a single SSE stream to exhaustion. Returns whether a terminal
1372
+ * event (``message_stop`` / ``error``) was seen, so the caller can decide
1373
+ * whether an ended stream means "turn done" vs "dropped, reconnect".
1374
+ */
1375
+ async pumpStream(stream, conversationId, messageId, executeClientTools) {
1376
+ let sawTerminal = false;
1292
1377
  for await (const raw of stream) {
1293
1378
  let parsed;
1294
1379
  try {
@@ -1306,6 +1391,9 @@ var ChatSession = class {
1306
1391
  } catch {
1307
1392
  continue;
1308
1393
  }
1394
+ if (parsed.type === "message_stop" || parsed.type === "error") {
1395
+ sawTerminal = true;
1396
+ }
1309
1397
  await this.dispatchWireEvent(
1310
1398
  parsed,
1311
1399
  conversationId,
@@ -1313,6 +1401,39 @@ var ChatSession = class {
1313
1401
  executeClientTools
1314
1402
  );
1315
1403
  }
1404
+ return sawTerminal;
1405
+ }
1406
+ /** Sleep for ``ms``, resolving early if the turn is aborted mid-backoff. */
1407
+ sleepUnlessAborted(ms, signal) {
1408
+ return new Promise((resolve) => {
1409
+ if (signal?.aborted) return resolve();
1410
+ const timer = setTimeout(() => {
1411
+ signal?.removeEventListener("abort", onAbort);
1412
+ resolve();
1413
+ }, ms);
1414
+ const onAbort = () => {
1415
+ clearTimeout(timer);
1416
+ resolve();
1417
+ };
1418
+ signal?.addEventListener("abort", onAbort, { once: true });
1419
+ });
1420
+ }
1421
+ /** POST a client-tool result, retrying transient failures a few times. */
1422
+ async submitToolResultWithRetry(payload) {
1423
+ const signal = this.abortController?.signal;
1424
+ for (let attempt = 0; ; attempt++) {
1425
+ try {
1426
+ await this.client.submitToolResult(payload);
1427
+ return;
1428
+ } catch (err) {
1429
+ if (signal?.aborted) throw err;
1430
+ if (err instanceof AuthenticationError || err instanceof RateLimitError) {
1431
+ throw err;
1432
+ }
1433
+ if (attempt >= TOOL_RESULT_MAX_RETRIES) throw err;
1434
+ await this.sleepUnlessAborted(sseReconnectDelayMs(attempt + 1), signal);
1435
+ }
1436
+ }
1316
1437
  }
1317
1438
  async dispatchWireEvent(wire, conversationId, messageId, executeClientTools) {
1318
1439
  this.applyWireSideEffects(wire, conversationId, messageId);
@@ -1322,18 +1443,22 @@ var ChatSession = class {
1322
1443
  }
1323
1444
  if (executeClientTools && wire.type === "block_stop" && wire.status === "awaiting_client_result" && wire.final?.call_id) {
1324
1445
  const f = wire.final;
1325
- const request = {
1326
- callId: f.call_id ?? "",
1327
- toolName: f.tool_name ?? "",
1328
- arguments: f.input ?? {},
1329
- isClientTool: true
1330
- };
1331
- const results = await this.executeClientTools([request]);
1332
- await this.client.submitToolResult({
1333
- conversation_id: conversationId,
1334
- message_id: messageId,
1335
- tool_results: results
1336
- });
1446
+ const callId = f.call_id ?? "";
1447
+ if (callId && !this.submittedToolCallIds.has(callId)) {
1448
+ const request = {
1449
+ callId,
1450
+ toolName: f.tool_name ?? "",
1451
+ arguments: f.input ?? {},
1452
+ isClientTool: true
1453
+ };
1454
+ const results = await this.executeClientTools([request]);
1455
+ await this.submitToolResultWithRetry({
1456
+ conversation_id: conversationId,
1457
+ message_id: messageId,
1458
+ tool_results: results
1459
+ });
1460
+ this.submittedToolCallIds.add(callId);
1461
+ }
1337
1462
  }
1338
1463
  }
1339
1464
  /**
@@ -1430,16 +1555,12 @@ var ChatSession = class {
1430
1555
  this.isStreaming = true;
1431
1556
  this.currentJobId = jobId;
1432
1557
  this.lastSeq = -1;
1558
+ this.submittedToolCallIds.clear();
1433
1559
  this.resetStreamingState();
1434
1560
  this.abortController = new AbortController();
1435
1561
  try {
1436
- const stream = this.client.streamJobEvents(
1437
- jobId,
1438
- this.lastSeq,
1439
- this.abortController?.signal
1440
- );
1441
1562
  await this.consumeEventStream(
1442
- stream,
1563
+ jobId,
1443
1564
  this.conversationId ?? "",
1444
1565
  "",
1445
1566
  false