@octavus/client-sdk 1.0.0 → 2.1.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.js CHANGED
@@ -181,6 +181,22 @@ var OctavusChat = class {
181
181
  options;
182
182
  transport;
183
183
  streamingState = null;
184
+ // Client tool state
185
+ // Keyed by toolName -> array of pending tools for that name
186
+ _pendingToolsByName = /* @__PURE__ */ new Map();
187
+ // Keyed by toolCallId -> pending tool state (for internal lookup when submitting)
188
+ _pendingToolsByCallId = /* @__PURE__ */ new Map();
189
+ // Cache for React useSyncExternalStore compatibility
190
+ _pendingClientToolsCache = {};
191
+ _completedToolResults = [];
192
+ _clientToolAbortController = null;
193
+ // Server tool results from mixed server+client tools (for continuation)
194
+ _serverToolResults = [];
195
+ // Execution ID for continuation (from client-tool-request event)
196
+ _pendingExecutionId = null;
197
+ // Flag indicating automatic client tools have completed and are ready to continue
198
+ // We wait for the finish event before actually continuing to avoid race conditions
199
+ _readyToContinue = false;
184
200
  // Listener sets for reactive frameworks
185
201
  listeners = /* @__PURE__ */ new Set();
186
202
  constructor(options) {
@@ -204,6 +220,27 @@ var OctavusChat = class {
204
220
  get error() {
205
221
  return this._error;
206
222
  }
223
+ /**
224
+ * Pending interactive tool calls keyed by tool name.
225
+ * Each tool has bound `submit()` and `cancel()` methods.
226
+ *
227
+ * @example
228
+ * ```tsx
229
+ * const feedbackTools = pendingClientTools['request-feedback'] ?? [];
230
+ *
231
+ * {feedbackTools.map(tool => (
232
+ * <FeedbackModal
233
+ * key={tool.toolCallId}
234
+ * {...tool.args}
235
+ * onSubmit={(result) => tool.submit(result)}
236
+ * onCancel={() => tool.cancel()}
237
+ * />
238
+ * ))}
239
+ * ```
240
+ */
241
+ get pendingClientTools() {
242
+ return this._pendingClientToolsCache;
243
+ }
207
244
  // =========================================================================
208
245
  // Subscription Methods (for reactive frameworks)
209
246
  // =========================================================================
@@ -233,6 +270,19 @@ var OctavusChat = class {
233
270
  this._error = error;
234
271
  this.notifyListeners();
235
272
  }
273
+ updatePendingClientToolsCache() {
274
+ const cache = {};
275
+ for (const [toolName, tools] of this._pendingToolsByName.entries()) {
276
+ cache[toolName] = tools.map((tool) => ({
277
+ toolCallId: tool.toolCallId,
278
+ toolName: tool.toolName,
279
+ args: tool.args,
280
+ submit: (result) => this.submitToolResult(tool.toolCallId, result),
281
+ cancel: (reason) => this.submitToolResult(tool.toolCallId, void 0, reason ?? "User cancelled")
282
+ }));
283
+ }
284
+ this._pendingClientToolsCache = cache;
285
+ }
236
286
  // =========================================================================
237
287
  // Public Methods
238
288
  // =========================================================================
@@ -294,6 +344,13 @@ var OctavusChat = class {
294
344
  this.setStatus("streaming");
295
345
  this.setError(null);
296
346
  this.streamingState = createEmptyStreamingState();
347
+ this._pendingToolsByName.clear();
348
+ this._pendingToolsByCallId.clear();
349
+ this._completedToolResults = [];
350
+ this._serverToolResults = [];
351
+ this._pendingExecutionId = null;
352
+ this._readyToContinue = false;
353
+ this.updatePendingClientToolsCache();
297
354
  try {
298
355
  for await (const event of this.transport.trigger(triggerName, processedInput)) {
299
356
  if (this.streamingState === null) break;
@@ -307,6 +364,48 @@ var OctavusChat = class {
307
364
  retryable: false,
308
365
  cause: err
309
366
  });
367
+ const state = this.streamingState;
368
+ if (state !== null) {
369
+ const messages = [...this._messages];
370
+ const lastMsg = messages[messages.length - 1];
371
+ if (state.parts.length > 0) {
372
+ const finalParts = state.parts.map((part) => {
373
+ if (part.type === "text" || part.type === "reasoning") {
374
+ if (part.status === "streaming") {
375
+ return { ...part, status: "done" };
376
+ }
377
+ }
378
+ if (part.type === "object" && part.status === "streaming") {
379
+ return { ...part, status: "done" };
380
+ }
381
+ if (part.type === "tool-call") {
382
+ if (part.status === "pending" || part.status === "running") {
383
+ return { ...part, status: "cancelled" };
384
+ }
385
+ }
386
+ if (part.type === "operation" && part.status === "running") {
387
+ return { ...part, status: "cancelled" };
388
+ }
389
+ return part;
390
+ });
391
+ const finalMessage = {
392
+ id: state.messageId,
393
+ role: "assistant",
394
+ parts: finalParts,
395
+ status: "done",
396
+ createdAt: /* @__PURE__ */ new Date()
397
+ };
398
+ if (lastMsg?.id === state.messageId) {
399
+ messages[messages.length - 1] = finalMessage;
400
+ } else {
401
+ messages.push(finalMessage);
402
+ }
403
+ this.setMessages(messages);
404
+ } else if (lastMsg?.id === state.messageId) {
405
+ messages.pop();
406
+ this.setMessages(messages);
407
+ }
408
+ }
310
409
  this.setError(errorObj);
311
410
  this.setStatus("error");
312
411
  this.streamingState = null;
@@ -339,11 +438,59 @@ var OctavusChat = class {
339
438
  onProgress
340
439
  });
341
440
  }
441
+ /**
442
+ * Internal: Submit a result for a pending tool.
443
+ * Called by bound submit/cancel methods on InteractiveTool.
444
+ */
445
+ submitToolResult(toolCallId, result, error) {
446
+ const pendingTool = this._pendingToolsByCallId.get(toolCallId);
447
+ if (!pendingTool) {
448
+ return;
449
+ }
450
+ this._pendingToolsByCallId.delete(toolCallId);
451
+ const toolsForName = this._pendingToolsByName.get(pendingTool.toolName);
452
+ if (toolsForName) {
453
+ const filtered = toolsForName.filter((t) => t.toolCallId !== toolCallId);
454
+ if (filtered.length === 0) {
455
+ this._pendingToolsByName.delete(pendingTool.toolName);
456
+ } else {
457
+ this._pendingToolsByName.set(pendingTool.toolName, filtered);
458
+ }
459
+ }
460
+ this.updatePendingClientToolsCache();
461
+ const toolResult = {
462
+ toolCallId,
463
+ toolName: pendingTool.toolName,
464
+ result: error ? void 0 : result,
465
+ error,
466
+ outputVariable: pendingTool.outputVariable,
467
+ blockIndex: pendingTool.blockIndex
468
+ };
469
+ this._completedToolResults.push(toolResult);
470
+ if (error) {
471
+ this.emitToolOutputError(toolCallId, error);
472
+ } else {
473
+ this.emitToolOutputAvailable(toolCallId, result);
474
+ }
475
+ if (this._pendingToolsByCallId.size === 0) {
476
+ void this.continueWithClientToolResults();
477
+ }
478
+ this.notifyListeners();
479
+ }
342
480
  /** Stop the current streaming and finalize any partial message */
343
481
  stop() {
344
- if (this._status !== "streaming") {
482
+ if (this._status !== "streaming" && this._status !== "awaiting-input") {
345
483
  return;
346
484
  }
485
+ this._clientToolAbortController?.abort();
486
+ this._clientToolAbortController = null;
487
+ this._pendingToolsByName.clear();
488
+ this._pendingToolsByCallId.clear();
489
+ this._completedToolResults = [];
490
+ this._serverToolResults = [];
491
+ this._pendingExecutionId = null;
492
+ this._readyToContinue = false;
493
+ this.updatePendingClientToolsCache();
347
494
  this.transport.stop();
348
495
  const state = this.streamingState;
349
496
  if (state && state.parts.length > 0) {
@@ -679,6 +826,15 @@ var OctavusChat = class {
679
826
  this.options.onResourceUpdate?.(event.name, event.value);
680
827
  break;
681
828
  case "finish": {
829
+ if (event.finishReason === "client-tool-calls") {
830
+ if (this._pendingToolsByCallId.size > 0) {
831
+ this.setStatus("awaiting-input");
832
+ } else if (this._readyToContinue) {
833
+ this._readyToContinue = false;
834
+ void this.continueWithClientToolResults();
835
+ }
836
+ return;
837
+ }
682
838
  const finalMessage = buildMessageFromState(state, "done");
683
839
  finalMessage.parts = finalMessage.parts.map((part) => {
684
840
  if (part.type === "text" || part.type === "reasoning") {
@@ -718,6 +874,11 @@ var OctavusChat = class {
718
874
  }
719
875
  case "tool-request":
720
876
  break;
877
+ case "client-tool-request":
878
+ this._pendingExecutionId = event.executionId;
879
+ this._serverToolResults = event.serverToolResults ?? [];
880
+ void this.handleClientToolRequest(event.toolCalls, state);
881
+ break;
721
882
  }
722
883
  }
723
884
  updateStreamingMessage() {
@@ -733,6 +894,161 @@ var OctavusChat = class {
733
894
  }
734
895
  this.setMessages(messages);
735
896
  }
897
+ /**
898
+ * Emit a tool-output-available event for a client tool result.
899
+ */
900
+ emitToolOutputAvailable(toolCallId, output) {
901
+ const state = this.streamingState;
902
+ if (!state) return;
903
+ const toolPartIndex = state.parts.findIndex(
904
+ (p) => p.type === "tool-call" && p.toolCallId === toolCallId
905
+ );
906
+ if (toolPartIndex >= 0) {
907
+ const part = state.parts[toolPartIndex];
908
+ part.result = output;
909
+ part.status = "done";
910
+ state.parts[toolPartIndex] = { ...part };
911
+ this.updateStreamingMessage();
912
+ }
913
+ }
914
+ /**
915
+ * Emit a tool-output-error event for a client tool result.
916
+ */
917
+ emitToolOutputError(toolCallId, error) {
918
+ const state = this.streamingState;
919
+ if (!state) return;
920
+ const toolPartIndex = state.parts.findIndex(
921
+ (p) => p.type === "tool-call" && p.toolCallId === toolCallId
922
+ );
923
+ if (toolPartIndex >= 0) {
924
+ const part = state.parts[toolPartIndex];
925
+ part.error = error;
926
+ part.status = "error";
927
+ state.parts[toolPartIndex] = { ...part };
928
+ this.updateStreamingMessage();
929
+ }
930
+ }
931
+ /**
932
+ * Continue execution with collected client tool results.
933
+ */
934
+ async continueWithClientToolResults() {
935
+ if (this._completedToolResults.length === 0) return;
936
+ if (this._pendingExecutionId === null) {
937
+ const errorObj = new OctavusError({
938
+ errorType: "internal_error",
939
+ message: "Cannot continue execution: execution ID was lost.",
940
+ source: "client",
941
+ retryable: false
942
+ });
943
+ this.setError(errorObj);
944
+ this.setStatus("error");
945
+ this.options.onError?.(errorObj);
946
+ return;
947
+ }
948
+ const allResults = [...this._serverToolResults, ...this._completedToolResults];
949
+ const executionId = this._pendingExecutionId;
950
+ this._serverToolResults = [];
951
+ this._completedToolResults = [];
952
+ this._pendingExecutionId = null;
953
+ this.setStatus("streaming");
954
+ try {
955
+ for await (const event of this.transport.continueWithToolResults(executionId, allResults)) {
956
+ if (this.streamingState === null) break;
957
+ this.handleStreamEvent(event, this.streamingState);
958
+ }
959
+ } catch (err) {
960
+ const errorObj = OctavusError.isInstance(err) ? err : new OctavusError({
961
+ errorType: "internal_error",
962
+ message: err instanceof Error ? err.message : "Unknown error",
963
+ source: "client",
964
+ retryable: false,
965
+ cause: err
966
+ });
967
+ this.setError(errorObj);
968
+ this.setStatus("error");
969
+ this.streamingState = null;
970
+ this.options.onError?.(errorObj);
971
+ }
972
+ }
973
+ /**
974
+ * Handle client tool request event.
975
+ *
976
+ * IMPORTANT: Interactive tools must be registered synchronously (before any await)
977
+ * to avoid a race condition where the finish event is processed before tools are added.
978
+ */
979
+ async handleClientToolRequest(toolCalls, state) {
980
+ this._clientToolAbortController = new AbortController();
981
+ for (const tc of toolCalls) {
982
+ const handler = this.options.clientTools?.[tc.toolName];
983
+ if (handler === "interactive") {
984
+ const toolState = {
985
+ toolCallId: tc.toolCallId,
986
+ toolName: tc.toolName,
987
+ args: tc.args,
988
+ source: tc.source,
989
+ outputVariable: tc.outputVariable,
990
+ blockIndex: tc.blockIndex
991
+ };
992
+ this._pendingToolsByCallId.set(tc.toolCallId, toolState);
993
+ const existing = this._pendingToolsByName.get(tc.toolName) ?? [];
994
+ this._pendingToolsByName.set(tc.toolName, [...existing, toolState]);
995
+ }
996
+ }
997
+ if (this._pendingToolsByCallId.size > 0) {
998
+ this.updatePendingClientToolsCache();
999
+ }
1000
+ for (const tc of toolCalls) {
1001
+ const handler = this.options.clientTools?.[tc.toolName];
1002
+ if (handler === "interactive") {
1003
+ const toolPartIndex = state.parts.findIndex(
1004
+ (p) => p.type === "tool-call" && p.toolCallId === tc.toolCallId
1005
+ );
1006
+ if (toolPartIndex >= 0) {
1007
+ const part = state.parts[toolPartIndex];
1008
+ state.parts[toolPartIndex] = { ...part };
1009
+ }
1010
+ } else if (handler) {
1011
+ try {
1012
+ const result = await handler(tc.args, {
1013
+ toolCallId: tc.toolCallId,
1014
+ toolName: tc.toolName,
1015
+ signal: this._clientToolAbortController.signal
1016
+ });
1017
+ this._completedToolResults.push({
1018
+ toolCallId: tc.toolCallId,
1019
+ toolName: tc.toolName,
1020
+ result,
1021
+ outputVariable: tc.outputVariable,
1022
+ blockIndex: tc.blockIndex
1023
+ });
1024
+ this.emitToolOutputAvailable(tc.toolCallId, result);
1025
+ } catch (err) {
1026
+ const errorMessage = err instanceof Error ? err.message : "Tool execution failed";
1027
+ this._completedToolResults.push({
1028
+ toolCallId: tc.toolCallId,
1029
+ toolName: tc.toolName,
1030
+ error: errorMessage,
1031
+ outputVariable: tc.outputVariable,
1032
+ blockIndex: tc.blockIndex
1033
+ });
1034
+ this.emitToolOutputError(tc.toolCallId, errorMessage);
1035
+ }
1036
+ } else {
1037
+ const errorMessage = `No client handler for tool: ${tc.toolName}`;
1038
+ this._completedToolResults.push({
1039
+ toolCallId: tc.toolCallId,
1040
+ toolName: tc.toolName,
1041
+ error: errorMessage,
1042
+ outputVariable: tc.outputVariable,
1043
+ blockIndex: tc.blockIndex
1044
+ });
1045
+ this.emitToolOutputError(tc.toolCallId, errorMessage);
1046
+ }
1047
+ }
1048
+ if (this._pendingToolsByCallId.size === 0 && this._completedToolResults.length > 0) {
1049
+ this._readyToContinue = true;
1050
+ }
1051
+ }
736
1052
  };
737
1053
 
738
1054
  // src/stream/reader.ts
@@ -793,32 +1109,45 @@ function isSocketTransport(transport) {
793
1109
  import { isAbortError as isAbortError2 } from "@octavus/core";
794
1110
  function createHttpTransport(options) {
795
1111
  let abortController = null;
1112
+ async function* streamResponse(responsePromise) {
1113
+ try {
1114
+ const response = await responsePromise;
1115
+ if (!response.ok) {
1116
+ const errorText = await response.text().catch(() => `Request failed: ${response.status}`);
1117
+ throw new Error(errorText);
1118
+ }
1119
+ if (!response.body) {
1120
+ throw new Error("Response body is empty");
1121
+ }
1122
+ for await (const event of parseSSEStream(response, abortController.signal)) {
1123
+ if (abortController?.signal.aborted) {
1124
+ break;
1125
+ }
1126
+ yield event;
1127
+ }
1128
+ } catch (err) {
1129
+ if (isAbortError2(err)) {
1130
+ return;
1131
+ }
1132
+ throw err;
1133
+ }
1134
+ }
796
1135
  return {
797
1136
  async *trigger(triggerName, input) {
798
1137
  abortController = new AbortController();
799
- try {
800
- const response = await options.triggerRequest(triggerName, input, {
801
- signal: abortController.signal
802
- });
803
- if (!response.ok) {
804
- const errorText = await response.text().catch(() => `Request failed: ${response.status}`);
805
- throw new Error(errorText);
806
- }
807
- if (!response.body) {
808
- throw new Error("Response body is empty");
809
- }
810
- for await (const event of parseSSEStream(response, abortController.signal)) {
811
- if (abortController.signal.aborted) {
812
- break;
813
- }
814
- yield event;
815
- }
816
- } catch (err) {
817
- if (isAbortError2(err)) {
818
- return;
819
- }
820
- throw err;
821
- }
1138
+ const response = options.request(
1139
+ { type: "trigger", triggerName, input },
1140
+ { signal: abortController.signal }
1141
+ );
1142
+ yield* streamResponse(response);
1143
+ },
1144
+ async *continueWithToolResults(executionId, toolResults) {
1145
+ abortController = new AbortController();
1146
+ const response = options.request(
1147
+ { type: "continue", executionId, toolResults },
1148
+ { signal: abortController.signal }
1149
+ );
1150
+ yield* streamResponse(response);
822
1151
  },
823
1152
  stop() {
824
1153
  abortController?.abort();
@@ -950,6 +1279,7 @@ function createSocketTransport(options) {
950
1279
  async *trigger(triggerName, input) {
951
1280
  await ensureConnected();
952
1281
  eventQueue = [];
1282
+ eventResolver = null;
953
1283
  isStreaming = true;
954
1284
  socket.send(
955
1285
  JSON.stringify({
@@ -974,18 +1304,105 @@ function createSocketTransport(options) {
974
1304
  eventResolver(null);
975
1305
  eventResolver = null;
976
1306
  }
1307
+ },
1308
+ /**
1309
+ * Continue execution with tool results after client-side tool handling.
1310
+ * @param executionId - The execution ID from the client-tool-request event
1311
+ * @param toolResults - All tool results (server + client) to send
1312
+ */
1313
+ async *continueWithToolResults(executionId, toolResults) {
1314
+ await ensureConnected();
1315
+ eventQueue = [];
1316
+ eventResolver = null;
1317
+ isStreaming = true;
1318
+ socket.send(
1319
+ JSON.stringify({
1320
+ type: "continue",
1321
+ executionId,
1322
+ toolResults
1323
+ })
1324
+ );
1325
+ while (true) {
1326
+ const event = await nextEvent();
1327
+ if (event === null) break;
1328
+ yield event;
1329
+ if (event.type === "finish" || event.type === "error") break;
1330
+ }
977
1331
  }
978
1332
  };
979
1333
  }
980
1334
 
981
1335
  // src/index.ts
982
- export * from "@octavus/core";
1336
+ import {
1337
+ AppError,
1338
+ NotFoundError,
1339
+ ValidationError,
1340
+ ConflictError,
1341
+ ForbiddenError,
1342
+ OctavusError as OctavusError2,
1343
+ isRateLimitError,
1344
+ isAuthenticationError,
1345
+ isProviderError,
1346
+ isToolError,
1347
+ isRetryableError,
1348
+ isValidationError,
1349
+ createErrorEvent,
1350
+ errorToStreamEvent,
1351
+ createInternalErrorEvent,
1352
+ createApiErrorEvent,
1353
+ generateId as generateId2,
1354
+ isAbortError as isAbortError3,
1355
+ MAIN_THREAD,
1356
+ resolveThread,
1357
+ isMainThread,
1358
+ threadForPart as threadForPart2,
1359
+ isOtherThread,
1360
+ isFileReference,
1361
+ isFileReferenceArray as isFileReferenceArray2,
1362
+ safeParseStreamEvent as safeParseStreamEvent3,
1363
+ safeParseUIMessage,
1364
+ safeParseUIMessages,
1365
+ OCTAVUS_SKILL_TOOLS,
1366
+ isOctavusSkillTool,
1367
+ getSkillSlugFromToolCall
1368
+ } from "@octavus/core";
983
1369
  export {
1370
+ AppError,
1371
+ ConflictError,
1372
+ ForbiddenError,
1373
+ MAIN_THREAD,
1374
+ NotFoundError,
1375
+ OCTAVUS_SKILL_TOOLS,
984
1376
  OctavusChat,
1377
+ OctavusError2 as OctavusError,
1378
+ ValidationError,
1379
+ createApiErrorEvent,
1380
+ createErrorEvent,
985
1381
  createHttpTransport,
1382
+ createInternalErrorEvent,
986
1383
  createSocketTransport,
1384
+ errorToStreamEvent,
1385
+ generateId2 as generateId,
1386
+ getSkillSlugFromToolCall,
1387
+ isAbortError3 as isAbortError,
1388
+ isAuthenticationError,
1389
+ isFileReference,
1390
+ isFileReferenceArray2 as isFileReferenceArray,
1391
+ isMainThread,
1392
+ isOctavusSkillTool,
1393
+ isOtherThread,
1394
+ isProviderError,
1395
+ isRateLimitError,
1396
+ isRetryableError,
987
1397
  isSocketTransport,
1398
+ isToolError,
1399
+ isValidationError,
988
1400
  parseSSEStream,
1401
+ resolveThread,
1402
+ safeParseStreamEvent3 as safeParseStreamEvent,
1403
+ safeParseUIMessage,
1404
+ safeParseUIMessages,
1405
+ threadForPart2 as threadForPart,
989
1406
  uploadFiles
990
1407
  };
991
1408
  //# sourceMappingURL=index.js.map