bs-agent 0.0.12 → 0.0.16

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.
@@ -60,22 +60,22 @@ var useSessionUtils = (agentId, allSessions, setAllSessions, currentSessionId, s
60
60
  setAllSessions((prev) => {
61
61
  const updatedAgentSessions = { ...prev[agentId] };
62
62
  delete updatedAgentSessions[sessionId];
63
+ if (sessionId === currentSessionId) {
64
+ const remainingSessions = Object.values(updatedAgentSessions);
65
+ if (remainingSessions.length > 0) {
66
+ const mostRecent = remainingSessions.sort((a, b) => b.updatedAt - a.updatedAt)[0];
67
+ setCurrentSessionId(mostRecent.id);
68
+ } else {
69
+ setCurrentSessionId(TEMPORARY_SESSION_ID);
70
+ }
71
+ }
63
72
  return {
64
73
  ...prev,
65
74
  [agentId]: updatedAgentSessions
66
75
  };
67
76
  });
68
- if (sessionId === currentSessionId) {
69
- const remainingSessions = Object.values(agentSessions).filter((s) => s.id !== sessionId);
70
- if (remainingSessions.length > 0) {
71
- const mostRecent = remainingSessions.sort((a, b) => b.updatedAt - a.updatedAt)[0];
72
- setCurrentSessionId(mostRecent.id);
73
- } else {
74
- setCurrentSessionId(TEMPORARY_SESSION_ID);
75
- }
76
- }
77
77
  },
78
- [agentId, currentSessionId, agentSessions, setAllSessions, setCurrentSessionId]
78
+ [agentId, currentSessionId, setAllSessions, setCurrentSessionId]
79
79
  );
80
80
  const sessionsList = useMemo(
81
81
  () => Object.values(agentSessions).sort((a, b) => b.updatedAt - a.updatedAt),
@@ -281,15 +281,13 @@ function useClientToolHelpers(agentId, toolContext) {
281
281
  const tools = toolContext.getToolsForAgent(agentId);
282
282
  const map = /* @__PURE__ */ new Map();
283
283
  for (const tool of tools) {
284
- if (tool.handler) {
285
- map.set(tool.name, {
286
- name: tool.name,
287
- description: tool.description,
288
- parameters: tool.parameters,
289
- await: tool.await,
290
- handler: tool.handler
291
- });
292
- }
284
+ map.set(tool.name, {
285
+ name: tool.name,
286
+ description: tool.description,
287
+ parameters: tool.parameters,
288
+ await: tool.await,
289
+ handler: tool.handler
290
+ });
293
291
  }
294
292
  return map;
295
293
  }, [agentId, toolContext]);
@@ -306,233 +304,6 @@ function useClientToolHelpers(agentId, toolContext) {
306
304
  return { getClientToolsMap, getClientToolDefs };
307
305
  }
308
306
 
309
- // src/core/stream.ts
310
- function tryParseJSON2(value) {
311
- if (typeof value === "string") {
312
- try {
313
- return JSON.parse(value);
314
- } catch {
315
- return value;
316
- }
317
- }
318
- return value;
319
- }
320
- var FatalStreamError = class extends Error {
321
- constructor(message, statusCode) {
322
- super(message);
323
- this.statusCode = statusCode;
324
- this.name = "FatalStreamError";
325
- }
326
- };
327
- async function executeStream(options) {
328
- const {
329
- url,
330
- body,
331
- headers,
332
- callbacks,
333
- clientTools,
334
- signal,
335
- onSessionId,
336
- onPaused,
337
- onAutoResume,
338
- onResponse
339
- } = options;
340
- let fullText = "";
341
- const pendingAutoResumes = [];
342
- const response = await fetch(url, {
343
- method: "POST",
344
- headers: {
345
- "Content-Type": "application/json",
346
- Accept: "text/event-stream",
347
- ...headers
348
- },
349
- body: JSON.stringify(body),
350
- signal
351
- });
352
- const sessionId = response.headers.get("X-BuildShip-Agent-Session-ID");
353
- if (sessionId && onSessionId) {
354
- const sessionName = response.headers.get("X-BuildShip-Agent-Session-Name") || void 0;
355
- onSessionId(sessionId, sessionName);
356
- }
357
- onResponse?.(response);
358
- if (!response.ok) {
359
- let errorBody = "";
360
- try {
361
- errorBody = await response.text();
362
- } catch {
363
- }
364
- const error = new FatalStreamError(
365
- `HTTP ${response.status}: ${errorBody || response.statusText}`,
366
- response.status
367
- );
368
- callbacks.onError?.(error);
369
- throw error;
370
- }
371
- if (!response.body) {
372
- callbacks.onComplete?.(fullText);
373
- return;
374
- }
375
- const reader = response.body.getReader();
376
- const decoder = new TextDecoder();
377
- let buffer = "";
378
- try {
379
- while (true) {
380
- const { done, value } = await reader.read();
381
- if (done) break;
382
- buffer += decoder.decode(value, { stream: true });
383
- const parts = buffer.split("\n\n");
384
- buffer = parts.pop() || "";
385
- for (const part of parts) {
386
- const trimmedPart = part.trim();
387
- if (!trimmedPart) continue;
388
- let jsonStr = "";
389
- for (const line of trimmedPart.split("\n")) {
390
- if (line.startsWith("data: ")) {
391
- jsonStr += line.slice(6);
392
- } else if (line.startsWith("data:")) {
393
- jsonStr += line.slice(5);
394
- }
395
- }
396
- jsonStr = jsonStr.trim();
397
- if (!jsonStr) continue;
398
- let raw;
399
- try {
400
- raw = JSON.parse(jsonStr);
401
- } catch {
402
- continue;
403
- }
404
- const streamEvent = normalizeEvent(raw);
405
- handleEvent(
406
- streamEvent,
407
- fullText,
408
- callbacks,
409
- clientTools,
410
- onPaused,
411
- onAutoResume,
412
- pendingAutoResumes
413
- );
414
- if (streamEvent.type === "text_delta") {
415
- fullText += streamEvent.data;
416
- }
417
- }
418
- }
419
- if (buffer.trim()) {
420
- let jsonStr = "";
421
- for (const line of buffer.trim().split("\n")) {
422
- if (line.startsWith("data: ")) {
423
- jsonStr += line.slice(6);
424
- } else if (line.startsWith("data:")) {
425
- jsonStr += line.slice(5);
426
- }
427
- }
428
- jsonStr = jsonStr.trim();
429
- if (jsonStr) {
430
- try {
431
- const raw = JSON.parse(jsonStr);
432
- const streamEvent = normalizeEvent(raw);
433
- handleEvent(
434
- streamEvent,
435
- fullText,
436
- callbacks,
437
- clientTools,
438
- onPaused,
439
- onAutoResume,
440
- pendingAutoResumes
441
- );
442
- if (streamEvent.type === "text_delta") {
443
- fullText += streamEvent.data;
444
- }
445
- } catch {
446
- }
447
- }
448
- }
449
- } catch (err) {
450
- if (err instanceof Error && err.name === "AbortError") {
451
- throw err;
452
- }
453
- const error = err instanceof Error ? err : new Error(String(err));
454
- callbacks.onError?.(error);
455
- throw error;
456
- }
457
- if (pendingAutoResumes.length > 0) {
458
- await Promise.allSettled(pendingAutoResumes);
459
- }
460
- callbacks.onComplete?.(fullText);
461
- }
462
- function normalizeEvent(raw) {
463
- const typeMap = {
464
- llm_text_delta: "text_delta",
465
- llm_reasoning_delta: "reasoning_delta"
466
- };
467
- if (raw && typeof raw.type === "string" && typeMap[raw.type]) {
468
- raw.type = typeMap[raw.type];
469
- }
470
- return raw;
471
- }
472
- function handleEvent(event, _fullText, callbacks, clientTools, onPaused, onAutoResume, pendingAutoResumes) {
473
- callbacks.onEvent?.(event);
474
- switch (event.type) {
475
- case "text_delta": {
476
- callbacks.onText?.(event.data);
477
- break;
478
- }
479
- case "reasoning_delta": {
480
- callbacks.onReasoning?.(event.data.delta, event.data.index);
481
- break;
482
- }
483
- case "agent_handoff": {
484
- callbacks.onAgentHandoff?.(event.data.agentName);
485
- break;
486
- }
487
- case "tool_call_start": {
488
- const { callId, toolName, toolType, inputs: rawInputs, paused } = event.data;
489
- const inputs = tryParseJSON2(rawInputs);
490
- callbacks.onToolStart?.(toolName, toolType);
491
- if (toolType === "client") {
492
- const tool = clientTools.get(toolName);
493
- if (tool) {
494
- if (paused && tool.handler) {
495
- const handlerPromise = (async () => {
496
- try {
497
- const result = await tool.handler(inputs);
498
- await onAutoResume?.(callId, result);
499
- } catch {
500
- callbacks.onPaused?.(toolName, inputs);
501
- onPaused?.({ callId, toolName, args: inputs });
502
- }
503
- })();
504
- pendingAutoResumes.push(handlerPromise);
505
- } else if (paused) {
506
- callbacks.onPaused?.(toolName, inputs);
507
- onPaused?.({ callId, toolName, args: inputs });
508
- } else if (tool.handler) {
509
- try {
510
- tool.handler(inputs);
511
- } catch {
512
- }
513
- }
514
- } else if (paused) {
515
- callbacks.onPaused?.(toolName, inputs);
516
- onPaused?.({ callId, toolName, args: inputs });
517
- }
518
- }
519
- break;
520
- }
521
- case "tool_call_end": {
522
- const { toolName, result, error } = event.data;
523
- callbacks.onToolEnd?.(toolName, result, error);
524
- break;
525
- }
526
- case "run_error": {
527
- const { message, code } = event.data;
528
- const error = new Error(message);
529
- if (code != null) error.code = code;
530
- callbacks.onError?.(error);
531
- break;
532
- }
533
- }
534
- }
535
-
536
307
  // src/react/utils/message.ts
537
308
  function updateAgentMessageParts(parts, newPart) {
538
309
  const last = parts[parts.length - 1];
@@ -552,7 +323,15 @@ function updateAgentMessageParts(parts, newPart) {
552
323
 
553
324
  // src/react/stream-callbacks.ts
554
325
  function buildStreamCallbacks(deps, debugKey) {
555
- const { setMessages, setInProgress, syncSessionRef, messagesRef, toolContext, agentId } = deps;
326
+ const {
327
+ setMessages,
328
+ setInProgress,
329
+ syncSessionRef,
330
+ messagesRef,
331
+ toolContext,
332
+ agentId,
333
+ textDeltaModifier
334
+ } = deps;
556
335
  return {
557
336
  onComplete: () => {
558
337
  console.log("Agent closed");
@@ -570,7 +349,7 @@ function buildStreamCallbacks(deps, debugKey) {
570
349
  },
571
350
  onEvent: (event) => {
572
351
  if (event.type === "text_delta") {
573
- handleTextDelta(event, setMessages, syncSessionRef);
352
+ handleTextDelta(event, setMessages, syncSessionRef, textDeltaModifier, agentId);
574
353
  } else if (event.type === "tool_call_start" && event.data.toolType === "client") {
575
354
  handleClientToolCall(event, setMessages, syncSessionRef, toolContext, agentId);
576
355
  if (debugKey) {
@@ -600,82 +379,21 @@ function buildStreamCallbacks(deps, debugKey) {
600
379
  }
601
380
  };
602
381
  }
603
- var TEMPORARY_SESSION_ID2 = "sess_temp";
604
- var DEFAULT_SESSION_NAME2 = "New Chat";
605
- async function executeAgentStream(deps, body, headers, callbacks, debugKey) {
606
- const {
607
- agentUrl,
608
- signal,
609
- currentSessionId,
610
- lastRequestContextRef,
611
- messagesRef,
612
- createSessionFromResponse,
613
- setMessages,
614
- syncSessionRef,
615
- runAgentRef
616
- } = deps;
617
- await executeStream({
618
- url: agentUrl,
619
- body,
620
- headers,
621
- callbacks,
622
- clientTools: deps.getClientToolsMap(),
623
- signal,
624
- onSessionId: (sessionId, sessionName) => {
625
- lastRequestContextRef.current.sessionId = sessionId;
626
- if (!currentSessionId || currentSessionId === TEMPORARY_SESSION_ID2) {
627
- createSessionFromResponse(
628
- sessionId,
629
- sessionName || DEFAULT_SESSION_NAME2,
630
- messagesRef.current
631
- );
632
- deps.setCurrentSessionId(sessionId);
633
- }
634
- },
635
- // Session creation is handled by onSessionId above.
636
- onAutoResume: async (callId, result) => {
637
- setMessages((prev) => {
638
- const updatedMessages = prev.map((msg) => {
639
- if (msg.parts) {
640
- const updatedParts = msg.parts.map((part) => {
641
- if (part.type === "widget" && part.callId === callId) {
642
- return { ...part, status: "submitted", result };
643
- }
644
- return part;
645
- });
646
- return { ...msg, parts: updatedParts };
647
- }
648
- return msg;
649
- });
650
- if (syncSessionRef.current) {
651
- syncSessionRef.current(updatedMessages);
652
- }
653
- return updatedMessages;
654
- });
655
- if (debugKey) {
656
- deps.debugHandlers.handleStreamEvent({
657
- type: "tool_call_end",
658
- data: { callId, toolName: "", toolType: "client", result },
659
- meta: { executionId: debugKey, sequence: 0 }
660
- });
661
- }
662
- try {
663
- await runAgentRef.current?.(void 0, {
664
- resumeToolCallId: callId,
665
- resumeToolResult: result
666
- });
667
- } catch (error) {
668
- console.error("Auto-resume failed", error);
669
- }
670
- }
671
- });
672
- }
673
- function handleTextDelta(event, setMessages, syncSessionRef) {
382
+ function handleTextDelta(event, setMessages, syncSessionRef, modifier, agentId) {
674
383
  const sequence = event.meta.sequence;
675
- const text = event.data;
384
+ const originalText = event.data;
676
385
  const eventExecutionId = event.meta.executionId;
677
386
  setMessages((prev) => {
678
387
  const lastMessage = prev[prev.length - 1];
388
+ let text = originalText;
389
+ if (modifier) {
390
+ const currentFullText = lastMessage?.role === "agent" ? lastMessage.content : "";
391
+ text = modifier(originalText, currentFullText, {
392
+ executionId: eventExecutionId,
393
+ sequence,
394
+ agentId: event.meta.agentId || agentId
395
+ });
396
+ }
679
397
  const newPart = {
680
398
  type: "text",
681
399
  text,
@@ -743,7 +461,8 @@ function handleClientToolCall(event, setMessages, syncSessionRef, toolContext, a
743
461
  }
744
462
 
745
463
  // src/react/use-agent.ts
746
- function useAgent(agentId, agentUrl, accessKey) {
464
+ function useAgent(agent, options) {
465
+ const agentId = agent._agentId;
747
466
  const { allSessions, setAllSessions, debugData, setDebugData } = useAgentGlobalState();
748
467
  const toolContext = useContext2(AgentToolContext);
749
468
  const [inProgress, setInProgress] = useState(false);
@@ -761,7 +480,7 @@ function useAgent(agentId, agentUrl, accessKey) {
761
480
  useEffect(() => {
762
481
  const initialSessionId = sessionUtils.getInitialSessionId();
763
482
  setCurrentSessionId(initialSessionId);
764
- }, []);
483
+ }, [agentId]);
765
484
  const debugHandlers = useMemo2(() => createDebugHandlers(setDebugData), [setDebugData]);
766
485
  useEffect(() => {
767
486
  messagesRef.current = messages;
@@ -774,82 +493,90 @@ function useAgent(agentId, agentUrl, accessKey) {
774
493
  } else {
775
494
  setMessages([]);
776
495
  }
777
- }, [currentSessionId, agentId]);
496
+ }, [currentSessionId, agentId, sessionUtils.agentSessions, inProgress]);
778
497
  useEffect(() => {
498
+ const syncRef = sessionUtils.syncSessionRef;
499
+ const msgRef = messagesRef;
779
500
  return () => {
780
- if (messagesRef.current.length > 0 && sessionUtils.syncSessionRef.current) {
781
- sessionUtils.syncSessionRef.current();
501
+ if (msgRef.current.length > 0 && syncRef.current) {
502
+ syncRef.current();
782
503
  }
783
504
  };
784
- }, []);
785
- const controller = useRef2();
786
- const lastRequestContextRef = useRef2({});
787
- const { getClientToolsMap, getClientToolDefs } = useClientToolHelpers(agentId, toolContext);
788
- const runAgentRef = useRef2();
505
+ }, [sessionUtils.syncSessionRef]);
506
+ const optionsRef = useRef2(options);
507
+ useEffect(() => {
508
+ optionsRef.current = options;
509
+ }, [options]);
510
+ const lastRunOptionsRef = useRef2({});
511
+ const { getClientToolsMap } = useClientToolHelpers(agentId, toolContext);
512
+ const activeSessionRef = useRef2(null);
789
513
  const runAgent = useCallback3(
790
514
  async (input, runOptions) => {
791
- const realSessionId = currentSessionId && currentSessionId !== TEMPORARY_SESSION_ID ? currentSessionId : lastRequestContextRef.current.sessionId;
792
- lastRequestContextRef.current = {
793
- additionalHeaders: runOptions?.additionalHeaders ?? lastRequestContextRef.current.additionalHeaders,
794
- additionalBody: runOptions?.additionalBody ?? lastRequestContextRef.current.additionalBody,
795
- sessionId: realSessionId
796
- };
515
+ const isNewSession = !currentSessionId || currentSessionId === TEMPORARY_SESSION_ID;
797
516
  setInProgress(true);
798
- if (!runOptions?.resumeToolCallId && controller.current) {
799
- controller.current.abort();
800
- }
801
- controller.current = new AbortController();
802
- const headers = {
803
- ...lastRequestContextRef.current.additionalHeaders || {},
804
- ...accessKey ? { Authorization: `Bearer ${accessKey}` } : {}
805
- };
806
- if (realSessionId) {
807
- headers["x-buildship-agent-session-id"] = realSessionId;
808
- }
809
- const body = {
810
- stream: true,
811
- ...lastRequestContextRef.current.additionalBody || {}
812
- };
813
- if (input !== void 0) {
814
- body.input = input;
815
- }
816
- if (runOptions?.context) {
817
- Object.assign(body, { context: runOptions.context });
818
- }
819
- if (runOptions?.resumeToolCallId) {
820
- body.toolCallResult = {
821
- callId: runOptions.resumeToolCallId,
822
- result: runOptions.resumeToolResult
823
- };
824
- }
825
- const registeredToolDefs = getClientToolDefs() || [];
826
- const legacyToolDefs = runOptions?.clientTools || [];
827
- const allToolDefs = [...registeredToolDefs, ...legacyToolDefs];
828
- if (allToolDefs.length > 0) {
829
- body.clientTools = allToolDefs;
830
- }
831
517
  const debugKey = runOptions?.optimisticExecutionId || messagesRef.current.findLast((m) => m.role === "user")?.executionId;
832
518
  const deps = {
833
- agentUrl,
834
519
  agentId,
835
- accessKey,
836
520
  currentSessionId,
837
521
  messagesRef,
838
522
  setMessages,
839
523
  setInProgress,
840
- setCurrentSessionId,
841
524
  syncSessionRef: sessionUtils.syncSessionRef,
842
- createSessionFromResponse: sessionUtils.createSessionFromResponse,
843
525
  debugHandlers,
844
526
  toolContext,
845
- lastRequestContextRef,
846
- getClientToolsMap,
847
- signal: controller.current.signal,
848
- runAgentRef
527
+ textDeltaModifier: optionsRef.current?.textDeltaModifier
849
528
  };
850
529
  const callbacks = buildStreamCallbacks(deps, debugKey);
530
+ const executeOptions = {
531
+ context: runOptions?.context,
532
+ headers: runOptions?.additionalHeaders,
533
+ body: runOptions?.additionalBody
534
+ };
535
+ const toolsMap = getClientToolsMap();
536
+ for (const tool of toolsMap.values()) {
537
+ agent.registerClientTool(tool);
538
+ }
539
+ if (runOptions?.clientTools && runOptions.clientTools.length > 0) {
540
+ executeOptions.body = {
541
+ ...executeOptions.body || {},
542
+ clientTools: [
543
+ ...executeOptions.body?.clientTools || [],
544
+ ...runOptions.clientTools
545
+ ]
546
+ };
547
+ }
851
548
  try {
852
- await executeAgentStream(deps, body, headers, callbacks, debugKey);
549
+ if (runOptions?.resumeToolCallId) {
550
+ if (isNewSession) throw new Error("Cannot resume a tool call on a temporary session.");
551
+ const session = agent.session(currentSessionId);
552
+ activeSessionRef.current = session;
553
+ await session.resumeWithCallId(
554
+ runOptions.resumeToolCallId,
555
+ runOptions.resumeToolResult,
556
+ callbacks,
557
+ executeOptions
558
+ );
559
+ } else {
560
+ if (isNewSession) {
561
+ const extendedOptions = {
562
+ ...executeOptions,
563
+ onSessionId: (newSessionId, sessionName) => {
564
+ sessionUtils.createSessionFromResponse(
565
+ newSessionId,
566
+ sessionName || DEFAULT_SESSION_NAME,
567
+ messagesRef.current
568
+ );
569
+ setCurrentSessionId(newSessionId);
570
+ }
571
+ };
572
+ const session = await agent.execute(input || "", callbacks, extendedOptions);
573
+ activeSessionRef.current = session;
574
+ } else {
575
+ const session = agent.session(currentSessionId);
576
+ activeSessionRef.current = session;
577
+ await session.execute(input || "", callbacks, executeOptions);
578
+ }
579
+ }
853
580
  } catch (error) {
854
581
  console.log("Agent execution failed", error);
855
582
  setInProgress(false);
@@ -857,32 +584,20 @@ function useAgent(agentId, agentUrl, accessKey) {
857
584
  sessionUtils.syncSessionRef.current(messagesRef.current);
858
585
  }
859
586
  throw error;
587
+ } finally {
588
+ activeSessionRef.current = null;
860
589
  }
861
590
  },
862
- [
863
- agentUrl,
864
- accessKey,
865
- currentSessionId,
866
- sessionUtils.syncSessionRef,
867
- sessionUtils.createSessionFromResponse,
868
- debugHandlers,
869
- getClientToolsMap,
870
- getClientToolDefs,
871
- agentId,
872
- toolContext
873
- ]
591
+ [currentSessionId, sessionUtils, debugHandlers, agentId, toolContext, agent, getClientToolsMap]
874
592
  );
875
- useEffect(() => {
876
- runAgentRef.current = runAgent;
877
- }, [runAgent]);
878
593
  const handleSend = useCallback3(
879
- async (input, options) => {
594
+ async (input, options2) => {
880
595
  const userMessage = {
881
596
  role: "user",
882
597
  content: input,
883
598
  executionId: Date.now().toString()
884
599
  };
885
- if (!options?.skipUserMessage) {
600
+ if (!options2?.skipUserMessage) {
886
601
  setMessages((prev) => {
887
602
  const updatedMessages = [...prev, userMessage];
888
603
  if (sessionUtils.syncSessionRef.current) {
@@ -891,14 +606,19 @@ function useAgent(agentId, agentUrl, accessKey) {
891
606
  return updatedMessages;
892
607
  });
893
608
  }
894
- const effectiveExecutionId = options?.skipUserMessage ? messagesRef.current.findLast((m) => m.role === "user")?.executionId ?? userMessage.executionId : userMessage.executionId;
609
+ const effectiveExecutionId = options2?.skipUserMessage ? messagesRef.current.findLast((m) => m.role === "user")?.executionId ?? userMessage.executionId : userMessage.executionId;
610
+ lastRunOptionsRef.current = {
611
+ context: options2?.context,
612
+ additionalHeaders: options2?.additionalHeaders,
613
+ additionalBody: options2?.additionalBody
614
+ };
895
615
  try {
896
616
  await runAgent(input, {
897
- ...options,
617
+ ...options2,
898
618
  optimisticExecutionId: effectiveExecutionId
899
619
  });
900
620
  } catch (error) {
901
- if (!options?.skipUserMessage) {
621
+ if (!options2?.skipUserMessage) {
902
622
  setMessages((prev) => {
903
623
  const updatedMessages = prev.some((m) => m === userMessage) ? prev : [...prev, userMessage];
904
624
  if (sessionUtils.syncSessionRef.current) {
@@ -942,7 +662,8 @@ function useAgent(agentId, agentUrl, accessKey) {
942
662
  }
943
663
  await runAgent(void 0, {
944
664
  resumeToolCallId: callId,
945
- resumeToolResult: result
665
+ resumeToolResult: result,
666
+ ...lastRunOptionsRef.current
946
667
  });
947
668
  },
948
669
  [runAgent, sessionUtils.syncSessionRef, debugHandlers]
@@ -965,8 +686,8 @@ function useAgent(agentId, agentUrl, accessKey) {
965
686
  [sessionUtils.syncSessionRef]
966
687
  );
967
688
  const abort = useCallback3(() => {
968
- if (controller.current) {
969
- controller.current.abort();
689
+ if (activeSessionRef.current) {
690
+ activeSessionRef.current.abort();
970
691
  }
971
692
  setInProgress(false);
972
693
  if (sessionUtils.syncSessionRef.current) {
@@ -988,6 +709,536 @@ function useAgent(agentId, agentUrl, accessKey) {
988
709
  };
989
710
  }
990
711
 
712
+ // src/core/stream.ts
713
+ function tryParseJSON2(value) {
714
+ if (typeof value === "string") {
715
+ try {
716
+ return JSON.parse(value);
717
+ } catch {
718
+ return value;
719
+ }
720
+ }
721
+ return value;
722
+ }
723
+ var FatalStreamError = class extends Error {
724
+ constructor(message, statusCode) {
725
+ super(message);
726
+ this.statusCode = statusCode;
727
+ this.name = "FatalStreamError";
728
+ }
729
+ };
730
+ async function executeStream(options) {
731
+ const {
732
+ url,
733
+ body,
734
+ headers,
735
+ callbacks,
736
+ clientTools,
737
+ signal,
738
+ onSessionId,
739
+ onPaused,
740
+ onAutoResume,
741
+ onResponse
742
+ } = options;
743
+ let fullText = "";
744
+ const pendingAutoResumes = [];
745
+ const response = await fetch(url, {
746
+ method: "POST",
747
+ headers: {
748
+ "Content-Type": "application/json",
749
+ Accept: "text/event-stream",
750
+ ...headers
751
+ },
752
+ body: JSON.stringify(body),
753
+ signal
754
+ });
755
+ const sessionId = response.headers.get("X-BuildShip-Agent-Session-ID");
756
+ if (sessionId && onSessionId) {
757
+ const sessionName = response.headers.get("X-BuildShip-Agent-Session-Name") || void 0;
758
+ onSessionId(sessionId, sessionName);
759
+ }
760
+ onResponse?.(response);
761
+ if (!response.ok) {
762
+ let errorBody = "";
763
+ try {
764
+ errorBody = await response.text();
765
+ } catch {
766
+ }
767
+ const error = new FatalStreamError(
768
+ `HTTP ${response.status}: ${errorBody || response.statusText}`,
769
+ response.status
770
+ );
771
+ callbacks.onError?.(error);
772
+ throw error;
773
+ }
774
+ if (!response.body) {
775
+ callbacks.onComplete?.(fullText);
776
+ return;
777
+ }
778
+ const reader = response.body.getReader();
779
+ const decoder = new TextDecoder();
780
+ let buffer = "";
781
+ try {
782
+ while (true) {
783
+ const { done, value } = await reader.read();
784
+ if (done) break;
785
+ buffer += decoder.decode(value, { stream: true });
786
+ const parts = buffer.split("\n\n");
787
+ buffer = parts.pop() || "";
788
+ for (const part of parts) {
789
+ const trimmedPart = part.trim();
790
+ if (!trimmedPart) continue;
791
+ let jsonStr = "";
792
+ for (const line of trimmedPart.split("\n")) {
793
+ if (line.startsWith("data: ")) {
794
+ jsonStr += line.slice(6);
795
+ } else if (line.startsWith("data:")) {
796
+ jsonStr += line.slice(5);
797
+ }
798
+ }
799
+ jsonStr = jsonStr.trim();
800
+ if (!jsonStr) continue;
801
+ let raw;
802
+ try {
803
+ raw = JSON.parse(jsonStr);
804
+ } catch {
805
+ continue;
806
+ }
807
+ const streamEvent = normalizeEvent(raw);
808
+ handleEvent(
809
+ streamEvent,
810
+ fullText,
811
+ callbacks,
812
+ clientTools,
813
+ onPaused,
814
+ onAutoResume,
815
+ pendingAutoResumes
816
+ );
817
+ if (streamEvent.type === "text_delta") {
818
+ fullText += streamEvent.data;
819
+ }
820
+ }
821
+ }
822
+ if (buffer.trim()) {
823
+ let jsonStr = "";
824
+ for (const line of buffer.trim().split("\n")) {
825
+ if (line.startsWith("data: ")) {
826
+ jsonStr += line.slice(6);
827
+ } else if (line.startsWith("data:")) {
828
+ jsonStr += line.slice(5);
829
+ }
830
+ }
831
+ jsonStr = jsonStr.trim();
832
+ if (jsonStr) {
833
+ try {
834
+ const raw = JSON.parse(jsonStr);
835
+ const streamEvent = normalizeEvent(raw);
836
+ handleEvent(
837
+ streamEvent,
838
+ fullText,
839
+ callbacks,
840
+ clientTools,
841
+ onPaused,
842
+ onAutoResume,
843
+ pendingAutoResumes
844
+ );
845
+ if (streamEvent.type === "text_delta") {
846
+ fullText += streamEvent.data;
847
+ }
848
+ } catch {
849
+ }
850
+ }
851
+ }
852
+ } catch (err) {
853
+ if (err instanceof Error && err.name === "AbortError") {
854
+ throw err;
855
+ }
856
+ const error = err instanceof Error ? err : new Error(String(err));
857
+ callbacks.onError?.(error);
858
+ throw error;
859
+ }
860
+ if (pendingAutoResumes.length > 0) {
861
+ const handlerResults = await Promise.allSettled(pendingAutoResumes);
862
+ for (const settled of handlerResults) {
863
+ if (settled.status === "fulfilled" && settled.value) {
864
+ const { callId, result } = settled.value;
865
+ await onAutoResume?.(callId, result);
866
+ }
867
+ }
868
+ }
869
+ callbacks.onComplete?.(fullText);
870
+ }
871
+ function normalizeEvent(raw) {
872
+ const typeMap = {
873
+ llm_text_delta: "text_delta",
874
+ llm_reasoning_delta: "reasoning_delta"
875
+ };
876
+ if (raw && typeof raw.type === "string" && typeMap[raw.type]) {
877
+ raw.type = typeMap[raw.type];
878
+ }
879
+ return raw;
880
+ }
881
+ function handleEvent(event, _fullText, callbacks, clientTools, onPaused, onAutoResume, pendingAutoResumes) {
882
+ callbacks.onEvent?.(event);
883
+ switch (event.type) {
884
+ case "text_delta": {
885
+ callbacks.onText?.(event.data);
886
+ break;
887
+ }
888
+ case "reasoning_delta": {
889
+ callbacks.onReasoning?.(event.data.delta, event.data.index);
890
+ break;
891
+ }
892
+ case "agent_handoff": {
893
+ callbacks.onAgentHandoff?.(event.data.agentName);
894
+ break;
895
+ }
896
+ case "tool_call_start": {
897
+ const { callId, toolName, toolType, inputs: rawInputs, paused } = event.data;
898
+ const inputs = tryParseJSON2(rawInputs);
899
+ callbacks.onToolStart?.(toolName, toolType);
900
+ if (toolType === "client") {
901
+ const tool = clientTools.get(toolName);
902
+ if (tool) {
903
+ if (paused && tool.handler) {
904
+ const handlerPromise = (async () => {
905
+ try {
906
+ const result = await tool.handler(inputs);
907
+ callbacks.onEvent?.({
908
+ type: "tool_call_end",
909
+ data: {
910
+ callId,
911
+ toolName,
912
+ toolType: "client",
913
+ result
914
+ },
915
+ meta: event.meta
916
+ });
917
+ return { callId, result };
918
+ } catch (error) {
919
+ callbacks.onPaused?.(toolName, inputs);
920
+ onPaused?.({ callId, toolName, args: inputs });
921
+ return null;
922
+ }
923
+ })();
924
+ pendingAutoResumes.push(handlerPromise);
925
+ } else if (paused) {
926
+ callbacks.onPaused?.(toolName, inputs);
927
+ onPaused?.({ callId, toolName, args: inputs });
928
+ } else if (tool.handler) {
929
+ try {
930
+ tool.handler(inputs);
931
+ } catch {
932
+ }
933
+ }
934
+ } else if (paused) {
935
+ callbacks.onPaused?.(toolName, inputs);
936
+ onPaused?.({ callId, toolName, args: inputs });
937
+ }
938
+ }
939
+ break;
940
+ }
941
+ case "tool_call_end": {
942
+ const { toolName, result, error } = event.data;
943
+ callbacks.onToolEnd?.(toolName, result, error);
944
+ break;
945
+ }
946
+ case "run_error": {
947
+ const { message, code } = event.data;
948
+ const error = new Error(message);
949
+ if (code != null) error.code = code;
950
+ callbacks.onError?.(error);
951
+ break;
952
+ }
953
+ }
954
+ }
955
+
956
+ // src/core/session.ts
957
+ import { toJSONSchema as toJSONSchema2 } from "zod";
958
+ var AgentSession = class {
959
+ /** @internal */
960
+ _agent;
961
+ /** @internal */
962
+ _sessionId;
963
+ /** @internal */
964
+ _paused = false;
965
+ /** @internal */
966
+ _pausedToolInfo = null;
967
+ /** @internal */
968
+ _abortController = null;
969
+ /** @internal */
970
+ constructor(agent, sessionId) {
971
+ this._agent = agent;
972
+ this._sessionId = sessionId;
973
+ }
974
+ // ─── Public API ────────────────────────────────────────────────────
975
+ /**
976
+ * Send a message in this session.
977
+ *
978
+ * @param message - The message to send
979
+ * @param callbacks - Event handlers for the stream
980
+ * @param callbacks - Event handlers for the stream
981
+ * @param options - Optional settings like context, headers, or body
982
+ * @returns This session (for chaining)
983
+ */
984
+ async execute(message, callbacks, options) {
985
+ this._paused = false;
986
+ this._pausedToolInfo = null;
987
+ const body = {
988
+ ...options?.body || {},
989
+ input: message,
990
+ stream: true
991
+ };
992
+ if (options?.context) {
993
+ Object.assign(body, { context: options.context });
994
+ }
995
+ const clientToolDefs = this._getClientToolDefs();
996
+ if (clientToolDefs.length > 0) {
997
+ body.clientTools = clientToolDefs;
998
+ }
999
+ await this._run(body, callbacks, options);
1000
+ return this;
1001
+ }
1002
+ /**
1003
+ * Resume a paused session with a tool result.
1004
+ *
1005
+ * @param result - The result to send back to the agent
1006
+ * @param callbacks - Event handlers for the resumed stream
1007
+ * @param options - Optional settings like headers or body
1008
+ * @returns This session (for chaining)
1009
+ */
1010
+ async resume(result, callbacks, options) {
1011
+ if (!this._paused || !this._pausedToolInfo) {
1012
+ throw new Error("AgentSession.resume(): session is not paused. Check isPaused() first.");
1013
+ }
1014
+ const body = {
1015
+ ...options?.body || {},
1016
+ stream: true,
1017
+ toolCallResult: {
1018
+ callId: this._pausedToolInfo.callId,
1019
+ result
1020
+ }
1021
+ };
1022
+ const clientToolDefs = this._getClientToolDefs();
1023
+ if (clientToolDefs.length > 0) {
1024
+ body.clientTools = clientToolDefs;
1025
+ }
1026
+ this._paused = false;
1027
+ this._pausedToolInfo = null;
1028
+ await this._run(body, callbacks, options);
1029
+ return this;
1030
+ }
1031
+ /**
1032
+ * Resume a session with an explicit tool call ID and result.
1033
+ *
1034
+ * Unlike `resume()`, this does NOT require the session to be in a paused state,
1035
+ * making it suitable for external resume flows (e.g. React widget submissions)
1036
+ * where the session object may have been re-created.
1037
+ *
1038
+ * @param callId - The tool call ID to resume
1039
+ * @param result - The result to send back to the agent
1040
+ * @param callbacks - Event handlers for the resumed stream
1041
+ * @param options - Optional settings like headers or body
1042
+ * @returns This session (for chaining)
1043
+ */
1044
+ async resumeWithCallId(callId, result, callbacks, options) {
1045
+ const body = {
1046
+ ...options?.body || {},
1047
+ stream: true,
1048
+ toolCallResult: { callId, result }
1049
+ };
1050
+ const clientToolDefs = this._getClientToolDefs();
1051
+ if (clientToolDefs.length > 0) {
1052
+ body.clientTools = clientToolDefs;
1053
+ }
1054
+ this._paused = false;
1055
+ this._pausedToolInfo = null;
1056
+ await this._run(body, callbacks, options);
1057
+ return this;
1058
+ }
1059
+ /**
1060
+ * Check if this session is waiting for a tool result.
1061
+ */
1062
+ isPaused() {
1063
+ return this._paused;
1064
+ }
1065
+ /**
1066
+ * Get information about the paused tool call.
1067
+ * Returns `null` if the session is not paused.
1068
+ */
1069
+ getPausedTool() {
1070
+ return this._pausedToolInfo;
1071
+ }
1072
+ /**
1073
+ * Get the session ID.
1074
+ * May be `undefined` if the session hasn't executed yet.
1075
+ */
1076
+ getSessionId() {
1077
+ if (!this._sessionId) {
1078
+ throw new Error(
1079
+ "AgentSession.getSessionId(): session ID not yet available. Call execute() first."
1080
+ );
1081
+ }
1082
+ return this._sessionId;
1083
+ }
1084
+ /**
1085
+ * Cancel the current streaming operation.
1086
+ */
1087
+ abort() {
1088
+ this._abortController?.abort();
1089
+ this._abortController = null;
1090
+ }
1091
+ // ─── Private helpers ───────────────────────────────────────────────
1092
+ /** @internal */
1093
+ async _run(body, callbacks, options) {
1094
+ this._abortController = new AbortController();
1095
+ const baseHeaders = this._agent._buildHeaders(this._sessionId);
1096
+ const mergedHeaders = { ...baseHeaders, ...options?.headers || {} };
1097
+ await executeStream({
1098
+ url: this._agent._url,
1099
+ body,
1100
+ headers: mergedHeaders,
1101
+ callbacks,
1102
+ clientTools: this._agent._clientTools,
1103
+ signal: this._abortController.signal,
1104
+ onSessionId: (id, name) => {
1105
+ this._sessionId = id;
1106
+ options?.onSessionId?.(id, name);
1107
+ },
1108
+ onPaused: (info) => {
1109
+ this._paused = true;
1110
+ this._pausedToolInfo = info;
1111
+ },
1112
+ onAutoResume: async (callId, result) => {
1113
+ const resumeBody = {
1114
+ ...options?.body || {},
1115
+ stream: true,
1116
+ toolCallResult: { callId, result }
1117
+ };
1118
+ const clientToolDefs = this._getClientToolDefs();
1119
+ if (clientToolDefs.length > 0) {
1120
+ resumeBody.clientTools = clientToolDefs;
1121
+ }
1122
+ await this._run(resumeBody, callbacks, options);
1123
+ }
1124
+ });
1125
+ }
1126
+ /** @internal */
1127
+ _getClientToolDefs() {
1128
+ const defs = [];
1129
+ for (const tool of this._agent._clientTools.values()) {
1130
+ defs.push({
1131
+ name: tool.name,
1132
+ description: tool.description,
1133
+ parameters: resolveParameters2(tool.parameters),
1134
+ await: tool.await
1135
+ });
1136
+ }
1137
+ return defs;
1138
+ }
1139
+ };
1140
+ function resolveParameters2(params) {
1141
+ let schema;
1142
+ if (params && typeof params === "object" && "_def" in params) {
1143
+ schema = toJSONSchema2(params);
1144
+ delete schema.$schema;
1145
+ } else {
1146
+ schema = { ...params };
1147
+ }
1148
+ if (schema.type === "object") {
1149
+ schema.additionalProperties = false;
1150
+ if (schema.properties) {
1151
+ schema.required = Object.keys(schema.properties);
1152
+ }
1153
+ }
1154
+ return schema;
1155
+ }
1156
+
1157
+ // src/core/agent.ts
1158
+ var DEFAULT_BASE_URL = "https://api.buildship.run";
1159
+ var BuildShipAgent = class {
1160
+ /** @internal */
1161
+ _agentId;
1162
+ /** @internal */
1163
+ _accessKey;
1164
+ /** @internal */
1165
+ _baseUrl;
1166
+ /** @internal */
1167
+ _clientTools = /* @__PURE__ */ new Map();
1168
+ constructor(config) {
1169
+ if (!config.agentId) {
1170
+ throw new Error("BuildShipAgent: agentId is required");
1171
+ }
1172
+ this._agentId = config.agentId;
1173
+ this._accessKey = config.accessKey;
1174
+ this._baseUrl = (config.baseUrl || DEFAULT_BASE_URL).replace(/\/$/, "");
1175
+ }
1176
+ /**
1177
+ * The URL for the agent's execute endpoint.
1178
+ * @internal
1179
+ */
1180
+ get _url() {
1181
+ return `${this._baseUrl}/executeAgent/${this._agentId}`;
1182
+ }
1183
+ /**
1184
+ * Build the authorization / common headers.
1185
+ * @internal
1186
+ */
1187
+ _buildHeaders(sessionId) {
1188
+ const headers = {};
1189
+ if (this._accessKey) {
1190
+ headers["Authorization"] = `Bearer ${this._accessKey}`;
1191
+ }
1192
+ if (sessionId) {
1193
+ headers["X-BuildShip-Agent-Session-ID"] = sessionId;
1194
+ }
1195
+ return headers;
1196
+ }
1197
+ // ─── Public API ────────────────────────────────────────────────────
1198
+ /**
1199
+ * Start a new conversation.
1200
+ *
1201
+ * Creates a fresh session and sends the first message.
1202
+ *
1203
+ * @param message - The message to send
1204
+ * @param callbacks - Event handlers for the stream
1205
+ * @param options - Optional execution settings (context, headers, body, onSessionId)
1206
+ * @returns The new session
1207
+ */
1208
+ async execute(message, callbacks, options) {
1209
+ const session = new AgentSession(this);
1210
+ await session.execute(message, callbacks, options);
1211
+ return session;
1212
+ }
1213
+ /**
1214
+ * Get an existing session by ID to continue a conversation.
1215
+ *
1216
+ * @param sessionId - The session ID from a previous conversation
1217
+ * @returns The session object
1218
+ */
1219
+ session(sessionId) {
1220
+ if (!sessionId) {
1221
+ throw new Error("BuildShipAgent.session(): sessionId is required");
1222
+ }
1223
+ return new AgentSession(this, sessionId);
1224
+ }
1225
+ /**
1226
+ * Register a client-side tool that the agent can call.
1227
+ */
1228
+ registerClientTool(tool) {
1229
+ if (!tool.name) {
1230
+ throw new Error("registerClientTool: tool.name is required");
1231
+ }
1232
+ this._clientTools.set(tool.name, tool);
1233
+ }
1234
+ /**
1235
+ * Remove a registered client tool.
1236
+ */
1237
+ unregisterClientTool(name) {
1238
+ this._clientTools.delete(name);
1239
+ }
1240
+ };
1241
+
991
1242
  // src/react/utils/use-synced-local-storage.ts
992
1243
  import { useState as useState2, useEffect as useEffect2, useCallback as useCallback4 } from "react";
993
1244
  function parseJSON(value, defaultValue) {
@@ -1057,16 +1308,21 @@ function AgentContextProvider({ children }) {
1057
1308
  AGENT_DEBUG_DATA_KEY,
1058
1309
  {}
1059
1310
  );
1060
- const initializeAgent = useCallback5((agentId, agentUrl, accessKey) => {
1061
- const existing = activeAgentsRef.current.get(agentId);
1062
- if (!existing) {
1063
- activeAgentsRef.current.set(agentId, { agentUrl, accessKey });
1064
- forceUpdate({});
1065
- } else if (existing.agentUrl !== agentUrl || existing.accessKey !== accessKey) {
1066
- activeAgentsRef.current.set(agentId, { agentUrl, accessKey });
1067
- forceUpdate({});
1068
- }
1069
- }, []);
1311
+ const initializeAgent = useCallback5(
1312
+ (agentId, agentUrl, accessKey, options) => {
1313
+ const existing = activeAgentsRef.current.get(agentId);
1314
+ if (!existing) {
1315
+ activeAgentsRef.current.set(agentId, { agentUrl, accessKey, options });
1316
+ forceUpdate({});
1317
+ } else if (existing.agentUrl !== agentUrl || existing.accessKey !== accessKey) {
1318
+ activeAgentsRef.current.set(agentId, { agentUrl, accessKey, options });
1319
+ forceUpdate({});
1320
+ } else if (options) {
1321
+ existing.options = options;
1322
+ }
1323
+ },
1324
+ []
1325
+ );
1070
1326
  const registerRunner = useCallback5((agentId, runner) => {
1071
1327
  runnersRef.current.set(agentId, runner);
1072
1328
  const listeners = listenersRef.current.get(agentId);
@@ -1135,41 +1391,53 @@ function AgentContextProvider({ children }) {
1135
1391
  );
1136
1392
  return /* @__PURE__ */ jsx(AgentContext.Provider, { value: agentContextValue, children: /* @__PURE__ */ jsxs(AgentToolContext.Provider, { value: toolContextValue, children: [
1137
1393
  children,
1138
- Array.from(activeAgentsRef.current.entries()).map(([agentId, { agentUrl, accessKey }]) => /* @__PURE__ */ jsx(
1139
- AgentRunnerInstance,
1140
- {
1141
- agentId,
1142
- agentUrl,
1143
- accessKey
1144
- },
1145
- agentId
1146
- ))
1394
+ Array.from(activeAgentsRef.current.entries()).map(
1395
+ ([agentId, { agentUrl, accessKey, options }]) => /* @__PURE__ */ jsx(
1396
+ AgentRunnerInstance,
1397
+ {
1398
+ agentId,
1399
+ agentUrl,
1400
+ accessKey,
1401
+ options
1402
+ },
1403
+ agentId
1404
+ )
1405
+ )
1147
1406
  ] }) });
1148
1407
  }
1149
1408
  function AgentRunnerInstance({
1150
1409
  agentId,
1151
1410
  agentUrl,
1152
- accessKey
1411
+ accessKey,
1412
+ options
1153
1413
  }) {
1154
- const agent = useAgent(agentId, agentUrl, accessKey);
1414
+ const agent = useMemo3(
1415
+ () => new BuildShipAgent({ agentId, baseUrl: agentUrl, accessKey }),
1416
+ [agentId, agentUrl, accessKey]
1417
+ );
1418
+ const optionsRef = useRef3(options);
1419
+ optionsRef.current = options;
1420
+ const agentRunner = useAgent(agent, optionsRef.current);
1155
1421
  const context = useContext3(AgentContext);
1156
1422
  useEffect3(() => {
1157
1423
  if (context) {
1158
- context.registerRunner(agentId, agent);
1424
+ context.registerRunner(agentId, agentRunner);
1159
1425
  }
1160
- }, [agentId, agent, context]);
1426
+ }, [agentId, agentRunner, context]);
1161
1427
  if (!context) return null;
1162
1428
  return null;
1163
1429
  }
1164
- function useAgentContext(agentId, agentUrl, accessKey) {
1430
+ function useAgentContext(agentId, agentUrl, accessKey, options) {
1165
1431
  const context = useContext3(AgentContext);
1166
1432
  if (!context) {
1167
1433
  throw new Error("useAgentContext must be used within AgentContextProvider");
1168
1434
  }
1169
1435
  const { initializeAgent, getRunner, listenersRef } = context;
1436
+ const optionsRef = useRef3(options);
1437
+ optionsRef.current = options;
1170
1438
  useEffect3(() => {
1171
- initializeAgent(agentId, agentUrl, accessKey);
1172
- }, [agentId, agentUrl, initializeAgent]);
1439
+ initializeAgent(agentId, agentUrl, accessKey, optionsRef.current);
1440
+ }, [agentId, agentUrl, accessKey, initializeAgent]);
1173
1441
  const [runner, setRunner] = useState3(() => getRunner(agentId));
1174
1442
  useEffect3(() => {
1175
1443
  const currentRunner = getRunner(agentId);