@dxos/react-ui-trace 0.11.1 → 0.14.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/README.md +1 -1
- package/dist/lib/index.mjs +1722 -561
- package/dist/lib/index.mjs.map +1 -1
- package/dist/lib/testing.mjs +9 -8
- package/dist/lib/testing.mjs.map +1 -1
- package/dist/lib/translations.mjs +5 -0
- package/dist/lib/translations.mjs.map +1 -1
- package/dist/types/src/components/Gantt/Gantt.d.ts +109 -24
- package/dist/types/src/components/Gantt/Gantt.d.ts.map +1 -1
- package/dist/types/src/components/Gantt/Gantt.stories.d.ts +73 -3
- package/dist/types/src/components/Gantt/Gantt.stories.d.ts.map +1 -1
- package/dist/types/src/components/Gantt/gantt-rows.d.ts +26 -0
- package/dist/types/src/components/Gantt/gantt-rows.d.ts.map +1 -0
- package/dist/types/src/components/Gantt/gantt-rows.test.d.ts +2 -0
- package/dist/types/src/components/Gantt/gantt-rows.test.d.ts.map +1 -0
- package/dist/types/src/components/Gantt/gantt-scale.d.ts +57 -0
- package/dist/types/src/components/Gantt/gantt-scale.d.ts.map +1 -0
- package/dist/types/src/components/Gantt/gantt-scale.test.d.ts +2 -0
- package/dist/types/src/components/Gantt/gantt-scale.test.d.ts.map +1 -0
- package/dist/types/src/components/Gantt/index.d.ts +2 -0
- package/dist/types/src/components/Gantt/index.d.ts.map +1 -1
- package/dist/types/src/components/Gantt/useEnter.d.ts +13 -0
- package/dist/types/src/components/Gantt/useEnter.d.ts.map +1 -0
- package/dist/types/src/components/Gantt/useRangeTween.d.ts +12 -0
- package/dist/types/src/components/Gantt/useRangeTween.d.ts.map +1 -0
- package/dist/types/src/components/ProcessTree/ProcessTree.d.ts +3 -3
- package/dist/types/src/components/ProcessTree/ProcessTree.d.ts.map +1 -1
- package/dist/types/src/components/ProcessTree/ProcessTree.stories.d.ts.map +1 -1
- package/dist/types/src/components/Timeline/Timeline.d.ts +10 -2
- package/dist/types/src/components/Timeline/Timeline.d.ts.map +1 -1
- package/dist/types/src/components/Timeline/Timeline.stories.d.ts +8 -4
- package/dist/types/src/components/Timeline/Timeline.stories.d.ts.map +1 -1
- package/dist/types/src/components/Timeline/timeline-layout.d.ts +35 -0
- package/dist/types/src/components/Timeline/timeline-layout.d.ts.map +1 -0
- package/dist/types/src/components/Timeline/timeline-layout.test.d.ts +2 -0
- package/dist/types/src/components/Timeline/timeline-layout.test.d.ts.map +1 -0
- package/dist/types/src/components/TracePanel/SpanTreeView.d.ts +15 -0
- package/dist/types/src/components/TracePanel/SpanTreeView.d.ts.map +1 -0
- package/dist/types/src/components/TracePanel/TracePanel.d.ts +3 -3
- package/dist/types/src/components/TracePanel/TracePanel.d.ts.map +1 -1
- package/dist/types/src/components/TracePanel/TracePanel.stories.d.ts +46 -0
- package/dist/types/src/components/TracePanel/TracePanel.stories.d.ts.map +1 -0
- package/dist/types/src/components/TracePanel/trace-filter.d.ts +7 -2
- package/dist/types/src/components/TracePanel/trace-filter.d.ts.map +1 -1
- package/dist/types/src/components/TracePanel/useTraceMenu.d.ts.map +1 -1
- package/dist/types/src/execution-graph/execution-graph.d.ts +1 -1
- package/dist/types/src/execution-graph/execution-graph.d.ts.map +1 -1
- package/dist/types/src/hooks/useExecutionGraph.d.ts +2 -2
- package/dist/types/src/hooks/useExecutionGraph.d.ts.map +1 -1
- package/dist/types/src/hooks/useSessionTimeline.d.ts +1 -1
- package/dist/types/src/hooks/useSessionTimeline.d.ts.map +1 -1
- package/dist/types/src/hooks/useTraceMessages.d.ts +1 -1
- package/dist/types/src/hooks/useTraceMessages.d.ts.map +1 -1
- package/dist/types/src/session-timeline/TaskHistory.stories.d.ts +12 -0
- package/dist/types/src/session-timeline/TaskHistory.stories.d.ts.map +1 -0
- package/dist/types/src/session-timeline/gantt-mapping.d.ts +14 -0
- package/dist/types/src/session-timeline/gantt-mapping.d.ts.map +1 -0
- package/dist/types/src/session-timeline/gantt-mapping.test.d.ts +2 -0
- package/dist/types/src/session-timeline/gantt-mapping.test.d.ts.map +1 -0
- package/dist/types/src/session-timeline/index.d.ts +1 -0
- package/dist/types/src/session-timeline/index.d.ts.map +1 -1
- package/dist/types/src/session-timeline/session-timeline.d.ts +17 -2
- package/dist/types/src/session-timeline/session-timeline.d.ts.map +1 -1
- package/dist/types/src/session-timeline/types.d.ts +32 -0
- package/dist/types/src/session-timeline/types.d.ts.map +1 -1
- package/dist/types/src/testing/simulated-agent.d.ts +5 -4
- package/dist/types/src/testing/simulated-agent.d.ts.map +1 -1
- package/dist/types/src/testing/test-trace.d.ts +2 -2
- package/dist/types/src/testing/test-trace.d.ts.map +1 -1
- package/dist/types/src/translations.d.ts +5 -0
- package/dist/types/src/translations.d.ts.map +1 -1
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/package.json +30 -26
- package/src/components/Gantt/Gantt.stories.tsx +914 -96
- package/src/components/Gantt/Gantt.tsx +964 -261
- package/src/components/Gantt/gantt-rows.test.ts +123 -0
- package/src/components/Gantt/gantt-rows.ts +71 -0
- package/src/components/Gantt/gantt-scale.test.ts +105 -0
- package/src/components/Gantt/gantt-scale.ts +143 -0
- package/src/components/Gantt/index.ts +2 -0
- package/src/components/Gantt/useEnter.ts +33 -0
- package/src/components/Gantt/useRangeTween.ts +61 -0
- package/src/components/ProcessTree/ProcessTree.stories.tsx +2 -3
- package/src/components/ProcessTree/ProcessTree.tsx +99 -90
- package/src/components/Timeline/Timeline.stories.tsx +110 -22
- package/src/components/Timeline/Timeline.tsx +319 -271
- package/src/components/Timeline/timeline-layout.test.ts +177 -0
- package/src/components/Timeline/timeline-layout.ts +153 -0
- package/src/components/TracePanel/SpanTreeView.tsx +39 -0
- package/src/components/TracePanel/TracePanel.stories.tsx +106 -0
- package/src/components/TracePanel/TracePanel.tsx +113 -106
- package/src/components/TracePanel/trace-filter.test.ts +31 -2
- package/src/components/TracePanel/trace-filter.ts +28 -3
- package/src/components/TracePanel/useTraceMenu.ts +0 -1
- package/src/execution-graph/SPEC.md +10 -10
- package/src/execution-graph/execution-graph.test.ts +16 -15
- package/src/execution-graph/execution-graph.ts +108 -60
- package/src/execution-graph/pending-block-status.ts +1 -1
- package/src/execution-graph/sub-agent-delegation.test.ts +1 -1
- package/src/hooks/useExecutionGraph.ts +14 -11
- package/src/hooks/useSessionTimeline.ts +35 -9
- package/src/hooks/useTraceMessages.ts +1 -1
- package/src/session-timeline/TaskHistory.stories.tsx +142 -0
- package/src/session-timeline/gantt-mapping.test.ts +118 -0
- package/src/session-timeline/gantt-mapping.ts +158 -0
- package/src/session-timeline/index.ts +1 -0
- package/src/session-timeline/session-timeline.test.ts +585 -35
- package/src/session-timeline/session-timeline.ts +433 -62
- package/src/session-timeline/types.ts +14 -0
- package/src/testing/simulated-agent.ts +17 -16
- package/src/testing/test-trace.ts +14 -12
- package/src/translations.ts +8 -2
- package/dist/types/src/execution-graph/remote-snapshot.test.d.ts +0 -2
- package/dist/types/src/execution-graph/remote-snapshot.test.d.ts.map +0 -1
- package/src/execution-graph/remote-snapshot.test.ts +0 -116
|
@@ -7,13 +7,14 @@ import * as Schema from 'effect/Schema';
|
|
|
7
7
|
|
|
8
8
|
import * as Process from '@dxos/compute/Process';
|
|
9
9
|
import * as Trace from '@dxos/compute/Trace';
|
|
10
|
-
import { Annotation } from '@dxos/echo';
|
|
10
|
+
import { Annotation, Obj } from '@dxos/echo';
|
|
11
11
|
import { EID } from '@dxos/keys';
|
|
12
12
|
import { Task } from '@dxos/types';
|
|
13
|
+
import { getHashHue } from '@dxos/ui-theme';
|
|
13
14
|
|
|
14
15
|
import { type Span, buildSpanTree, flattenSpanTree } from '../execution-graph/index.ts';
|
|
15
|
-
|
|
16
16
|
import {
|
|
17
|
+
type DelegationSource,
|
|
17
18
|
type Lane,
|
|
18
19
|
type LaneStatus,
|
|
19
20
|
type Marker,
|
|
@@ -24,19 +25,99 @@ import {
|
|
|
24
25
|
|
|
25
26
|
export interface BuildSessionTimelineInput {
|
|
26
27
|
traceMessages: readonly Trace.Message[];
|
|
27
|
-
processes?: readonly Process.
|
|
28
|
+
processes?: readonly Process.Process[];
|
|
28
29
|
/** The sessions to draw; each contributes a lane and its checklist's task lanes. */
|
|
29
30
|
sessions?: readonly Session[];
|
|
30
31
|
tasks?: readonly Task.Task[];
|
|
32
|
+
/** Per task id, the status moves its edit history records, oldest first (see {@link readTaskStatusChanges}). */
|
|
33
|
+
taskStatusChanges?: ReadonlyMap<string, readonly TaskStatusChange[]>;
|
|
31
34
|
/** Reference time; extends the range past open lanes. */
|
|
32
35
|
now?: number;
|
|
33
36
|
}
|
|
34
37
|
|
|
38
|
+
/** One move of a task's status, as its edit history records it. */
|
|
39
|
+
export type TaskStatusChange = {
|
|
40
|
+
/** Epoch ms. */
|
|
41
|
+
timestamp: number;
|
|
42
|
+
status: Task.Status;
|
|
43
|
+
/** Absent when the task held no status before. */
|
|
44
|
+
previousStatus?: Task.Status;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const isStatus = Schema.is(Task.Status);
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* The status moves in a task's edit history, oldest first; none for a task not stored in a database,
|
|
51
|
+
* which keeps no history.
|
|
52
|
+
*/
|
|
53
|
+
export const readTaskStatusChanges = (task: Task.Task): TaskStatusChange[] =>
|
|
54
|
+
Obj.getDatabase(task) === undefined
|
|
55
|
+
? []
|
|
56
|
+
: Obj.getChanges(task, { property: 'status' }).flatMap(({ time, before, after }) =>
|
|
57
|
+
isStatus(after)
|
|
58
|
+
? [{ timestamp: time, status: after, ...(isStatus(before) ? { previousStatus: before } : {}) }]
|
|
59
|
+
: [],
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
/** Statuses of a task not yet picked up. */
|
|
63
|
+
const PENDING_STATUSES = new Set<Task.Status>(['todo', 'backlog']);
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* The edit history's status moves, each timed by the trace event recording the same move when there is
|
|
67
|
+
* one. History times are whole seconds, rounded down, so on their own they start a lane up to a second
|
|
68
|
+
* before the node that marks its start; everything derived from them — spans, nodes — reads these.
|
|
69
|
+
*/
|
|
70
|
+
const refineStatusChanges = (
|
|
71
|
+
changes: ReadonlyMap<string, readonly TaskStatusChange[]>,
|
|
72
|
+
events: readonly Trace.FlatEvent[],
|
|
73
|
+
): ReadonlyMap<string, readonly TaskStatusChange[]> => {
|
|
74
|
+
const traced = new Map<string, { timestamp: number; status: string }[]>();
|
|
75
|
+
for (const event of events) {
|
|
76
|
+
if (event.type === Trace.TaskStatusChanged.key) {
|
|
77
|
+
const data = decode(Trace.TaskStatusChanged.schema, event.data);
|
|
78
|
+
if (data) {
|
|
79
|
+
traced.set(data.taskId, [
|
|
80
|
+
...(traced.get(data.taskId) ?? []),
|
|
81
|
+
{ timestamp: event.timestamp, status: data.status },
|
|
82
|
+
]);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return new Map(
|
|
87
|
+
[...changes].map(([taskId, list]) => {
|
|
88
|
+
const candidates = [...(traced.get(taskId) ?? [])];
|
|
89
|
+
return [
|
|
90
|
+
taskId,
|
|
91
|
+
list.map((change) => {
|
|
92
|
+
const index = candidates.findIndex(
|
|
93
|
+
({ timestamp, status }) =>
|
|
94
|
+
status === change.status && Math.abs(timestamp - change.timestamp) <= HISTORY_MATCH_MS,
|
|
95
|
+
);
|
|
96
|
+
if (index < 0) {
|
|
97
|
+
return change;
|
|
98
|
+
}
|
|
99
|
+
const [match] = candidates.splice(index, 1);
|
|
100
|
+
return { ...change, timestamp: match.timestamp };
|
|
101
|
+
}),
|
|
102
|
+
];
|
|
103
|
+
}),
|
|
104
|
+
);
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
/** Statuses that end a stretch of work on a task. */
|
|
108
|
+
const CLOSED_STATUSES = new Set<Task.Status>(['done', 'review', 'failed', 'cancelled']);
|
|
109
|
+
|
|
110
|
+
const isStatusChange = (detail: unknown): detail is TaskStatusChange =>
|
|
111
|
+
typeof detail === 'object' && detail !== null && 'status' in detail && isStatus(detail.status);
|
|
112
|
+
|
|
35
113
|
const ACTIVE_STATES = new Set<Process.State>([Process.State.RUNNING, Process.State.HYBERNATING]);
|
|
36
114
|
|
|
37
115
|
/** How long an open lane may be silent before the axis stops following `now`. */
|
|
38
116
|
const OPEN_LANE_STALE_MS = 10 * 60_000;
|
|
39
117
|
|
|
118
|
+
/** How far apart a task's recorded status move and the trace event recording the same move may land. */
|
|
119
|
+
const HISTORY_MATCH_MS = 5_000;
|
|
120
|
+
|
|
40
121
|
const TASK_STATUS: Partial<Record<Task.Status, LaneStatus>> = {
|
|
41
122
|
todo: 'pending',
|
|
42
123
|
backlog: 'pending',
|
|
@@ -83,21 +164,19 @@ interface TaskSegment {
|
|
|
83
164
|
}
|
|
84
165
|
|
|
85
166
|
/**
|
|
86
|
-
* Cuts a session's events into
|
|
87
|
-
*
|
|
88
|
-
*
|
|
89
|
-
* open or move the boundary.
|
|
167
|
+
* Cuts a session's events into segments of task work, from the status events its task tools write.
|
|
168
|
+
* Only tasks in `taskIds` — the session's own checklist — take part: an agent is free to move a task
|
|
169
|
+
* belonging to nothing on this chart, and such an event must not open or close anything here.
|
|
90
170
|
*
|
|
91
|
-
*
|
|
92
|
-
*
|
|
93
|
-
*
|
|
94
|
-
* a start: delegation marks every task it hands over
|
|
95
|
-
*
|
|
96
|
-
*
|
|
97
|
-
*
|
|
98
|
-
*
|
|
99
|
-
*
|
|
100
|
-
* rather than overlapping it.
|
|
171
|
+
* An agent may hold several tasks in progress at once, and may put one down and pick it up again as
|
|
172
|
+
* it discovers what depends on what: a `started` event opens a segment for its task and leaves every
|
|
173
|
+
* other open segment alone, and any other status closes that task's open segment. A task can also
|
|
174
|
+
* finish without a start: delegation marks every task it hands over `started` before the agent's
|
|
175
|
+
* first turn, so the run's only event for that task is the one closing it. Such a task gets the
|
|
176
|
+
* stretch since the last boundary, which is where its work happened — but only on the transition out
|
|
177
|
+
* of `started` and only while no other task holds the stretch: a task merely dismissed
|
|
178
|
+
* (`todo` → `blocked`) claims nothing, a second close (`review` → `done`) mints nothing, and a close
|
|
179
|
+
* arriving while other work is open leaves that work alone rather than overlapping it.
|
|
101
180
|
*/
|
|
102
181
|
const buildTaskSegments = (
|
|
103
182
|
events: readonly Trace.FlatEvent[],
|
|
@@ -105,15 +184,8 @@ const buildTaskSegments = (
|
|
|
105
184
|
taskIds: ReadonlySet<string>,
|
|
106
185
|
): TaskSegment[] => {
|
|
107
186
|
const segments: TaskSegment[] = [];
|
|
108
|
-
|
|
187
|
+
const open = new Map<string, TaskSegment>();
|
|
109
188
|
let boundary = sessionStart;
|
|
110
|
-
const close = (segment: TaskSegment, timestamp: number): void => {
|
|
111
|
-
segment.end = timestamp;
|
|
112
|
-
boundary = timestamp;
|
|
113
|
-
if (open === segment) {
|
|
114
|
-
open = undefined;
|
|
115
|
-
}
|
|
116
|
-
};
|
|
117
189
|
|
|
118
190
|
for (const event of events) {
|
|
119
191
|
if (event.type !== Trace.TaskStatusChanged.key) {
|
|
@@ -123,37 +195,37 @@ const buildTaskSegments = (
|
|
|
123
195
|
if (!data || !taskIds.has(data.taskId)) {
|
|
124
196
|
continue;
|
|
125
197
|
}
|
|
198
|
+
const current = open.get(data.taskId);
|
|
126
199
|
if (data.status === 'started') {
|
|
127
|
-
if (
|
|
128
|
-
|
|
129
|
-
}
|
|
130
|
-
if (!open) {
|
|
131
|
-
open = { taskId: data.taskId, laneId: taskLaneId(data.taskId), start: event.timestamp };
|
|
132
|
-
segments.push(open);
|
|
133
|
-
boundary = event.timestamp;
|
|
134
|
-
}
|
|
135
|
-
} else {
|
|
136
|
-
const started = segments.findLast((candidate) => candidate.taskId === data.taskId && candidate.end === undefined);
|
|
137
|
-
if (started) {
|
|
138
|
-
close(started, event.timestamp);
|
|
139
|
-
} else if (data.previousStatus === 'started' && open === undefined) {
|
|
140
|
-
const segment = {
|
|
141
|
-
taskId: data.taskId,
|
|
142
|
-
laneId: taskLaneId(data.taskId),
|
|
143
|
-
start: Math.min(boundary ?? event.timestamp, event.timestamp),
|
|
144
|
-
};
|
|
200
|
+
if (!current) {
|
|
201
|
+
const segment = { taskId: data.taskId, laneId: taskLaneId(data.taskId), start: event.timestamp };
|
|
145
202
|
segments.push(segment);
|
|
146
|
-
|
|
203
|
+
open.set(data.taskId, segment);
|
|
147
204
|
}
|
|
205
|
+
boundary = event.timestamp;
|
|
206
|
+
} else if (current) {
|
|
207
|
+
current.end = event.timestamp;
|
|
208
|
+
open.delete(data.taskId);
|
|
209
|
+
boundary = event.timestamp;
|
|
210
|
+
} else if (data.previousStatus === 'started' && open.size === 0) {
|
|
211
|
+
segments.push({
|
|
212
|
+
taskId: data.taskId,
|
|
213
|
+
laneId: taskLaneId(data.taskId),
|
|
214
|
+
start: Math.min(boundary ?? event.timestamp, event.timestamp),
|
|
215
|
+
end: event.timestamp,
|
|
216
|
+
});
|
|
217
|
+
boundary = event.timestamp;
|
|
148
218
|
}
|
|
149
219
|
}
|
|
150
220
|
return segments;
|
|
151
221
|
};
|
|
152
222
|
|
|
153
223
|
/**
|
|
154
|
-
* The segment an event belongs to.
|
|
155
|
-
*
|
|
156
|
-
*
|
|
224
|
+
* The segment an event belongs to. A status event goes to its own task's segment. Any other event
|
|
225
|
+
* goes to the one segment in progress when it happened; where segments merely meet, the one ending
|
|
226
|
+
* there yields to the one beginning — the newcomer's start is the first thing the agent did on it.
|
|
227
|
+
* With several tasks genuinely in progress at once the event belongs to none of them in particular,
|
|
228
|
+
* so it stays with the session rather than being credited to whichever started last.
|
|
157
229
|
*/
|
|
158
230
|
const segmentFor = (segments: readonly TaskSegment[] | undefined, event: Trace.FlatEvent): TaskSegment | undefined => {
|
|
159
231
|
if (!segments) {
|
|
@@ -165,7 +237,76 @@ const segmentFor = (segments: readonly TaskSegment[] | undefined, event: Trace.F
|
|
|
165
237
|
const data = decode(Trace.TaskStatusChanged.schema, event.data);
|
|
166
238
|
return data && segments.findLast((segment) => segment.taskId === data.taskId && contains(segment));
|
|
167
239
|
}
|
|
168
|
-
|
|
240
|
+
const candidates = segments.filter(contains);
|
|
241
|
+
const inside = candidates.filter((segment) => segment.end === undefined || event.timestamp < segment.end);
|
|
242
|
+
if (inside.length > 1) {
|
|
243
|
+
return undefined;
|
|
244
|
+
}
|
|
245
|
+
return inside[0] ?? candidates.at(-1);
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* The stretch a task's edit history says it was worked: from its first move to `started` to the last
|
|
250
|
+
* move out of it. `end` is absent while the history leaves it `started`; `last` is its last move.
|
|
251
|
+
* Absent when the task was never started.
|
|
252
|
+
*/
|
|
253
|
+
const historySpan = (
|
|
254
|
+
changes: readonly TaskStatusChange[],
|
|
255
|
+
): { start: number; end?: number; last: number } | undefined => {
|
|
256
|
+
let start: number | undefined;
|
|
257
|
+
let end: number | undefined;
|
|
258
|
+
let open = false;
|
|
259
|
+
for (const change of changes) {
|
|
260
|
+
if (change.status === 'started') {
|
|
261
|
+
start ??= change.timestamp;
|
|
262
|
+
open = true;
|
|
263
|
+
} else if (open) {
|
|
264
|
+
end = change.timestamp;
|
|
265
|
+
open = false;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
const last = changes.at(-1)?.timestamp;
|
|
269
|
+
return start === undefined || last === undefined ? undefined : { start, end: open ? undefined : end, last };
|
|
270
|
+
};
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* The stretches a task was put down between two runs of work: from a move out of `started` that did
|
|
274
|
+
* not finish it (a question blocking it, say) to the next move back. One still open is no gap — the
|
|
275
|
+
* lane simply ends there — and nothing before the first start is one either.
|
|
276
|
+
*/
|
|
277
|
+
const workGaps = (changes: readonly TaskStatusChange[]): { start: number; end: number }[] => {
|
|
278
|
+
const gaps: { start: number; end: number }[] = [];
|
|
279
|
+
let started = false;
|
|
280
|
+
let pausedAt: number | undefined;
|
|
281
|
+
for (const change of changes) {
|
|
282
|
+
if (change.status === 'started') {
|
|
283
|
+
if (pausedAt !== undefined && change.timestamp > pausedAt) {
|
|
284
|
+
gaps.push({ start: pausedAt, end: change.timestamp });
|
|
285
|
+
}
|
|
286
|
+
started = true;
|
|
287
|
+
pausedAt = undefined;
|
|
288
|
+
} else if (started && pausedAt === undefined && !CLOSED_STATUSES.has(change.status)) {
|
|
289
|
+
pausedAt = change.timestamp;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
return gaps;
|
|
293
|
+
};
|
|
294
|
+
|
|
295
|
+
/** A question or answer in the task's log; other entries are edits the edit history already holds. */
|
|
296
|
+
const exchangeMarker = (entry: Task.HistoryEntry, id: string, laneId: string): Marker | undefined => {
|
|
297
|
+
const timestamp = Date.parse(entry.date);
|
|
298
|
+
if (Number.isNaN(timestamp)) {
|
|
299
|
+
return undefined;
|
|
300
|
+
}
|
|
301
|
+
const base = { id, laneId, kind: 'task' as const, timestamp, detail: entry };
|
|
302
|
+
switch (entry.event) {
|
|
303
|
+
case 'question':
|
|
304
|
+
return { ...base, label: entry.text, level: 'warn' };
|
|
305
|
+
case 'answer':
|
|
306
|
+
return { ...base, label: `Answered: ${entry.answer}` };
|
|
307
|
+
default:
|
|
308
|
+
return undefined;
|
|
309
|
+
}
|
|
169
310
|
};
|
|
170
311
|
|
|
171
312
|
interface SubAgentSpan {
|
|
@@ -183,11 +324,13 @@ export const buildSessionTimeline = ({
|
|
|
183
324
|
processes = [],
|
|
184
325
|
sessions,
|
|
185
326
|
tasks = [],
|
|
327
|
+
taskStatusChanges: recordedChanges,
|
|
186
328
|
now,
|
|
187
329
|
}: BuildSessionTimelineInput): SessionTimeline => {
|
|
188
330
|
const root = buildSpanTree(traceMessages);
|
|
189
331
|
const spans = flattenSpanTree(root);
|
|
190
332
|
const events = spans.flatMap((span) => span.events).sort((a, b) => a.timestamp - b.timestamp);
|
|
333
|
+
const taskStatusChanges = recordedChanges && refineStatusChanges(recordedChanges, events);
|
|
191
334
|
const requestBegins = events.filter(
|
|
192
335
|
(event): event is Trace.FlatEvent & { meta: { pid: string } } =>
|
|
193
336
|
event.type === Trace.AgentRequestBegin.key && event.meta.pid !== undefined,
|
|
@@ -243,7 +386,7 @@ export const buildSessionTimeline = ({
|
|
|
243
386
|
sources.push(...byKey.values());
|
|
244
387
|
}
|
|
245
388
|
|
|
246
|
-
const processByPid = new Map<string, Process.
|
|
389
|
+
const processByPid = new Map<string, Process.Process>(processes.map((process) => [process.pid, process]));
|
|
247
390
|
const taskById = new Map(tasks.map((task) => [task.id, task]));
|
|
248
391
|
const laneByPid = new Map<string, string>();
|
|
249
392
|
const lanes: MutableLane[] = [];
|
|
@@ -325,12 +468,15 @@ export const buildSessionTimeline = ({
|
|
|
325
468
|
const chatTaskIds = new Set(chatTasks.map((task) => task.id));
|
|
326
469
|
const taskLanes = new Map<string, MutableLane>();
|
|
327
470
|
for (const task of chatTasks) {
|
|
471
|
+
// A sub-task of a task on the same checklist nests under it, so the chart keeps the hierarchy
|
|
472
|
+
// the ledger shows; any other task hangs off the session.
|
|
473
|
+
const parentTask = Task.getParentTask(task);
|
|
328
474
|
const taskLane: MutableLane = {
|
|
329
475
|
id: taskLaneId(task.id),
|
|
330
476
|
kind: 'task',
|
|
331
477
|
label: task.title,
|
|
332
478
|
status: taskLaneStatus(task, tasks),
|
|
333
|
-
parentId: laneId,
|
|
479
|
+
parentId: parentTask && chatTaskIds.has(parentTask.id) ? taskLaneId(parentTask.id) : laneId,
|
|
334
480
|
sessionId: source.session?.id,
|
|
335
481
|
taskId: task.id,
|
|
336
482
|
};
|
|
@@ -371,6 +517,25 @@ export const buildSessionTimeline = ({
|
|
|
371
517
|
taskLane.end = segment.end === undefined ? undefined : Math.max(taskLane.end ?? segment.end, segment.end);
|
|
372
518
|
}
|
|
373
519
|
|
|
520
|
+
// The task's edit history bounds its lane too, so a task worked where the trace does not reach
|
|
521
|
+
// (another device, a pruned feed, a person's edit) still gets a span. The lane stays open only while
|
|
522
|
+
// the task itself is `started`, whatever the history last says.
|
|
523
|
+
for (const task of chatTasks) {
|
|
524
|
+
const taskLane = taskLanes.get(task.id);
|
|
525
|
+
const span = historySpan(taskStatusChanges?.get(task.id) ?? []);
|
|
526
|
+
if (!taskLane || !span) {
|
|
527
|
+
continue;
|
|
528
|
+
}
|
|
529
|
+
const tracedEnd = taskLane.start === undefined ? undefined : taskLane.end;
|
|
530
|
+
taskLane.start = Math.min(taskLane.start ?? span.start, span.start);
|
|
531
|
+
if (span.end === undefined && task.status === 'started') {
|
|
532
|
+
taskLane.end = undefined;
|
|
533
|
+
} else {
|
|
534
|
+
const end = span.end ?? tracedEnd ?? span.last;
|
|
535
|
+
taskLane.end = tracedEnd === undefined ? end : Math.max(tracedEnd, end);
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
|
|
374
539
|
for (const subPid of subAgentPids) {
|
|
375
540
|
const match = subAgentSpans.find((candidate) => candidate.pid === subPid);
|
|
376
541
|
const process = processByPid.get(subPid);
|
|
@@ -410,6 +575,36 @@ export const buildSessionTimeline = ({
|
|
|
410
575
|
}
|
|
411
576
|
}
|
|
412
577
|
|
|
578
|
+
// A task no drawn session works (an external harness's, a person's) is drawn from its edit history
|
|
579
|
+
// alone, as a lane of its own, so the chart still shows it; one never started has no span to draw.
|
|
580
|
+
const lanedTaskIds = new Set(lanes.flatMap((lane) => (lane.taskId === undefined ? [] : [lane.taskId])));
|
|
581
|
+
const unownedLanes = new Map<string, MutableLane>();
|
|
582
|
+
for (const task of tasks) {
|
|
583
|
+
const span = lanedTaskIds.has(task.id) ? undefined : historySpan(taskStatusChanges?.get(task.id) ?? []);
|
|
584
|
+
if (!span) {
|
|
585
|
+
continue;
|
|
586
|
+
}
|
|
587
|
+
unownedLanes.set(task.id, {
|
|
588
|
+
id: taskLaneId(task.id),
|
|
589
|
+
kind: 'task',
|
|
590
|
+
label: task.title,
|
|
591
|
+
status: taskLaneStatus(task, tasks),
|
|
592
|
+
start: span.start,
|
|
593
|
+
end: span.end === undefined && task.status === 'started' ? undefined : (span.end ?? span.last),
|
|
594
|
+
taskId: task.id,
|
|
595
|
+
});
|
|
596
|
+
}
|
|
597
|
+
for (const [taskId, lane] of unownedLanes) {
|
|
598
|
+
const blockedOn = (taskById.get(taskId)?.dependsOn ?? [])
|
|
599
|
+
.map((ref) => Task.refEntityId(ref))
|
|
600
|
+
.filter((id): id is string => id !== undefined && unownedLanes.has(id))
|
|
601
|
+
.map(taskLaneId);
|
|
602
|
+
if (blockedOn.length > 0) {
|
|
603
|
+
lane.blockedOn = blockedOn;
|
|
604
|
+
}
|
|
605
|
+
lanes.push(lane);
|
|
606
|
+
}
|
|
607
|
+
|
|
413
608
|
// A delegated task IS its child session, drawn with the child's own span; the parent's markers in
|
|
414
609
|
// that stretch are the parent's work of handing it over, so the segment goes rather than moving.
|
|
415
610
|
for (const [sessionId, segments] of segmentsBySession) {
|
|
@@ -419,6 +614,67 @@ export const buildSessionTimeline = ({
|
|
|
419
614
|
);
|
|
420
615
|
}
|
|
421
616
|
|
|
617
|
+
// Each status move in a task's edit history is a node on its lane, and so is each question and answer
|
|
618
|
+
// in its log. A move the trace also recorded is drawn once, from the history, taking the trace event's
|
|
619
|
+
// pid. Only a trace event on a drawn lane is matched, so none is hidden that had a node.
|
|
620
|
+
const tracedChanges = new Map<string, Trace.FlatEvent[]>();
|
|
621
|
+
for (const event of events) {
|
|
622
|
+
const drawn =
|
|
623
|
+
(event.meta.pid !== undefined && laneByPid.has(event.meta.pid)) ||
|
|
624
|
+
(event.meta.parentPid !== undefined && laneByPid.has(event.meta.parentPid));
|
|
625
|
+
if (drawn && event.type === Trace.TaskStatusChanged.key) {
|
|
626
|
+
const data = decode(Trace.TaskStatusChanged.schema, event.data);
|
|
627
|
+
if (data) {
|
|
628
|
+
tracedChanges.set(data.taskId, [...(tracedChanges.get(data.taskId) ?? []), event]);
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
const mergedEvents = new Set<Trace.FlatEvent>();
|
|
633
|
+
// A delegated task's lane is its child session, which carries the task id, so it gets the nodes too.
|
|
634
|
+
for (const lane of lanes) {
|
|
635
|
+
const task = lane.taskId === undefined ? undefined : taskById.get(lane.taskId);
|
|
636
|
+
if (!task) {
|
|
637
|
+
continue;
|
|
638
|
+
}
|
|
639
|
+
const traced = tracedChanges.get(task.id) ?? [];
|
|
640
|
+
const changes = taskStatusChanges?.get(task.id) ?? [];
|
|
641
|
+
// Waiting to be picked up is the lane's absence, not an event on it: a node for it would sit
|
|
642
|
+
// alone before the bar, reading as work that happened and then a pause.
|
|
643
|
+
const firstStart = changes.findIndex((change) => change.status === 'started');
|
|
644
|
+
for (const [index, change] of changes.entries()) {
|
|
645
|
+
if (PENDING_STATUSES.has(change.status) && (firstStart < 0 || index < firstStart)) {
|
|
646
|
+
continue;
|
|
647
|
+
}
|
|
648
|
+
const match = traced.find(
|
|
649
|
+
(event) =>
|
|
650
|
+
!mergedEvents.has(event) &&
|
|
651
|
+
decode(Trace.TaskStatusChanged.schema, event.data)?.status === change.status &&
|
|
652
|
+
Math.abs(event.timestamp - change.timestamp) <= HISTORY_MATCH_MS,
|
|
653
|
+
);
|
|
654
|
+
if (match) {
|
|
655
|
+
mergedEvents.add(match);
|
|
656
|
+
}
|
|
657
|
+
markers.push({
|
|
658
|
+
id: `${lane.id}:${markers.length}`,
|
|
659
|
+
laneId: lane.id,
|
|
660
|
+
kind: 'task',
|
|
661
|
+
// The trace's instant when it recorded the same move: edit-history times are whole seconds, so
|
|
662
|
+
// on their own they land before the work they mark.
|
|
663
|
+
timestamp: match?.timestamp ?? change.timestamp,
|
|
664
|
+
label: `Task ${change.status}`,
|
|
665
|
+
level: change.status === 'failed' ? 'error' : undefined,
|
|
666
|
+
pid: match?.meta.pid,
|
|
667
|
+
detail: change,
|
|
668
|
+
});
|
|
669
|
+
}
|
|
670
|
+
for (const entry of task.history ?? []) {
|
|
671
|
+
const marker = exchangeMarker(entry, `${lane.id}:${markers.length}`, lane.id);
|
|
672
|
+
if (marker) {
|
|
673
|
+
markers.push(marker);
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
|
|
422
678
|
// Dependencies named the task lane; they follow it to the session that replaced it.
|
|
423
679
|
for (const lane of lanes) {
|
|
424
680
|
if (lane.blockedOn) {
|
|
@@ -428,7 +684,11 @@ export const buildSessionTimeline = ({
|
|
|
428
684
|
|
|
429
685
|
// Markers and token totals, attributed to the lane owning the event's pid or its parent pid.
|
|
430
686
|
const tokens = new Map<string, { usage: TokenUsage; toolCalls: number }>();
|
|
431
|
-
|
|
687
|
+
// The marker's own lane travels with its id: `markerLaneId` routes an event to whichever task
|
|
688
|
+
// segment was open at the time, so a delegation node does not always land on the session's lane —
|
|
689
|
+
// and a connector drawn to a row that does not hold the node points at nothing.
|
|
690
|
+
const spawnMarkerByPid = new Map<string, DelegationSource>();
|
|
691
|
+
const returnMarkerByPid = new Map<string, DelegationSource>();
|
|
432
692
|
for (const event of events) {
|
|
433
693
|
const laneId =
|
|
434
694
|
(event.meta.pid && laneByPid.get(event.meta.pid)) ??
|
|
@@ -443,13 +703,21 @@ export const buildSessionTimeline = ({
|
|
|
443
703
|
event.type === Trace.AgentRequestBegin.key || event.type === Trace.AgentRequestEnd.key
|
|
444
704
|
? laneId
|
|
445
705
|
: (segmentFor(segmentsBySession.get(laneId), event)?.laneId ?? laneId);
|
|
446
|
-
const marker =
|
|
706
|
+
const marker = mergedEvents.has(event)
|
|
707
|
+
? undefined
|
|
708
|
+
: toMarker(event, `${markerLaneId}:${markers.length}`, markerLaneId);
|
|
447
709
|
if (marker) {
|
|
448
710
|
markers.push(marker);
|
|
449
|
-
if (
|
|
711
|
+
if (event.type === Trace.DelegationSpawned.key) {
|
|
450
712
|
const data = decode(Trace.DelegationSpawned.schema, event.data);
|
|
451
713
|
if (data) {
|
|
452
|
-
spawnMarkerByPid.set(data.pid, marker.id);
|
|
714
|
+
spawnMarkerByPid.set(data.pid, { laneId: marker.laneId, markerId: marker.id });
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
if (event.type === Trace.DelegationCompleted.key) {
|
|
718
|
+
const data = decode(Trace.DelegationCompleted.schema, event.data);
|
|
719
|
+
if (data) {
|
|
720
|
+
returnMarkerByPid.set(data.pid, { laneId: marker.laneId, markerId: marker.id });
|
|
453
721
|
}
|
|
454
722
|
}
|
|
455
723
|
}
|
|
@@ -481,26 +749,45 @@ export const buildSessionTimeline = ({
|
|
|
481
749
|
|
|
482
750
|
// The connector starts at the spawn marker, else at the supervisor's last node before the child began.
|
|
483
751
|
for (const { lane, sessionLaneId } of childSessions) {
|
|
484
|
-
const
|
|
485
|
-
(
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
lane.
|
|
752
|
+
const fallbackId = markers
|
|
753
|
+
.filter((marker) => marker.laneId === sessionLaneId && marker.timestamp <= (lane.start ?? 0))
|
|
754
|
+
.at(-1)?.id;
|
|
755
|
+
const spawn =
|
|
756
|
+
(lane.pid === undefined ? undefined : spawnMarkerByPid.get(lane.pid)) ??
|
|
757
|
+
(fallbackId === undefined ? undefined : { laneId: sessionLaneId, markerId: fallbackId });
|
|
758
|
+
if (spawn) {
|
|
759
|
+
lane.delegatedFrom = spawn;
|
|
760
|
+
}
|
|
761
|
+
// The return connector has no such fallback: only the completion event says the child reported
|
|
762
|
+
// back, and guessing from the supervisor's next node would invent a causal edge that may not
|
|
763
|
+
// exist — a child can end without ever answering.
|
|
764
|
+
const returned = lane.pid === undefined ? undefined : returnMarkerByPid.get(lane.pid);
|
|
765
|
+
if (returned) {
|
|
766
|
+
lane.returnedTo = returned;
|
|
489
767
|
}
|
|
490
768
|
}
|
|
491
769
|
|
|
492
770
|
// A session working exactly one task is that task: the two lanes would carry the same label,
|
|
493
771
|
// so the task lane folds into the session's (which keeps its run span and totals, and takes the
|
|
494
772
|
// task's id and status so it still reads as the work). Only a lone in-session task, with nothing
|
|
495
|
-
// depending on it and no spawned child, folds — a checklist of several stays a tree.
|
|
773
|
+
// depending on it, no sub-tasks and no spawned child, folds — a checklist of several stays a tree.
|
|
496
774
|
for (const session of lanes.filter((lane) => lane.kind === 'session' && lane.parentId === undefined)) {
|
|
497
775
|
const children = lanes.filter((lane) => lane.parentId === session.id);
|
|
498
776
|
const [task] = children;
|
|
499
|
-
if (
|
|
777
|
+
if (
|
|
778
|
+
children.length !== 1 ||
|
|
779
|
+
task.kind !== 'task' ||
|
|
780
|
+
lanes.some((lane) => lane.parentId === task.id || lane.blockedOn?.includes(task.id))
|
|
781
|
+
) {
|
|
500
782
|
continue;
|
|
501
783
|
}
|
|
502
784
|
lanes.splice(lanes.indexOf(task), 1);
|
|
503
785
|
session.taskId = task.taskId;
|
|
786
|
+
// The task was started (delegated) before the session's first request, so the folded lane begins
|
|
787
|
+
// with the task; otherwise its first node would sit alone ahead of the bar.
|
|
788
|
+
if (task.start !== undefined) {
|
|
789
|
+
session.start = session.start === undefined ? task.start : Math.min(session.start, task.start);
|
|
790
|
+
}
|
|
504
791
|
if (task.status === 'blocked' || task.status === 'review' || task.status === 'pending') {
|
|
505
792
|
session.status = task.status;
|
|
506
793
|
}
|
|
@@ -511,6 +798,80 @@ export const buildSessionTimeline = ({
|
|
|
511
798
|
});
|
|
512
799
|
}
|
|
513
800
|
|
|
801
|
+
// A parent task's lane carries each of its sub-tasks' starts and finishes as nodes of its own, so it reads as the
|
|
802
|
+
// span of the work it contains rather than one unbroken bar, and its first node is where that work began. Read from
|
|
803
|
+
// the task tree rather than the lanes' `parentId`, which joins a sub-task to its parent only when both sit on one
|
|
804
|
+
// checklist, and carried to every ancestor, since a grandchild's work is inside its grandparent too.
|
|
805
|
+
const laneById = new Map(lanes.map((lane) => [lane.id, lane]));
|
|
806
|
+
const laneByTaskId = new Map(lanes.flatMap((lane) => (lane.taskId === undefined ? [] : [[lane.taskId, lane]])));
|
|
807
|
+
const ancestorLanesOf = (lane: Lane): Lane[] => {
|
|
808
|
+
const task = lane.taskId === undefined ? undefined : taskById.get(lane.taskId);
|
|
809
|
+
const ancestors: Lane[] = [];
|
|
810
|
+
const seen = new Set<string>(task ? [task.id] : []);
|
|
811
|
+
for (
|
|
812
|
+
let parent = task && Task.getParentTask(task);
|
|
813
|
+
parent && !seen.has(parent.id);
|
|
814
|
+
parent = Task.getParentTask(parent)
|
|
815
|
+
) {
|
|
816
|
+
seen.add(parent.id);
|
|
817
|
+
const parentLane = laneByTaskId.get(parent.id);
|
|
818
|
+
if (parentLane?.kind === 'task') {
|
|
819
|
+
ancestors.push(parentLane);
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
return ancestors;
|
|
823
|
+
};
|
|
824
|
+
for (const marker of [...markers]) {
|
|
825
|
+
const lane = laneById.get(marker.laneId);
|
|
826
|
+
if (lane?.kind !== 'task' || !isStatusChange(marker.detail)) {
|
|
827
|
+
continue;
|
|
828
|
+
}
|
|
829
|
+
const { status } = marker.detail;
|
|
830
|
+
if (status !== 'started' && !CLOSED_STATUSES.has(status)) {
|
|
831
|
+
continue;
|
|
832
|
+
}
|
|
833
|
+
for (const ancestor of ancestorLanesOf(lane)) {
|
|
834
|
+
markers.push({
|
|
835
|
+
...marker,
|
|
836
|
+
id: `${ancestor.id}:sub:${marker.id}`,
|
|
837
|
+
laneId: ancestor.id,
|
|
838
|
+
label: `${lane.label}: ${status}`,
|
|
839
|
+
});
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
// A parent task encloses its sub-tasks, so its bar begins no later than the earliest of theirs. Read
|
|
844
|
+
// from the task tree rather than the lanes' `parentId`, which joins a sub-task to its parent only when
|
|
845
|
+
// both sit on one checklist: a parent drawn from its edit history alone, or a sub-task worked by
|
|
846
|
+
// another session, would otherwise begin at its own `started` move, after work it contains.
|
|
847
|
+
for (const lane of lanes) {
|
|
848
|
+
const task = lane.taskId === undefined ? undefined : taskById.get(lane.taskId);
|
|
849
|
+
if (!task || lane.start === undefined) {
|
|
850
|
+
continue;
|
|
851
|
+
}
|
|
852
|
+
const seen = new Set<string>([task.id]);
|
|
853
|
+
for (let parent = Task.getParentTask(task); parent && !seen.has(parent.id); parent = Task.getParentTask(parent)) {
|
|
854
|
+
seen.add(parent.id);
|
|
855
|
+
const parentLane = laneByTaskId.get(parent.id);
|
|
856
|
+
if (parentLane?.start !== undefined && lane.start < parentLane.start) {
|
|
857
|
+
parentLane.start = lane.start;
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
// Hashed from the mnemonic, as the task's mnemonic chip is, so a lane and its chip share a hue.
|
|
863
|
+
for (const lane of lanes) {
|
|
864
|
+
const task = lane.taskId === undefined ? undefined : taskById.get(lane.taskId);
|
|
865
|
+
if (task) {
|
|
866
|
+
lane.hue = getHashHue(Obj.getMnemonic(task));
|
|
867
|
+
// After the fold, so a session standing for its one task breaks where the task was put down too.
|
|
868
|
+
const gaps = workGaps(taskStatusChanges?.get(task.id) ?? []);
|
|
869
|
+
if (gaps.length > 0) {
|
|
870
|
+
lane.gaps = gaps;
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
|
|
514
875
|
const times = [
|
|
515
876
|
...lanes.flatMap((lane) => [lane.start, lane.end]).filter((time): time is number => time !== undefined),
|
|
516
877
|
...markers.map((marker) => marker.timestamp),
|
|
@@ -535,7 +896,7 @@ const taskLaneStatus = (task: Task.Task, tasks: readonly Task.Task[]): LaneStatu
|
|
|
535
896
|
return (task.status && TASK_STATUS[task.status]) ?? 'pending';
|
|
536
897
|
};
|
|
537
898
|
|
|
538
|
-
const subSessionStatus = (endEvent: Trace.FlatEvent | undefined, process: Process.
|
|
899
|
+
const subSessionStatus = (endEvent: Trace.FlatEvent | undefined, process: Process.Process | undefined): LaneStatus => {
|
|
539
900
|
if (endEvent) {
|
|
540
901
|
return decode(Trace.OperationEnd.schema, endEvent.data)?.outcome === 'failure' ? 'failed' : 'done';
|
|
541
902
|
}
|
|
@@ -574,6 +935,16 @@ const toMarker = (event: Trace.FlatEvent, id: string, laneId: string): Marker |
|
|
|
574
935
|
const data = decode(Trace.DelegationSpawned.schema, event.data);
|
|
575
936
|
return { ...base, kind: 'delegation', label: 'Delegated', detail: data };
|
|
576
937
|
}
|
|
938
|
+
case Trace.DelegationCompleted.key: {
|
|
939
|
+
const data = decode(Trace.DelegationCompleted.schema, event.data);
|
|
940
|
+
return {
|
|
941
|
+
...base,
|
|
942
|
+
kind: 'delegation',
|
|
943
|
+
label: data?.status === 'failure' ? 'Sub-agent failed' : `Returned${data?.result ? `: ${data.result}` : ''}`,
|
|
944
|
+
level: data?.status === 'failure' ? 'error' : undefined,
|
|
945
|
+
detail: data,
|
|
946
|
+
};
|
|
947
|
+
}
|
|
577
948
|
case Trace.OperationStart.key: {
|
|
578
949
|
const data = decode(Trace.OperationStart.schema, event.data);
|
|
579
950
|
return { ...base, kind: 'operation', label: data?.name ?? data?.key ?? 'Operation' };
|