@optilogic/chat 1.3.2 → 1.3.4
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.cjs +188 -181
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +64 -2
- package/dist/index.d.ts +64 -2
- package/dist/index.js +188 -182
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
- package/src/components/agent-response/AgentResponse.tsx +27 -2
- package/src/components/agent-response/hooks/useAgentResponseAccumulator.ts +6 -216
- package/src/components/agent-response/index.ts +4 -1
- package/src/components/agent-response/reducer.ts +252 -0
- package/src/components/agent-response/types.ts +8 -0
- package/src/index.ts +3 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@optilogic/chat",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.4",
|
|
4
4
|
"description": "Chat UI components for Optilogic - AgentResponse and related components for LLM interactions",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.cjs",
|
|
@@ -24,8 +24,8 @@
|
|
|
24
24
|
"README.md"
|
|
25
25
|
],
|
|
26
26
|
"dependencies": {
|
|
27
|
-
"@optilogic/
|
|
28
|
-
"@optilogic/
|
|
27
|
+
"@optilogic/editor": "1.3.4",
|
|
28
|
+
"@optilogic/core": "1.3.4"
|
|
29
29
|
},
|
|
30
30
|
"peerDependencies": {
|
|
31
31
|
"react": "^18.0.0 || ^19.0.0",
|
|
@@ -15,6 +15,13 @@ import type { HITLInteraction } from "../hitl-interactions";
|
|
|
15
15
|
import { AgentTimeline, createTimelineUIState } from "../agent-timeline";
|
|
16
16
|
import type { TimelineUIState } from "../agent-timeline";
|
|
17
17
|
|
|
18
|
+
export interface AgentResponseClassNames {
|
|
19
|
+
/** Classes for the inner container (border, rounded corners, overflow) */
|
|
20
|
+
container?: string;
|
|
21
|
+
/** Classes for the response content section (background, padding) */
|
|
22
|
+
response?: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
18
25
|
export interface AgentResponseProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
19
26
|
/** The response state to render */
|
|
20
27
|
state: AgentResponseState;
|
|
@@ -98,6 +105,22 @@ export interface AgentResponseProps extends React.HTMLAttributes<HTMLDivElement>
|
|
|
98
105
|
* Defaults to "300px". Set to "none" to disable the constraint.
|
|
99
106
|
*/
|
|
100
107
|
timelineMaxHeight?: string;
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Class name overrides for internal elements.
|
|
111
|
+
* Use this to customize the container border/background or response section styling.
|
|
112
|
+
*
|
|
113
|
+
* @example
|
|
114
|
+
* // Transparent background and borders
|
|
115
|
+
* <AgentResponse
|
|
116
|
+
* state={state}
|
|
117
|
+
* classNames={{
|
|
118
|
+
* container: "border-transparent",
|
|
119
|
+
* response: "bg-transparent",
|
|
120
|
+
* }}
|
|
121
|
+
* />
|
|
122
|
+
*/
|
|
123
|
+
classNames?: AgentResponseClassNames;
|
|
101
124
|
}
|
|
102
125
|
|
|
103
126
|
/**
|
|
@@ -151,6 +174,7 @@ const AgentResponse = React.forwardRef<HTMLDivElement, AgentResponseProps>(
|
|
|
151
174
|
renderMarkdown,
|
|
152
175
|
renderThinkingMarkdown,
|
|
153
176
|
timelineMaxHeight,
|
|
177
|
+
classNames,
|
|
154
178
|
className,
|
|
155
179
|
...props
|
|
156
180
|
},
|
|
@@ -241,7 +265,7 @@ const AgentResponse = React.forwardRef<HTMLDivElement, AgentResponseProps>(
|
|
|
241
265
|
{...props}
|
|
242
266
|
>
|
|
243
267
|
{/* Message Content Container */}
|
|
244
|
-
<div className="border border-border rounded-lg overflow-hidden">
|
|
268
|
+
<div className={cn("border border-border rounded-lg overflow-hidden", classNames?.container)}>
|
|
245
269
|
{/* Metadata Row - show if there's any metadata or thinking */}
|
|
246
270
|
{showMetadataRow && (
|
|
247
271
|
<>
|
|
@@ -297,7 +321,8 @@ const AgentResponse = React.forwardRef<HTMLDivElement, AgentResponseProps>(
|
|
|
297
321
|
<div
|
|
298
322
|
className={cn(
|
|
299
323
|
"bg-muted/50 p-4",
|
|
300
|
-
showMetadataRow && "border-t border-border"
|
|
324
|
+
showMetadataRow && "border-t border-border",
|
|
325
|
+
classNames?.response
|
|
301
326
|
)}
|
|
302
327
|
>
|
|
303
328
|
{renderMarkdown ? (
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* useAgentResponseAccumulator Hook
|
|
3
3
|
*
|
|
4
|
-
* Accumulates agent response messages into a unified state
|
|
4
|
+
* Accumulates agent response messages into a unified state.
|
|
5
|
+
*
|
|
6
|
+
* Thin wrapper around `reduceAgentMessage` — the pure reducer can be used
|
|
7
|
+
* directly outside React for replaying historical events.
|
|
5
8
|
*/
|
|
6
9
|
|
|
7
10
|
import { useState, useCallback } from "react";
|
|
@@ -10,14 +13,8 @@ import {
|
|
|
10
13
|
type AgentResponseState,
|
|
11
14
|
type AgentMessage,
|
|
12
15
|
type GenericWebSocketMessage,
|
|
13
|
-
type ToolCall,
|
|
14
|
-
type KnowledgeItem,
|
|
15
|
-
type MemoryItem,
|
|
16
|
-
type StatusItem,
|
|
17
|
-
type ThinkingStep,
|
|
18
|
-
type PotentialResponse,
|
|
19
16
|
} from "../types";
|
|
20
|
-
import {
|
|
17
|
+
import { reduceAgentMessage } from "../reducer";
|
|
21
18
|
|
|
22
19
|
export interface UseAgentResponseAccumulatorOptions {
|
|
23
20
|
/** WebSocket topic to filter messages (optional, for convenience) */
|
|
@@ -54,7 +51,6 @@ export function useAgentResponseAccumulator(
|
|
|
54
51
|
|
|
55
52
|
const handleMessage = useCallback(
|
|
56
53
|
(message: unknown) => {
|
|
57
|
-
// If topic filter is provided, check for matching topic
|
|
58
54
|
let payload: AgentMessage;
|
|
59
55
|
|
|
60
56
|
if (topic) {
|
|
@@ -62,216 +58,10 @@ export function useAgentResponseAccumulator(
|
|
|
62
58
|
if (msg.topic !== topic) return;
|
|
63
59
|
payload = msg.message;
|
|
64
60
|
} else {
|
|
65
|
-
// Assume message is the payload directly
|
|
66
61
|
payload = message as AgentMessage;
|
|
67
62
|
}
|
|
68
63
|
|
|
69
|
-
setState((prev) =>
|
|
70
|
-
// If we receive a non-status message while idle, transition to processing
|
|
71
|
-
let newStatus = prev.status;
|
|
72
|
-
const isFirstMessage = prev.status === "idle" && payload.type !== "status";
|
|
73
|
-
if (isFirstMessage) {
|
|
74
|
-
newStatus = "processing";
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
// Track first message time for total time calculation
|
|
78
|
-
const firstMessageTime =
|
|
79
|
-
prev.firstMessageTime ?? (isFirstMessage ? Date.now() : null);
|
|
80
|
-
|
|
81
|
-
switch (payload.type) {
|
|
82
|
-
case "status":
|
|
83
|
-
// "Harness connected" resets to idle
|
|
84
|
-
if (
|
|
85
|
-
payload.message === "Harness connected" ||
|
|
86
|
-
payload.status === "Harness connected"
|
|
87
|
-
) {
|
|
88
|
-
return { ...initialAgentResponseState };
|
|
89
|
-
}
|
|
90
|
-
return { ...prev, status: newStatus };
|
|
91
|
-
|
|
92
|
-
case "thinking": {
|
|
93
|
-
// Check if this is a structured thinking step
|
|
94
|
-
if (payload.thinkingStep) {
|
|
95
|
-
const newStep: ThinkingStep = {
|
|
96
|
-
id: payload.thinkingStep.id || `step-${Date.now()}`,
|
|
97
|
-
label: payload.thinkingStep.label,
|
|
98
|
-
content: payload.thinkingStep.content,
|
|
99
|
-
depth: payload.thinkingStep.depth ?? payload.depth ?? 0,
|
|
100
|
-
isCollapsed: payload.thinkingStep.isCollapsed,
|
|
101
|
-
timestamp: Date.now(),
|
|
102
|
-
agentName: payload.agentName,
|
|
103
|
-
parentAgent: payload.parentAgent,
|
|
104
|
-
};
|
|
105
|
-
const thinkingStartTime = prev.thinkingStartTime ?? Date.now();
|
|
106
|
-
const next = {
|
|
107
|
-
...prev,
|
|
108
|
-
status: newStatus,
|
|
109
|
-
thinkingSteps: [...(prev.thinkingSteps || []), newStep],
|
|
110
|
-
thinkingStartTime,
|
|
111
|
-
firstMessageTime,
|
|
112
|
-
};
|
|
113
|
-
return { ...next, timelineEntries: buildTimelineEntries(next) };
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
// Plain text thinking — concatenate for backward compat AND
|
|
117
|
-
// push a ThinkingStep so the timeline gets individual entries.
|
|
118
|
-
const newThinking = payload.message || payload.content || "";
|
|
119
|
-
// Add line break between thinking messages
|
|
120
|
-
const separator = prev.thinking && newThinking ? "\n\n" : "";
|
|
121
|
-
// Set thinkingStartTime on first thinking message
|
|
122
|
-
const thinkingStartTime =
|
|
123
|
-
prev.thinkingStartTime ?? (newThinking ? Date.now() : null);
|
|
124
|
-
const prevSteps = prev.thinkingSteps || [];
|
|
125
|
-
const plainStep: ThinkingStep = {
|
|
126
|
-
id: `step-${prevSteps.length}`,
|
|
127
|
-
label: newThinking,
|
|
128
|
-
content: newThinking,
|
|
129
|
-
depth: payload.depth ?? 0,
|
|
130
|
-
timestamp: Date.now(),
|
|
131
|
-
agentName: payload.agentName,
|
|
132
|
-
parentAgent: payload.parentAgent,
|
|
133
|
-
};
|
|
134
|
-
const next = {
|
|
135
|
-
...prev,
|
|
136
|
-
status: newStatus,
|
|
137
|
-
thinking: prev.thinking + separator + newThinking,
|
|
138
|
-
thinkingSteps: [...prevSteps, plainStep],
|
|
139
|
-
thinkingStartTime,
|
|
140
|
-
firstMessageTime,
|
|
141
|
-
};
|
|
142
|
-
return { ...next, timelineEntries: buildTimelineEntries(next) };
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
case "tool_call": {
|
|
146
|
-
// Handle both formats: { message: "ToolName" } or { tool: { id, name, arguments } }
|
|
147
|
-
const toolName = payload.message || payload.tool?.name;
|
|
148
|
-
if (toolName) {
|
|
149
|
-
const newToolCall: ToolCall = {
|
|
150
|
-
id: payload.tool?.id || `tool-${Date.now()}`,
|
|
151
|
-
name: toolName,
|
|
152
|
-
arguments: payload.tool?.arguments,
|
|
153
|
-
timestamp: Date.now(),
|
|
154
|
-
agentName: payload.agentName,
|
|
155
|
-
parentAgent: payload.parentAgent,
|
|
156
|
-
depth: payload.depth,
|
|
157
|
-
};
|
|
158
|
-
const next = {
|
|
159
|
-
...prev,
|
|
160
|
-
status: newStatus,
|
|
161
|
-
toolCalls: [...prev.toolCalls, newToolCall],
|
|
162
|
-
firstMessageTime,
|
|
163
|
-
};
|
|
164
|
-
return { ...next, timelineEntries: buildTimelineEntries(next) };
|
|
165
|
-
}
|
|
166
|
-
return { ...prev, status: newStatus, firstMessageTime };
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
case "knowledge": {
|
|
170
|
-
// Handle both formats: { message: "content" } or { knowledge: { id, source, content } }
|
|
171
|
-
const knowledgeContent = payload.message || payload.knowledge?.content;
|
|
172
|
-
if (knowledgeContent) {
|
|
173
|
-
const newKnowledge: KnowledgeItem = {
|
|
174
|
-
id: payload.knowledge?.id || `knowledge-${Date.now()}`,
|
|
175
|
-
source: payload.knowledge?.source || "unknown",
|
|
176
|
-
content: knowledgeContent,
|
|
177
|
-
timestamp: Date.now(),
|
|
178
|
-
agentName: payload.agentName,
|
|
179
|
-
parentAgent: payload.parentAgent,
|
|
180
|
-
depth: payload.depth,
|
|
181
|
-
};
|
|
182
|
-
const next = {
|
|
183
|
-
...prev,
|
|
184
|
-
status: newStatus,
|
|
185
|
-
knowledge: [...prev.knowledge, newKnowledge],
|
|
186
|
-
firstMessageTime,
|
|
187
|
-
};
|
|
188
|
-
return { ...next, timelineEntries: buildTimelineEntries(next) };
|
|
189
|
-
}
|
|
190
|
-
return { ...prev, status: newStatus, firstMessageTime };
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
case "memory": {
|
|
194
|
-
// Handle both formats: { message: "content" } or { memory: { id, type, content } }
|
|
195
|
-
const memoryContent = payload.message || payload.memory?.content;
|
|
196
|
-
if (memoryContent) {
|
|
197
|
-
const newMemory: MemoryItem = {
|
|
198
|
-
id: payload.memory?.id || `memory-${Date.now()}`,
|
|
199
|
-
type: payload.memory?.type || "unknown",
|
|
200
|
-
content: memoryContent,
|
|
201
|
-
timestamp: Date.now(),
|
|
202
|
-
agentName: payload.agentName,
|
|
203
|
-
parentAgent: payload.parentAgent,
|
|
204
|
-
depth: payload.depth,
|
|
205
|
-
};
|
|
206
|
-
const next = {
|
|
207
|
-
...prev,
|
|
208
|
-
status: newStatus,
|
|
209
|
-
memory: [...prev.memory, newMemory],
|
|
210
|
-
firstMessageTime,
|
|
211
|
-
};
|
|
212
|
-
return { ...next, timelineEntries: buildTimelineEntries(next) };
|
|
213
|
-
}
|
|
214
|
-
return { ...prev, status: newStatus, firstMessageTime };
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
case "response":
|
|
218
|
-
return {
|
|
219
|
-
...prev,
|
|
220
|
-
status: "complete",
|
|
221
|
-
response: payload.message || payload.content || "",
|
|
222
|
-
responseCompleteTime: Date.now(),
|
|
223
|
-
firstMessageTime: prev.firstMessageTime ?? Date.now(),
|
|
224
|
-
};
|
|
225
|
-
|
|
226
|
-
case "status_update": {
|
|
227
|
-
const statusMessage = payload.message || payload.statusUpdate?.message;
|
|
228
|
-
if (statusMessage) {
|
|
229
|
-
const newStatusItem: StatusItem = {
|
|
230
|
-
id: payload.statusUpdate?.id || `status-${Date.now()}`,
|
|
231
|
-
message: statusMessage,
|
|
232
|
-
agent: payload.statusUpdate?.agent,
|
|
233
|
-
timestamp: Date.now(),
|
|
234
|
-
agentName: payload.agentName,
|
|
235
|
-
parentAgent: payload.parentAgent,
|
|
236
|
-
depth: payload.depth,
|
|
237
|
-
};
|
|
238
|
-
const next = {
|
|
239
|
-
...prev,
|
|
240
|
-
status: newStatus,
|
|
241
|
-
statusUpdates: [...prev.statusUpdates, newStatusItem],
|
|
242
|
-
firstMessageTime,
|
|
243
|
-
};
|
|
244
|
-
return { ...next, timelineEntries: buildTimelineEntries(next) };
|
|
245
|
-
}
|
|
246
|
-
return { ...prev, status: newStatus, firstMessageTime };
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
case "potential_response": {
|
|
250
|
-
const respContent = payload.message || payload.content || "";
|
|
251
|
-
if (respContent) {
|
|
252
|
-
const newResp: PotentialResponse = {
|
|
253
|
-
id: `resp-${Date.now()}`,
|
|
254
|
-
content: respContent,
|
|
255
|
-
timestamp: Date.now(),
|
|
256
|
-
agentName: payload.agentName,
|
|
257
|
-
parentAgent: payload.parentAgent,
|
|
258
|
-
depth: payload.depth,
|
|
259
|
-
};
|
|
260
|
-
const next = {
|
|
261
|
-
...prev,
|
|
262
|
-
status: newStatus,
|
|
263
|
-
potentialResponses: [...(prev.potentialResponses || []), newResp],
|
|
264
|
-
firstMessageTime,
|
|
265
|
-
};
|
|
266
|
-
return { ...next, timelineEntries: buildTimelineEntries(next) };
|
|
267
|
-
}
|
|
268
|
-
return { ...prev, status: newStatus, firstMessageTime };
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
default:
|
|
272
|
-
return { ...prev, status: newStatus, firstMessageTime };
|
|
273
|
-
}
|
|
274
|
-
});
|
|
64
|
+
setState((prev) => reduceAgentMessage(prev, payload));
|
|
275
65
|
},
|
|
276
66
|
[topic]
|
|
277
67
|
);
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
// Main component
|
|
9
9
|
export { AgentResponse } from "./AgentResponse";
|
|
10
|
-
export type { AgentResponseProps } from "./AgentResponse";
|
|
10
|
+
export type { AgentResponseProps, AgentResponseClassNames } from "./AgentResponse";
|
|
11
11
|
|
|
12
12
|
// Sub-components (for advanced customization)
|
|
13
13
|
export {
|
|
@@ -52,6 +52,9 @@ export type {
|
|
|
52
52
|
|
|
53
53
|
export { initialAgentResponseState } from "./types";
|
|
54
54
|
|
|
55
|
+
// Pure reducer (for non-React replay of AgentMessage streams)
|
|
56
|
+
export { reduceAgentMessage } from "./reducer";
|
|
57
|
+
|
|
55
58
|
// Utilities
|
|
56
59
|
export { formatTime, formatTotalTime } from "./utils";
|
|
57
60
|
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure reducer for agent response messages.
|
|
3
|
+
*
|
|
4
|
+
* Used by `useAgentResponseAccumulator` for live streaming, and exported so
|
|
5
|
+
* non-React callers (e.g. server-side or store-based replay of historical
|
|
6
|
+
* conversations) can rebuild the same `AgentResponseState` from a sequence of
|
|
7
|
+
* `AgentMessage` events without rendering a component.
|
|
8
|
+
*
|
|
9
|
+
* Replay note on stable IDs: when a payload omits a per-item id
|
|
10
|
+
* (`tool.id`, `thinkingStep.id`, `knowledge.id`, `memory.id`,
|
|
11
|
+
* `statusUpdate.id`), the reducer falls back to `${type}-${Date.now()}`. That
|
|
12
|
+
* is fine for live streams but will produce different React keys on each
|
|
13
|
+
* replay. Callers reconstructing `AgentMessage` events from persisted rows
|
|
14
|
+
* should populate these ids from a stable source (e.g. the supplement-row
|
|
15
|
+
* primary key) so timeline keys remain consistent across reloads.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import {
|
|
19
|
+
initialAgentResponseState,
|
|
20
|
+
type AgentResponseState,
|
|
21
|
+
type AgentMessage,
|
|
22
|
+
type ToolCall,
|
|
23
|
+
type KnowledgeItem,
|
|
24
|
+
type MemoryItem,
|
|
25
|
+
type StatusItem,
|
|
26
|
+
type ThinkingStep,
|
|
27
|
+
type PotentialResponse,
|
|
28
|
+
} from "./types";
|
|
29
|
+
import { buildTimelineEntries } from "../agent-timeline/utils";
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Pure reducer: apply a single `AgentMessage` to the accumulated state.
|
|
33
|
+
*
|
|
34
|
+
* `payload.timestamp` (epoch ms), if supplied, is used for the new item's
|
|
35
|
+
* `timestamp` and any state-level timing fields this call sets
|
|
36
|
+
* (`firstMessageTime`, `thinkingStartTime`, `responseCompleteTime`). When
|
|
37
|
+
* absent, `Date.now()` is used — matching the prior live-streaming behaviour.
|
|
38
|
+
*
|
|
39
|
+
* @example
|
|
40
|
+
* const state = events.reduce(reduceAgentMessage, initialAgentResponseState);
|
|
41
|
+
*/
|
|
42
|
+
export function reduceAgentMessage(
|
|
43
|
+
prev: AgentResponseState,
|
|
44
|
+
payload: AgentMessage,
|
|
45
|
+
): AgentResponseState {
|
|
46
|
+
const now = payload.timestamp ?? Date.now();
|
|
47
|
+
|
|
48
|
+
// If we receive a non-status message while idle, transition to processing
|
|
49
|
+
let newStatus = prev.status;
|
|
50
|
+
const isFirstMessage = prev.status === "idle" && payload.type !== "status";
|
|
51
|
+
if (isFirstMessage) {
|
|
52
|
+
newStatus = "processing";
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Track first message time for total time calculation
|
|
56
|
+
const firstMessageTime =
|
|
57
|
+
prev.firstMessageTime ?? (isFirstMessage ? now : null);
|
|
58
|
+
|
|
59
|
+
switch (payload.type) {
|
|
60
|
+
case "status":
|
|
61
|
+
// "Harness connected" resets to idle
|
|
62
|
+
if (
|
|
63
|
+
payload.message === "Harness connected" ||
|
|
64
|
+
payload.status === "Harness connected"
|
|
65
|
+
) {
|
|
66
|
+
return { ...initialAgentResponseState };
|
|
67
|
+
}
|
|
68
|
+
return { ...prev, status: newStatus };
|
|
69
|
+
|
|
70
|
+
case "thinking": {
|
|
71
|
+
// Check if this is a structured thinking step
|
|
72
|
+
if (payload.thinkingStep) {
|
|
73
|
+
const newStep: ThinkingStep = {
|
|
74
|
+
id: payload.thinkingStep.id || `step-${now}`,
|
|
75
|
+
label: payload.thinkingStep.label,
|
|
76
|
+
content: payload.thinkingStep.content,
|
|
77
|
+
depth: payload.thinkingStep.depth ?? payload.depth ?? 0,
|
|
78
|
+
isCollapsed: payload.thinkingStep.isCollapsed,
|
|
79
|
+
timestamp: now,
|
|
80
|
+
agentName: payload.agentName,
|
|
81
|
+
parentAgent: payload.parentAgent,
|
|
82
|
+
};
|
|
83
|
+
const thinkingStartTime = prev.thinkingStartTime ?? now;
|
|
84
|
+
const next = {
|
|
85
|
+
...prev,
|
|
86
|
+
status: newStatus,
|
|
87
|
+
thinkingSteps: [...(prev.thinkingSteps || []), newStep],
|
|
88
|
+
thinkingStartTime,
|
|
89
|
+
firstMessageTime,
|
|
90
|
+
};
|
|
91
|
+
return { ...next, timelineEntries: buildTimelineEntries(next) };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Plain text thinking — concatenate for backward compat AND
|
|
95
|
+
// push a ThinkingStep so the timeline gets individual entries.
|
|
96
|
+
const newThinking = payload.message || payload.content || "";
|
|
97
|
+
// Add line break between thinking messages
|
|
98
|
+
const separator = prev.thinking && newThinking ? "\n\n" : "";
|
|
99
|
+
// Set thinkingStartTime on first thinking message
|
|
100
|
+
const thinkingStartTime =
|
|
101
|
+
prev.thinkingStartTime ?? (newThinking ? now : null);
|
|
102
|
+
const prevSteps = prev.thinkingSteps || [];
|
|
103
|
+
const plainStep: ThinkingStep = {
|
|
104
|
+
id: `step-${prevSteps.length}`,
|
|
105
|
+
label: newThinking,
|
|
106
|
+
content: newThinking,
|
|
107
|
+
depth: payload.depth ?? 0,
|
|
108
|
+
timestamp: now,
|
|
109
|
+
agentName: payload.agentName,
|
|
110
|
+
parentAgent: payload.parentAgent,
|
|
111
|
+
};
|
|
112
|
+
const next = {
|
|
113
|
+
...prev,
|
|
114
|
+
status: newStatus,
|
|
115
|
+
thinking: prev.thinking + separator + newThinking,
|
|
116
|
+
thinkingSteps: [...prevSteps, plainStep],
|
|
117
|
+
thinkingStartTime,
|
|
118
|
+
firstMessageTime,
|
|
119
|
+
};
|
|
120
|
+
return { ...next, timelineEntries: buildTimelineEntries(next) };
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
case "tool_call": {
|
|
124
|
+
// Handle both formats: { message: "ToolName" } or { tool: { id, name, arguments } }
|
|
125
|
+
const toolName = payload.message || payload.tool?.name;
|
|
126
|
+
if (toolName) {
|
|
127
|
+
const newToolCall: ToolCall = {
|
|
128
|
+
id: payload.tool?.id || `tool-${now}`,
|
|
129
|
+
name: toolName,
|
|
130
|
+
arguments: payload.tool?.arguments,
|
|
131
|
+
timestamp: now,
|
|
132
|
+
agentName: payload.agentName,
|
|
133
|
+
parentAgent: payload.parentAgent,
|
|
134
|
+
depth: payload.depth,
|
|
135
|
+
};
|
|
136
|
+
const next = {
|
|
137
|
+
...prev,
|
|
138
|
+
status: newStatus,
|
|
139
|
+
toolCalls: [...prev.toolCalls, newToolCall],
|
|
140
|
+
firstMessageTime,
|
|
141
|
+
};
|
|
142
|
+
return { ...next, timelineEntries: buildTimelineEntries(next) };
|
|
143
|
+
}
|
|
144
|
+
return { ...prev, status: newStatus, firstMessageTime };
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
case "knowledge": {
|
|
148
|
+
// Handle both formats: { message: "content" } or { knowledge: { id, source, content } }
|
|
149
|
+
const knowledgeContent = payload.message || payload.knowledge?.content;
|
|
150
|
+
if (knowledgeContent) {
|
|
151
|
+
const newKnowledge: KnowledgeItem = {
|
|
152
|
+
id: payload.knowledge?.id || `knowledge-${now}`,
|
|
153
|
+
source: payload.knowledge?.source || "unknown",
|
|
154
|
+
content: knowledgeContent,
|
|
155
|
+
timestamp: now,
|
|
156
|
+
agentName: payload.agentName,
|
|
157
|
+
parentAgent: payload.parentAgent,
|
|
158
|
+
depth: payload.depth,
|
|
159
|
+
};
|
|
160
|
+
const next = {
|
|
161
|
+
...prev,
|
|
162
|
+
status: newStatus,
|
|
163
|
+
knowledge: [...prev.knowledge, newKnowledge],
|
|
164
|
+
firstMessageTime,
|
|
165
|
+
};
|
|
166
|
+
return { ...next, timelineEntries: buildTimelineEntries(next) };
|
|
167
|
+
}
|
|
168
|
+
return { ...prev, status: newStatus, firstMessageTime };
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
case "memory": {
|
|
172
|
+
// Handle both formats: { message: "content" } or { memory: { id, type, content } }
|
|
173
|
+
const memoryContent = payload.message || payload.memory?.content;
|
|
174
|
+
if (memoryContent) {
|
|
175
|
+
const newMemory: MemoryItem = {
|
|
176
|
+
id: payload.memory?.id || `memory-${now}`,
|
|
177
|
+
type: payload.memory?.type || "unknown",
|
|
178
|
+
content: memoryContent,
|
|
179
|
+
timestamp: now,
|
|
180
|
+
agentName: payload.agentName,
|
|
181
|
+
parentAgent: payload.parentAgent,
|
|
182
|
+
depth: payload.depth,
|
|
183
|
+
};
|
|
184
|
+
const next = {
|
|
185
|
+
...prev,
|
|
186
|
+
status: newStatus,
|
|
187
|
+
memory: [...prev.memory, newMemory],
|
|
188
|
+
firstMessageTime,
|
|
189
|
+
};
|
|
190
|
+
return { ...next, timelineEntries: buildTimelineEntries(next) };
|
|
191
|
+
}
|
|
192
|
+
return { ...prev, status: newStatus, firstMessageTime };
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
case "response":
|
|
196
|
+
return {
|
|
197
|
+
...prev,
|
|
198
|
+
status: "complete",
|
|
199
|
+
response: payload.message || payload.content || "",
|
|
200
|
+
responseCompleteTime: now,
|
|
201
|
+
firstMessageTime: prev.firstMessageTime ?? now,
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
case "status_update": {
|
|
205
|
+
const statusMessage = payload.message || payload.statusUpdate?.message;
|
|
206
|
+
if (statusMessage) {
|
|
207
|
+
const newStatusItem: StatusItem = {
|
|
208
|
+
id: payload.statusUpdate?.id || `status-${now}`,
|
|
209
|
+
message: statusMessage,
|
|
210
|
+
agent: payload.statusUpdate?.agent,
|
|
211
|
+
timestamp: now,
|
|
212
|
+
agentName: payload.agentName,
|
|
213
|
+
parentAgent: payload.parentAgent,
|
|
214
|
+
depth: payload.depth,
|
|
215
|
+
};
|
|
216
|
+
const next = {
|
|
217
|
+
...prev,
|
|
218
|
+
status: newStatus,
|
|
219
|
+
statusUpdates: [...prev.statusUpdates, newStatusItem],
|
|
220
|
+
firstMessageTime,
|
|
221
|
+
};
|
|
222
|
+
return { ...next, timelineEntries: buildTimelineEntries(next) };
|
|
223
|
+
}
|
|
224
|
+
return { ...prev, status: newStatus, firstMessageTime };
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
case "potential_response": {
|
|
228
|
+
const respContent = payload.message || payload.content || "";
|
|
229
|
+
if (respContent) {
|
|
230
|
+
const newResp: PotentialResponse = {
|
|
231
|
+
id: `resp-${now}`,
|
|
232
|
+
content: respContent,
|
|
233
|
+
timestamp: now,
|
|
234
|
+
agentName: payload.agentName,
|
|
235
|
+
parentAgent: payload.parentAgent,
|
|
236
|
+
depth: payload.depth,
|
|
237
|
+
};
|
|
238
|
+
const next = {
|
|
239
|
+
...prev,
|
|
240
|
+
status: newStatus,
|
|
241
|
+
potentialResponses: [...(prev.potentialResponses || []), newResp],
|
|
242
|
+
firstMessageTime,
|
|
243
|
+
};
|
|
244
|
+
return { ...next, timelineEntries: buildTimelineEntries(next) };
|
|
245
|
+
}
|
|
246
|
+
return { ...prev, status: newStatus, firstMessageTime };
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
default:
|
|
250
|
+
return { ...prev, status: newStatus, firstMessageTime };
|
|
251
|
+
}
|
|
252
|
+
}
|
|
@@ -165,6 +165,14 @@ export interface AgentMessage {
|
|
|
165
165
|
message?: string;
|
|
166
166
|
/** Alternative content field */
|
|
167
167
|
content?: string;
|
|
168
|
+
/**
|
|
169
|
+
* Optional event timestamp (epoch ms). When supplied, the reducer uses this
|
|
170
|
+
* for the item's `timestamp` and any state-level timing fields it sets
|
|
171
|
+
* (`firstMessageTime`, `thinkingStartTime`, `responseCompleteTime`) instead
|
|
172
|
+
* of `Date.now()`. Provide this when replaying historical events so durations
|
|
173
|
+
* reflect the original run rather than load time.
|
|
174
|
+
*/
|
|
175
|
+
timestamp?: number;
|
|
168
176
|
/** For status messages */
|
|
169
177
|
status?: string;
|
|
170
178
|
/** Agent name (multi-agent scenarios) */
|