@runtypelabs/persona 1.41.0 → 1.43.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/src/client.ts CHANGED
@@ -6,10 +6,12 @@ import {
6
6
  AgentWidgetContextProvider,
7
7
  AgentWidgetRequestMiddleware,
8
8
  AgentWidgetRequestPayload,
9
+ AgentWidgetAgentRequestPayload,
9
10
  AgentWidgetCustomFetch,
10
11
  AgentWidgetSSEEventParser,
11
12
  AgentWidgetHeadersFunction,
12
13
  AgentWidgetSSEEventResult as _AgentWidgetSSEEventResult,
14
+ AgentExecutionState,
13
15
  ClientSession,
14
16
  ClientInitResponse,
15
17
  ClientChatRequest,
@@ -79,6 +81,8 @@ function getParserFromType(parserType?: "plain" | "json" | "regex-json" | "xml")
79
81
  }
80
82
  }
81
83
 
84
+ export type SSEEventCallback = (eventType: string, payload: unknown) => void;
85
+
82
86
  export class AgentWidgetClient {
83
87
  private readonly apiUrl: string;
84
88
  private readonly headers: Record<string, string>;
@@ -89,6 +93,7 @@ export class AgentWidgetClient {
89
93
  private readonly customFetch?: AgentWidgetCustomFetch;
90
94
  private readonly parseSSEEvent?: AgentWidgetSSEEventParser;
91
95
  private readonly getHeaders?: AgentWidgetHeadersFunction;
96
+ private onSSEEvent?: SSEEventCallback;
92
97
 
93
98
  // Client token mode properties
94
99
  private clientSession: ClientSession | null = null;
@@ -110,6 +115,13 @@ export class AgentWidgetClient {
110
115
  this.getHeaders = config.getHeaders;
111
116
  }
112
117
 
118
+ /**
119
+ * Set callback for capturing raw SSE events
120
+ */
121
+ public setSSEEventCallback(callback: SSEEventCallback): void {
122
+ this.onSSEEvent = callback;
123
+ }
124
+
113
125
  /**
114
126
  * Check if running in client token mode
115
127
  */
@@ -117,6 +129,13 @@ export class AgentWidgetClient {
117
129
  return !!this.config.clientToken;
118
130
  }
119
131
 
132
+ /**
133
+ * Check if operating in agent execution mode
134
+ */
135
+ public isAgentMode(): boolean {
136
+ return !!this.config.agent;
137
+ }
138
+
120
139
  /**
121
140
  * Get the appropriate API URL based on mode
122
141
  */
@@ -384,6 +403,9 @@ export class AgentWidgetClient {
384
403
  * Send a message - handles both proxy and client token modes
385
404
  */
386
405
  public async dispatch(options: DispatchOptions, onEvent: SSEHandler) {
406
+ if (this.isAgentMode()) {
407
+ return this.dispatchAgent(options, onEvent);
408
+ }
387
409
  if (this.isClientTokenMode()) {
388
410
  return this.dispatchClientToken(options, onEvent);
389
411
  }
@@ -579,6 +601,146 @@ export class AgentWidgetClient {
579
601
  }
580
602
  }
581
603
 
604
+ /**
605
+ * Agent mode dispatch
606
+ */
607
+ private async dispatchAgent(options: DispatchOptions, onEvent: SSEHandler) {
608
+ const controller = new AbortController();
609
+ if (options.signal) {
610
+ options.signal.addEventListener("abort", () => controller.abort());
611
+ }
612
+
613
+ onEvent({ type: "status", status: "connecting" });
614
+
615
+ const payload = await this.buildAgentPayload(options.messages);
616
+
617
+ if (this.debug) {
618
+ // eslint-disable-next-line no-console
619
+ console.debug("[AgentWidgetClient] agent dispatch payload", payload);
620
+ }
621
+
622
+ // Build headers - merge static headers with dynamic headers if provided
623
+ let headers = { ...this.headers };
624
+ if (this.getHeaders) {
625
+ try {
626
+ const dynamicHeaders = await this.getHeaders();
627
+ headers = { ...headers, ...dynamicHeaders };
628
+ } catch (error) {
629
+ if (typeof console !== "undefined") {
630
+ // eslint-disable-next-line no-console
631
+ console.error("[AgentWidget] getHeaders error:", error);
632
+ }
633
+ }
634
+ }
635
+
636
+ // Use customFetch if provided, otherwise use default fetch
637
+ let response: Response;
638
+ if (this.customFetch) {
639
+ try {
640
+ response = await this.customFetch(
641
+ this.apiUrl,
642
+ {
643
+ method: "POST",
644
+ headers,
645
+ body: JSON.stringify(payload),
646
+ signal: controller.signal
647
+ },
648
+ payload as unknown as AgentWidgetRequestPayload
649
+ );
650
+ } catch (error) {
651
+ const err = error instanceof Error ? error : new Error(String(error));
652
+ onEvent({ type: "error", error: err });
653
+ throw err;
654
+ }
655
+ } else {
656
+ response = await fetch(this.apiUrl, {
657
+ method: "POST",
658
+ headers,
659
+ body: JSON.stringify(payload),
660
+ signal: controller.signal
661
+ });
662
+ }
663
+
664
+ if (!response.ok || !response.body) {
665
+ const error = new Error(
666
+ `Agent execution request failed: ${response.status} ${response.statusText}`
667
+ );
668
+ onEvent({ type: "error", error });
669
+ throw error;
670
+ }
671
+
672
+ onEvent({ type: "status", status: "connected" });
673
+ try {
674
+ await this.streamResponse(response.body, onEvent, options.assistantMessageId);
675
+ } finally {
676
+ onEvent({ type: "status", status: "idle" });
677
+ }
678
+ }
679
+
680
+ private async buildAgentPayload(
681
+ messages: AgentWidgetMessage[]
682
+ ): Promise<AgentWidgetAgentRequestPayload> {
683
+ if (!this.config.agent) {
684
+ throw new Error('Agent configuration required for agent mode');
685
+ }
686
+
687
+ // Filter out messages with empty content and normalize
688
+ const normalizedMessages = messages
689
+ .slice()
690
+ .filter(hasValidContent)
691
+ .filter(m => m.role === "user" || m.role === "assistant" || m.role === "system")
692
+ .filter(m => !m.variant || m.variant === "assistant")
693
+ .sort((a, b) => {
694
+ const timeA = new Date(a.createdAt).getTime();
695
+ const timeB = new Date(b.createdAt).getTime();
696
+ return timeA - timeB;
697
+ })
698
+ .map((message) => ({
699
+ role: message.role,
700
+ content: message.contentParts ?? message.llmContent ?? message.rawContent ?? message.content,
701
+ createdAt: message.createdAt
702
+ }));
703
+
704
+ const payload: AgentWidgetAgentRequestPayload = {
705
+ agent: this.config.agent,
706
+ messages: normalizedMessages,
707
+ options: {
708
+ streamResponse: true,
709
+ recordMode: 'virtual',
710
+ ...this.config.agentOptions
711
+ }
712
+ };
713
+
714
+ // Add context from providers
715
+ if (this.contextProviders.length) {
716
+ const contextAggregate: Record<string, unknown> = {};
717
+ await Promise.all(
718
+ this.contextProviders.map(async (provider) => {
719
+ try {
720
+ const result = await provider({
721
+ messages,
722
+ config: this.config
723
+ });
724
+ if (result && typeof result === "object") {
725
+ Object.assign(contextAggregate, result);
726
+ }
727
+ } catch (error) {
728
+ if (typeof console !== "undefined") {
729
+ // eslint-disable-next-line no-console
730
+ console.warn("[AgentWidget] Context provider failed:", error);
731
+ }
732
+ }
733
+ })
734
+ );
735
+
736
+ if (Object.keys(contextAggregate).length) {
737
+ payload.context = contextAggregate;
738
+ }
739
+ }
740
+
741
+ return payload;
742
+ }
743
+
582
744
  private async buildPayload(
583
745
  messages: AgentWidgetMessage[]
584
746
  ): Promise<AgentWidgetRequestPayload> {
@@ -981,6 +1143,12 @@ export class AgentWidgetClient {
981
1143
  // Track accumulated raw content for structured formats (JSON, XML, etc.)
982
1144
  const rawContentBuffers = new Map<string, string>();
983
1145
 
1146
+ // Agent execution state tracking
1147
+ let agentExecution: AgentExecutionState | null = null;
1148
+ // Track assistant messages per agent iteration for 'separate' mode
1149
+ const agentIterationMessages = new Map<number, AgentWidgetMessage>();
1150
+ const iterationDisplay = this.config.iterationDisplay ?? 'separate';
1151
+
984
1152
  // eslint-disable-next-line no-constant-condition
985
1153
  while (true) {
986
1154
  const { done, value } = await reader.read();
@@ -1021,6 +1189,9 @@ export class AgentWidgetClient {
1021
1189
  const payloadType =
1022
1190
  eventType !== "message" ? eventType : payload.type ?? "message";
1023
1191
 
1192
+ // Tap: capture raw SSE event for event stream inspector
1193
+ this.onSSEEvent?.(payloadType, payload);
1194
+
1024
1195
  // If custom SSE event parser is provided, try it first
1025
1196
  if (this.parseSSEEvent) {
1026
1197
  // Keep assistant message ref in sync
@@ -1579,6 +1750,208 @@ export class AgentWidgetClient {
1579
1750
  }
1580
1751
  }
1581
1752
  onEvent({ type: "status", status: "idle" });
1753
+ // ================================================================
1754
+ // Agent Loop Execution Events
1755
+ // ================================================================
1756
+ } else if (payloadType === "agent_start") {
1757
+ agentExecution = {
1758
+ executionId: payload.executionId,
1759
+ agentId: payload.agentId ?? 'virtual',
1760
+ agentName: payload.agentName ?? '',
1761
+ status: 'running',
1762
+ currentIteration: 0,
1763
+ maxIterations: payload.maxIterations ?? 1,
1764
+ startedAt: resolveTimestamp(payload.startedAt)
1765
+ };
1766
+ } else if (payloadType === "agent_iteration_start") {
1767
+ if (agentExecution) {
1768
+ agentExecution.currentIteration = payload.iteration;
1769
+ }
1770
+
1771
+ // In 'separate' mode, finalize previous iteration's message and create a new one
1772
+ if (iterationDisplay === 'separate' && payload.iteration > 1) {
1773
+ const prevMsg = assistantMessage as AgentWidgetMessage | null;
1774
+ if (prevMsg) {
1775
+ prevMsg.streaming = false;
1776
+ emitMessage(prevMsg);
1777
+ // Store the completed message for this iteration
1778
+ agentIterationMessages.set(payload.iteration - 1, prevMsg);
1779
+ // Reset assistant message so ensureAssistantMessage creates a new one
1780
+ assistantMessage = null;
1781
+ }
1782
+ }
1783
+ } else if (payloadType === "agent_turn_start") {
1784
+ // Nothing to do - turn tracking is handled by deltas
1785
+ } else if (payloadType === "agent_turn_delta") {
1786
+ if (payload.contentType === 'text') {
1787
+ // Stream text to assistant message
1788
+ const assistant = ensureAssistantMessage();
1789
+ assistant.content += payload.delta ?? '';
1790
+ assistant.agentMetadata = {
1791
+ executionId: payload.executionId,
1792
+ iteration: payload.iteration,
1793
+ turnId: payload.turnId,
1794
+ agentName: agentExecution?.agentName
1795
+ };
1796
+ emitMessage(assistant);
1797
+ } else if (payload.contentType === 'thinking') {
1798
+ // Stream thinking content to a reasoning message
1799
+ const reasoningId = payload.turnId ?? `agent-think-${payload.iteration}`;
1800
+ const reasoningMessage = ensureReasoningMessage(reasoningId);
1801
+ reasoningMessage.reasoning = reasoningMessage.reasoning ?? {
1802
+ id: reasoningId,
1803
+ status: "streaming",
1804
+ chunks: []
1805
+ };
1806
+ reasoningMessage.reasoning.chunks.push(payload.delta ?? '');
1807
+ reasoningMessage.agentMetadata = {
1808
+ executionId: payload.executionId,
1809
+ iteration: payload.iteration,
1810
+ turnId: payload.turnId
1811
+ };
1812
+ emitMessage(reasoningMessage);
1813
+ } else if (payload.contentType === 'tool_input') {
1814
+ // Stream tool input to current tool message
1815
+ const toolId = payload.toolCallId ?? toolContext.lastId;
1816
+ if (toolId) {
1817
+ const toolMessage = toolMessages.get(toolId);
1818
+ if (toolMessage?.toolCall) {
1819
+ toolMessage.toolCall.chunks = toolMessage.toolCall.chunks ?? [];
1820
+ toolMessage.toolCall.chunks.push(payload.delta ?? '');
1821
+ emitMessage(toolMessage);
1822
+ }
1823
+ }
1824
+ }
1825
+ } else if (payloadType === "agent_turn_complete") {
1826
+ // Mark any active reasoning for this turn as complete
1827
+ const reasoningId = payload.turnId;
1828
+ if (reasoningId) {
1829
+ const reasoningMessage = reasoningMessages.get(reasoningId);
1830
+ if (reasoningMessage?.reasoning) {
1831
+ reasoningMessage.reasoning.status = "complete";
1832
+ reasoningMessage.reasoning.completedAt = resolveTimestamp(payload.completedAt);
1833
+ const start = reasoningMessage.reasoning.startedAt ?? Date.now();
1834
+ reasoningMessage.reasoning.durationMs = Math.max(
1835
+ 0,
1836
+ (reasoningMessage.reasoning.completedAt ?? Date.now()) - start
1837
+ );
1838
+ reasoningMessage.streaming = false;
1839
+ emitMessage(reasoningMessage);
1840
+ }
1841
+ }
1842
+ } else if (payloadType === "agent_tool_start") {
1843
+ const toolId = payload.toolCallId ?? `agent-tool-${nextSequence()}`;
1844
+ trackToolId(getToolCallKey(payload), toolId);
1845
+ const toolMessage = ensureToolMessage(toolId);
1846
+ const tool = toolMessage.toolCall ?? {
1847
+ id: toolId, status: "pending" as const,
1848
+ name: undefined, args: undefined, chunks: undefined,
1849
+ result: undefined, duration: undefined, startedAt: undefined,
1850
+ completedAt: undefined, durationMs: undefined
1851
+ };
1852
+ tool.name = payload.toolName ?? tool.name;
1853
+ tool.status = "running";
1854
+ if (payload.parameters !== undefined) {
1855
+ tool.args = payload.parameters;
1856
+ }
1857
+ tool.startedAt = resolveTimestamp(payload.startedAt ?? payload.timestamp);
1858
+ toolMessage.toolCall = tool;
1859
+ toolMessage.streaming = true;
1860
+ toolMessage.agentMetadata = {
1861
+ executionId: payload.executionId,
1862
+ iteration: payload.iteration
1863
+ };
1864
+ emitMessage(toolMessage);
1865
+ } else if (payloadType === "agent_tool_delta") {
1866
+ const toolId = payload.toolCallId ?? toolContext.lastId;
1867
+ if (toolId) {
1868
+ const toolMessage = toolMessages.get(toolId) ?? ensureToolMessage(toolId);
1869
+ if (toolMessage.toolCall) {
1870
+ toolMessage.toolCall.chunks = toolMessage.toolCall.chunks ?? [];
1871
+ toolMessage.toolCall.chunks.push(payload.delta ?? '');
1872
+ toolMessage.toolCall.status = "running";
1873
+ toolMessage.streaming = true;
1874
+ emitMessage(toolMessage);
1875
+ }
1876
+ }
1877
+ } else if (payloadType === "agent_tool_complete") {
1878
+ const toolId = payload.toolCallId ?? toolContext.lastId;
1879
+ if (toolId) {
1880
+ const toolMessage = toolMessages.get(toolId) ?? ensureToolMessage(toolId);
1881
+ if (toolMessage.toolCall) {
1882
+ toolMessage.toolCall.status = "complete";
1883
+ if (payload.result !== undefined) {
1884
+ toolMessage.toolCall.result = payload.result;
1885
+ }
1886
+ if (typeof payload.executionTime === "number") {
1887
+ toolMessage.toolCall.durationMs = payload.executionTime;
1888
+ }
1889
+ toolMessage.toolCall.completedAt = resolveTimestamp(payload.completedAt ?? payload.timestamp);
1890
+ toolMessage.streaming = false;
1891
+ emitMessage(toolMessage);
1892
+ const callKey = getToolCallKey(payload);
1893
+ if (callKey) {
1894
+ toolContext.byCall.delete(callKey);
1895
+ }
1896
+ }
1897
+ }
1898
+ } else if (payloadType === "agent_iteration_complete") {
1899
+ // Iteration complete - no special handling needed
1900
+ // In 'separate' mode, message finalization happens at next iteration_start
1901
+ } else if (payloadType === "agent_reflection") {
1902
+ // Create a reasoning message for reflection content
1903
+ const reflectionId = `agent-reflection-${payload.executionId}-${payload.iteration}`;
1904
+ const reflectionMessage: AgentWidgetMessage = {
1905
+ id: reflectionId,
1906
+ role: "assistant",
1907
+ content: payload.reflection ?? '',
1908
+ createdAt: new Date().toISOString(),
1909
+ streaming: false,
1910
+ variant: "reasoning",
1911
+ sequence: nextSequence(),
1912
+ reasoning: {
1913
+ id: reflectionId,
1914
+ status: "complete",
1915
+ chunks: [payload.reflection ?? '']
1916
+ },
1917
+ agentMetadata: {
1918
+ executionId: payload.executionId,
1919
+ iteration: payload.iteration
1920
+ }
1921
+ };
1922
+ emitMessage(reflectionMessage);
1923
+ } else if (payloadType === "agent_complete") {
1924
+ if (agentExecution) {
1925
+ agentExecution.status = payload.success ? 'complete' : 'error';
1926
+ agentExecution.completedAt = resolveTimestamp(payload.completedAt);
1927
+ agentExecution.stopReason = payload.stopReason;
1928
+ }
1929
+
1930
+ // Finalize the current assistant message
1931
+ const finalMsg = assistantMessage as AgentWidgetMessage | null;
1932
+ if (finalMsg) {
1933
+ finalMsg.streaming = false;
1934
+ emitMessage(finalMsg);
1935
+ }
1936
+
1937
+ onEvent({ type: "status", status: "idle" });
1938
+ } else if (payloadType === "agent_error") {
1939
+ const errorMessage = typeof payload.error === 'string'
1940
+ ? payload.error
1941
+ : payload.error?.message ?? 'Agent execution error';
1942
+ if (payload.recoverable) {
1943
+ if (typeof console !== "undefined") {
1944
+ // eslint-disable-next-line no-console
1945
+ console.warn("[AgentWidget] Recoverable agent error:", errorMessage);
1946
+ }
1947
+ } else {
1948
+ onEvent({
1949
+ type: "error",
1950
+ error: new Error(errorMessage)
1951
+ });
1952
+ }
1953
+ } else if (payloadType === "agent_ping") {
1954
+ // Keep-alive heartbeat - no action needed
1582
1955
  } else if (payloadType === "error" && payload.error) {
1583
1956
  onEvent({
1584
1957
  type: "error",