@optilogic/chat 1.3.3 → 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 +184 -179
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +43 -2
- package/dist/index.d.ts +43 -2
- package/dist/index.js +184 -180
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
- package/src/components/agent-response/hooks/useAgentResponseAccumulator.ts +6 -216
- package/src/components/agent-response/index.ts +3 -0
- 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/editor": "1.3.
|
|
28
|
-
"@optilogic/core": "1.3.
|
|
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",
|
|
@@ -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
|
);
|
|
@@ -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) */
|