@pasko70/pibo 1.8.2 → 1.9.2
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/apps/chat/data/chat-data-mappers.js +8 -0
- package/dist/apps/chat/data/project-service.js +23 -0
- package/dist/apps/chat/static-assets.js +6 -5
- package/dist/apps/chat-ui/assets/{dist--lraqdDn.js → dist-2KdPXbMT.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-Dgz3iWay.js → dist-5GM30SQK.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-CMFjl7MX.js → dist-9lsp1UpA.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-6pcjNbPQ.js → dist-BWbWIOcD.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-COlFTw2x.js → dist-BYKMZlI0.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-IlQbTNzw.js → dist-C-5u2QIS.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-bLd-NaVv.js → dist-Cge8JklW.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-CoCvyd4f.js → dist-CjYtD7ZT.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-CZSEM5So.js → dist-DFhhiR8M.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-YdEosvsr.js → dist-Etxmpyxg.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-CajLLpaw.js → dist-Idz5kzy8.js} +1 -1
- package/dist/apps/chat-ui/assets/index-BqD_bm7z.js +173 -0
- package/dist/apps/chat-ui/assets/index-C0x9nEcf.css +1 -0
- package/dist/apps/chat-ui/index.html +2 -2
- package/dist/apps/chat-vscode-web/assets/index-Cst9OUkC.js +41 -0
- package/dist/apps/chat-vscode-web/index.html +1 -1
- package/dist/cli.js +17 -0
- package/dist/compute/resource-health.js +12 -11
- package/dist/gateway/server.js +14 -1
- package/dist/gateway/web.js +2 -1
- package/dist/plugins/context-files.js +4 -2
- package/dist/ralph/store.js +1 -1
- package/dist/resources/cli.js +135 -0
- package/dist/resources/lifecycle.js +353 -0
- package/dist/resources/reaper-state.js +94 -0
- package/dist/resources/reaper.js +145 -0
- package/dist/session-ui/activeTurn.js +126 -0
- package/dist/session-ui/delegation.js +89 -0
- package/dist/session-ui/index.js +1 -0
- package/dist/session-ui/terminalRows.js +61 -0
- package/dist/shared/trace-engine.js +3 -2
- package/dist/shared/trace-event-projection.js +124 -14
- package/dist/shared/trace-transcript.js +73 -5
- package/package.json +1 -1
- package/dist/apps/chat-ui/assets/index-DbRZGRDd.css +0 -1
- package/dist/apps/chat-ui/assets/index-HPWlrJwv.js +0 -173
- package/dist/apps/chat-vscode-web/assets/index-Dge6XEYB.js +0 -41
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
export const EMPTY_STABLE_ACTIVE_TURN = { active: false };
|
|
2
|
+
export function resolveStableActiveTurn(previous, observation) {
|
|
3
|
+
const sessionId = observation.sessionId ?? previous.sessionId;
|
|
4
|
+
const sameSession = !previous.sessionId || !sessionId || previous.sessionId === sessionId;
|
|
5
|
+
const current = sameSession ? previous : { active: false, sessionId };
|
|
6
|
+
const terminalKey = observation.terminal?.key;
|
|
7
|
+
const observedStartedAt = validTimestamp(observation.startedAt) ? observation.startedAt : undefined;
|
|
8
|
+
if (observedStartedAt && terminalClosesTurn(observation.terminal, observedStartedAt)) {
|
|
9
|
+
return inactiveState(current, sessionId, terminalKey);
|
|
10
|
+
}
|
|
11
|
+
if (observedStartedAt) {
|
|
12
|
+
if (current.active &&
|
|
13
|
+
current.sessionId === sessionId &&
|
|
14
|
+
current.startedAt === observedStartedAt &&
|
|
15
|
+
current.terminalBaselineKey === terminalKey)
|
|
16
|
+
return current;
|
|
17
|
+
return {
|
|
18
|
+
sessionId,
|
|
19
|
+
active: true,
|
|
20
|
+
startedAt: observedStartedAt,
|
|
21
|
+
terminalBaselineKey: terminalKey,
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
if (current.active) {
|
|
25
|
+
if (terminalKey && terminalKey !== current.terminalBaselineKey) {
|
|
26
|
+
return inactiveState(current, sessionId, terminalKey);
|
|
27
|
+
}
|
|
28
|
+
return current;
|
|
29
|
+
}
|
|
30
|
+
if (observation.activeEvidence && terminalKey !== current.endedByTerminalKey) {
|
|
31
|
+
return {
|
|
32
|
+
sessionId,
|
|
33
|
+
active: true,
|
|
34
|
+
terminalBaselineKey: terminalKey,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
if (current.sessionId === sessionId)
|
|
38
|
+
return current;
|
|
39
|
+
return { ...current, sessionId };
|
|
40
|
+
}
|
|
41
|
+
export function findLatestActiveTurnTerminal(traceView) {
|
|
42
|
+
if (!traceView)
|
|
43
|
+
return undefined;
|
|
44
|
+
let latest;
|
|
45
|
+
for (const event of traceView.rawEvents) {
|
|
46
|
+
const payload = record(event.payload);
|
|
47
|
+
const type = typeof payload?.type === "string" ? payload.type : event.type;
|
|
48
|
+
const isTerminal = type === "message_finished" || type === "session_error" ||
|
|
49
|
+
(type === "execution_result" && isTurnStoppingAction(payload?.action));
|
|
50
|
+
if (!isTerminal)
|
|
51
|
+
continue;
|
|
52
|
+
latest = latestActiveTurnTerminal(latest, {
|
|
53
|
+
key: `${type}:${event.eventSequence ?? event.streamId ?? event.id}`,
|
|
54
|
+
at: event.createdAt,
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
for (const node of flattenTraceNodes(traceView.nodes)) {
|
|
58
|
+
const terminalAt = node.type === "agent.turn" ? node.completedAt : node.type === "error" ? node.completedAt ?? node.startedAt : undefined;
|
|
59
|
+
if (!terminalAt)
|
|
60
|
+
continue;
|
|
61
|
+
latest = latestActiveTurnTerminal(latest, { key: `node:${node.id}:${node.status}`, at: terminalAt });
|
|
62
|
+
}
|
|
63
|
+
return latest;
|
|
64
|
+
}
|
|
65
|
+
export function findSignalActiveTurnStartedAt(sessionSignal, signalTree) {
|
|
66
|
+
if (!sessionSignal?.isTreeActive || !signalTree)
|
|
67
|
+
return undefined;
|
|
68
|
+
const currentTurn = sessionSignal.currentTurnId ? signalTree.nodes[sessionSignal.currentTurnId] : undefined;
|
|
69
|
+
if (currentTurn?.startedAt)
|
|
70
|
+
return currentTurn.startedAt;
|
|
71
|
+
return Object.values(signalTree.nodes)
|
|
72
|
+
.filter((node) => node.piboSessionId === sessionSignal.piboSessionId && node.kind === "turn" && isActiveSignalStatus(node.status) && node.startedAt)
|
|
73
|
+
.sort((left, right) => Date.parse(left.startedAt ?? left.createdAt) - Date.parse(right.startedAt ?? right.createdAt))
|
|
74
|
+
.at(-1)?.startedAt;
|
|
75
|
+
}
|
|
76
|
+
export function findSignalActiveTurnTerminal(sessionSignal) {
|
|
77
|
+
if (!sessionSignal || sessionSignal.isTreeActive)
|
|
78
|
+
return undefined;
|
|
79
|
+
const status = [sessionSignal.localStatus, sessionSignal.aggregateStatus].find(isTerminalSignalStatus);
|
|
80
|
+
return status ? { key: `signal:${status}:${sessionSignal.updatedAt}`, at: sessionSignal.updatedAt } : undefined;
|
|
81
|
+
}
|
|
82
|
+
export function latestActiveTurnTerminal(left, right) {
|
|
83
|
+
if (!left)
|
|
84
|
+
return right;
|
|
85
|
+
if (!right)
|
|
86
|
+
return left;
|
|
87
|
+
return Date.parse(right.at) >= Date.parse(left.at) ? right : left;
|
|
88
|
+
}
|
|
89
|
+
function flattenTraceNodes(nodes) {
|
|
90
|
+
return nodes.flatMap((node) => [node, ...flattenTraceNodes(node.children)]);
|
|
91
|
+
}
|
|
92
|
+
function inactiveState(current, sessionId, terminalKey) {
|
|
93
|
+
if (!current.active &&
|
|
94
|
+
current.sessionId === sessionId &&
|
|
95
|
+
current.endedByTerminalKey === terminalKey)
|
|
96
|
+
return current;
|
|
97
|
+
return {
|
|
98
|
+
sessionId,
|
|
99
|
+
active: false,
|
|
100
|
+
endedByTerminalKey: terminalKey,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
function terminalClosesTurn(terminal, startedAt) {
|
|
104
|
+
if (!terminal)
|
|
105
|
+
return false;
|
|
106
|
+
const terminalMs = Date.parse(terminal.at);
|
|
107
|
+
const startedMs = Date.parse(startedAt);
|
|
108
|
+
return Number.isFinite(terminalMs) && Number.isFinite(startedMs) && terminalMs >= startedMs;
|
|
109
|
+
}
|
|
110
|
+
function isTurnStoppingAction(value) {
|
|
111
|
+
return value === "abort" || value === "kill" || value === "kill_all" || value === "dispose";
|
|
112
|
+
}
|
|
113
|
+
function isActiveSignalStatus(status) {
|
|
114
|
+
return status === "queued" || status === "starting" || status === "running" || status === "streaming" || status === "compacting" || status === "blocked" || status === "paused";
|
|
115
|
+
}
|
|
116
|
+
function isTerminalSignalStatus(status) {
|
|
117
|
+
return status === "error" || status === "failed" || status === "cancelled" || status === "interrupted" || status === "disposed";
|
|
118
|
+
}
|
|
119
|
+
function validTimestamp(value) {
|
|
120
|
+
return Boolean(value && Number.isFinite(Date.parse(value)));
|
|
121
|
+
}
|
|
122
|
+
function record(value) {
|
|
123
|
+
return value !== null && typeof value === "object" && !Array.isArray(value)
|
|
124
|
+
? value
|
|
125
|
+
: undefined;
|
|
126
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
export function resolveAgentDelegationStatus(childSignal, fallbackStatus = "done", traceTerminal = false) {
|
|
2
|
+
if (!childSignal)
|
|
3
|
+
return fallbackStatus === "running" ? "running" : fallbackStatus === "error" ? "failed" : "completed";
|
|
4
|
+
if (traceTerminal)
|
|
5
|
+
return fallbackStatus === "error" ? "failed" : "completed";
|
|
6
|
+
const statuses = [childSignal.localStatus, childSignal.aggregateStatus];
|
|
7
|
+
if (statuses.some(isCancelledSignalStatus))
|
|
8
|
+
return "cancelled";
|
|
9
|
+
if (childSignal.hasError || childSignal.hasErrorDescendant || statuses.some(isFailedSignalStatus))
|
|
10
|
+
return "failed";
|
|
11
|
+
if (childSignal.isSettled === false || childSignal.isTreeActive || childSignal.isLocalActive || childSignal.hasActiveDescendant || statuses.some(isRunningSignalStatus))
|
|
12
|
+
return "running";
|
|
13
|
+
if (fallbackStatus === "running" && statuses.every(isIdleSignalStatus))
|
|
14
|
+
return "running";
|
|
15
|
+
return "completed";
|
|
16
|
+
}
|
|
17
|
+
export function extractAgentDelegationName(input, title, summary) {
|
|
18
|
+
const record = isRecord(input) ? input : undefined;
|
|
19
|
+
const rawName = stringValue(record?.subagentName)
|
|
20
|
+
?? subagentNameFromToolName(title)
|
|
21
|
+
?? stringValue(summary)
|
|
22
|
+
?? stringValue(title)
|
|
23
|
+
?? "Subagent";
|
|
24
|
+
return titleCaseAgentName(rawName);
|
|
25
|
+
}
|
|
26
|
+
export function extractAgentDelegationTask(input) {
|
|
27
|
+
if (typeof input === "string")
|
|
28
|
+
return nonEmpty(input);
|
|
29
|
+
if (!isRecord(input))
|
|
30
|
+
return undefined;
|
|
31
|
+
for (const key of ["message", "task", "prompt", "query"]) {
|
|
32
|
+
const value = nonEmpty(input[key]);
|
|
33
|
+
if (value)
|
|
34
|
+
return value;
|
|
35
|
+
}
|
|
36
|
+
return extractAgentDelegationTask(input.arguments);
|
|
37
|
+
}
|
|
38
|
+
export function compactAgentDelegationTask(input, maxLength = 180) {
|
|
39
|
+
const task = extractAgentDelegationTask(input)?.replace(/\s+/g, " ").trim();
|
|
40
|
+
if (!task || maxLength < 1)
|
|
41
|
+
return undefined;
|
|
42
|
+
return task.length <= maxLength ? task : `${task.slice(0, Math.max(1, maxLength - 1)).trimEnd()}…`;
|
|
43
|
+
}
|
|
44
|
+
export function formatAgentDelegationDuration(durationMs) {
|
|
45
|
+
const totalSeconds = Math.max(0, Math.floor(durationMs / 1000));
|
|
46
|
+
if (totalSeconds < 60)
|
|
47
|
+
return `${totalSeconds}s`;
|
|
48
|
+
const hours = Math.floor(totalSeconds / 3600);
|
|
49
|
+
const minutes = Math.floor((totalSeconds % 3600) / 60);
|
|
50
|
+
const seconds = totalSeconds % 60;
|
|
51
|
+
if (hours > 0)
|
|
52
|
+
return `${hours}h ${minutes}m ${seconds}s`;
|
|
53
|
+
return `${minutes}m ${seconds}s`;
|
|
54
|
+
}
|
|
55
|
+
function isRunningSignalStatus(status) {
|
|
56
|
+
return ["queued", "starting", "running", "streaming", "waiting", "blocked", "retrying", "compacting", "pausing", "paused"].includes(status);
|
|
57
|
+
}
|
|
58
|
+
function isIdleSignalStatus(status) {
|
|
59
|
+
return status === "idle" || status === "unknown";
|
|
60
|
+
}
|
|
61
|
+
function isFailedSignalStatus(status) {
|
|
62
|
+
return status === "error";
|
|
63
|
+
}
|
|
64
|
+
function isCancelledSignalStatus(status) {
|
|
65
|
+
return status === "cancelled" || status === "interrupted" || status === "disposed";
|
|
66
|
+
}
|
|
67
|
+
function subagentNameFromToolName(value) {
|
|
68
|
+
const name = nonEmpty(value);
|
|
69
|
+
if (!name)
|
|
70
|
+
return undefined;
|
|
71
|
+
return name.replace(/^pibo_subagent_/, "");
|
|
72
|
+
}
|
|
73
|
+
function titleCaseAgentName(value) {
|
|
74
|
+
return value
|
|
75
|
+
.replace(/^pibo_subagent_/, "")
|
|
76
|
+
.split(/[\s_-]+/)
|
|
77
|
+
.filter(Boolean)
|
|
78
|
+
.map((part) => `${part.charAt(0).toUpperCase()}${part.slice(1)}`)
|
|
79
|
+
.join(" ") || "Subagent";
|
|
80
|
+
}
|
|
81
|
+
function nonEmpty(value) {
|
|
82
|
+
return typeof value === "string" && value.trim() ? value.trim() : undefined;
|
|
83
|
+
}
|
|
84
|
+
function stringValue(value) {
|
|
85
|
+
return nonEmpty(value);
|
|
86
|
+
}
|
|
87
|
+
function isRecord(value) {
|
|
88
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
89
|
+
}
|
package/dist/session-ui/index.js
CHANGED
|
@@ -5,12 +5,43 @@ export const COMPACT_TERMINAL_EXPLORING_PREVIEW_LINES = 6;
|
|
|
5
5
|
export function buildCompactTerminalRows(traceView, options) {
|
|
6
6
|
if (!traceView)
|
|
7
7
|
return [];
|
|
8
|
+
const turnById = mapTurnNodes(traceView.nodes);
|
|
8
9
|
const flatNodes = flattenTraceNodes(traceView.nodes)
|
|
9
10
|
.sort((left, right) => compareTraceNodes(left.node, right.node))
|
|
10
11
|
.filter((item) => item.node.type !== "agent.turn" && (options.showThinking || item.node.type !== "model.reasoning"));
|
|
11
12
|
const candidates = syncThinkingToolRows(flatNodes.map((item) => createRowCandidate(item.node, item.turnId)));
|
|
13
|
+
applyCompletedTurnTiming(candidates, turnById);
|
|
12
14
|
return groupRelatedToolCandidates(candidates).map((candidate) => candidate.row);
|
|
13
15
|
}
|
|
16
|
+
export function findActiveTurnStartedAt(traceView) {
|
|
17
|
+
if (!traceView)
|
|
18
|
+
return undefined;
|
|
19
|
+
const terminalErrorEventIds = new Set(flattenTraceNodes(traceView.nodes)
|
|
20
|
+
.map(({ node }) => node)
|
|
21
|
+
.filter((node) => node.type === "error" && node.eventId)
|
|
22
|
+
.map((node) => node.eventId));
|
|
23
|
+
return [...mapTurnNodes(traceView.nodes).values()]
|
|
24
|
+
.filter((turn) => turn.startedAt && !turn.completedAt && (!turn.eventId || !terminalErrorEventIds.has(turn.eventId)))
|
|
25
|
+
.sort(compareTraceNodes)
|
|
26
|
+
.at(-1)?.startedAt;
|
|
27
|
+
}
|
|
28
|
+
export function formatTerminalDuration(durationMs) {
|
|
29
|
+
const totalSeconds = Math.max(0, Math.floor(durationMs / 1000));
|
|
30
|
+
const hours = Math.floor(totalSeconds / 3600);
|
|
31
|
+
const minutes = Math.floor((totalSeconds % 3600) / 60);
|
|
32
|
+
const seconds = totalSeconds % 60;
|
|
33
|
+
return [hours, minutes, seconds].map((value) => String(value).padStart(2, "0")).join(":");
|
|
34
|
+
}
|
|
35
|
+
function mapTurnNodes(nodes) {
|
|
36
|
+
const turns = new Map();
|
|
37
|
+
for (const node of nodes) {
|
|
38
|
+
if (node.type === "agent.turn")
|
|
39
|
+
turns.set(node.id, node);
|
|
40
|
+
for (const [id, turn] of mapTurnNodes(node.children))
|
|
41
|
+
turns.set(id, turn);
|
|
42
|
+
}
|
|
43
|
+
return turns;
|
|
44
|
+
}
|
|
14
45
|
function flattenTraceNodes(nodes, turnId) {
|
|
15
46
|
const result = [];
|
|
16
47
|
for (const node of nodes) {
|
|
@@ -71,6 +102,26 @@ function createRowCandidate(node, turnId) {
|
|
|
71
102
|
}
|
|
72
103
|
return { ...candidate, row: { ...candidate.row, ...debugFields(node) } };
|
|
73
104
|
}
|
|
105
|
+
function applyCompletedTurnTiming(candidates, turnById) {
|
|
106
|
+
for (const turn of turnById.values()) {
|
|
107
|
+
if (!turn.completedAt)
|
|
108
|
+
continue;
|
|
109
|
+
const turnCandidates = candidates.filter((candidate) => candidate.turnId === turn.id);
|
|
110
|
+
const finalCandidate = turnCandidates.at(-1);
|
|
111
|
+
if (finalCandidate?.row.kind !== "message.assistant" || finalCandidate.row.status === "running")
|
|
112
|
+
continue;
|
|
113
|
+
finalCandidate.row.startedAt = turn.startedAt;
|
|
114
|
+
finalCandidate.row.completedAt = turn.completedAt;
|
|
115
|
+
finalCandidate.row.durationMs = turn.durationMs ?? durationBetween(turn.startedAt, turn.completedAt);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
function durationBetween(startedAt, completedAt) {
|
|
119
|
+
if (!startedAt || !completedAt)
|
|
120
|
+
return undefined;
|
|
121
|
+
const start = new Date(startedAt).getTime();
|
|
122
|
+
const end = new Date(completedAt).getTime();
|
|
123
|
+
return Number.isFinite(start) && Number.isFinite(end) ? Math.max(0, end - start) : undefined;
|
|
124
|
+
}
|
|
74
125
|
function debugFields(node) {
|
|
75
126
|
return {
|
|
76
127
|
eventId: node.eventId,
|
|
@@ -90,6 +141,7 @@ function createUserMessageRow(node) {
|
|
|
90
141
|
lines: [{ prefix: "prompt", tokens: [token(text)] }],
|
|
91
142
|
sourceNodeIds: [node.id],
|
|
92
143
|
forkEntryId: node.entryId,
|
|
144
|
+
startedAt: node.startedAt,
|
|
93
145
|
output: text,
|
|
94
146
|
payloadRefs: node.payloadRefs,
|
|
95
147
|
};
|
|
@@ -101,6 +153,9 @@ function createAssistantMessageRow(node) {
|
|
|
101
153
|
status: mapStatus(node.status),
|
|
102
154
|
lines: [],
|
|
103
155
|
sourceNodeIds: [node.id],
|
|
156
|
+
startedAt: node.startedAt,
|
|
157
|
+
completedAt: node.source === "transcript" ? node.completedAt : undefined,
|
|
158
|
+
durationMs: node.source === "transcript" ? node.durationMs : undefined,
|
|
104
159
|
output: stringValue(node.output) || stringValue(node.summary) || "",
|
|
105
160
|
error: node.error,
|
|
106
161
|
payloadRefs: node.payloadRefs,
|
|
@@ -261,6 +316,9 @@ function createDelegationRow(node) {
|
|
|
261
316
|
id: node.id,
|
|
262
317
|
kind: "agent.delegation",
|
|
263
318
|
status: mapStatus(node.status),
|
|
319
|
+
errorKind: node.status === "error" ? "tool" : undefined,
|
|
320
|
+
title: node.title,
|
|
321
|
+
summary: node.summary,
|
|
264
322
|
lines: [
|
|
265
323
|
{
|
|
266
324
|
prefix: "bullet",
|
|
@@ -270,6 +328,9 @@ function createDelegationRow(node) {
|
|
|
270
328
|
],
|
|
271
329
|
sourceNodeIds: [node.id],
|
|
272
330
|
linkedPiboSessionId: node.linkedPiboSessionId,
|
|
331
|
+
startedAt: node.startedAt,
|
|
332
|
+
completedAt: node.completedAt,
|
|
333
|
+
durationMs: node.durationMs,
|
|
273
334
|
input: node.input,
|
|
274
335
|
output: node.output,
|
|
275
336
|
error: node.error,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { reconcileAsyncAgentRunStatuses } from "./trace-async-agent-runs.js";
|
|
2
|
-
import { applySingleEventToNodes, contentDeltaPatchNodeId, dedupeTraceEvents, eventsCanAffectAsyncAgentRunStatus, findOpenTranscriptEventIds, isConfirmedUserMessageEcho, latestTraceStreamId, traceEventDedupeKey, } from "./trace-event-projection.js";
|
|
2
|
+
import { applySingleEventToNodes, contentDeltaPatchNodeId, dedupeTraceEvents, eventsCanAffectAsyncAgentRunStatus, findOpenTranscriptEventIds, isConfirmedUserMessageEcho, latestTraceStreamId, messageTurnTimingsFromEvents, reconcileTranscriptUserMessageTimestamps, traceEventDedupeKey, } from "./trace-event-projection.js";
|
|
3
3
|
import { flattenTraceNodes, mapTraceNodesById, nestTraceNodes } from "./trace-nodes.js";
|
|
4
4
|
import { nestMutableCopiedTraceNodes, shareUnchangedTraceNodes } from "./trace-patch-nodes.js";
|
|
5
5
|
import { mapTraceChildSessionsByParent, mapTraceSubagentSessionLinks, } from "./trace-subagent-links.js";
|
|
@@ -14,7 +14,8 @@ export function buildTraceViewFromEvents(input) {
|
|
|
14
14
|
const allEntries = input.transcriptEntries ?? [];
|
|
15
15
|
const openTranscriptEventIds = findOpenTranscriptEventIds(events, sessionStatus);
|
|
16
16
|
const entries = projectTranscriptEntries(allEntries, sessionStatus, openTranscriptEventIds);
|
|
17
|
-
const nodes = traceNodesFromEntries(input.session.id, entries);
|
|
17
|
+
const nodes = traceNodesFromEntries(input.session.id, entries, messageTurnTimingsFromEvents(events));
|
|
18
|
+
reconcileTranscriptUserMessageTimestamps(nodes, events);
|
|
18
19
|
const byId = mapTraceNodesById(nodes);
|
|
19
20
|
const childByParent = mapTraceChildSessionsByParent(input.sessions ?? []);
|
|
20
21
|
const linkedChildByToolCallId = mapTraceSubagentSessionLinks(events);
|
|
@@ -26,10 +26,8 @@ export function applySingleEventToNodes(nodes, byId, piboSessionId, storedEvent,
|
|
|
26
26
|
const existing = byId.get(node.id);
|
|
27
27
|
if (existing) {
|
|
28
28
|
mergeAssistantMessageEvent(existing, node);
|
|
29
|
-
closeParentTurnForFinalAssistant(byId, existing);
|
|
30
29
|
return;
|
|
31
30
|
}
|
|
32
|
-
closeParentTurnForFinalAssistant(byId, node);
|
|
33
31
|
nodes.push(node);
|
|
34
32
|
byId.set(node.id, node);
|
|
35
33
|
return;
|
|
@@ -49,10 +47,8 @@ export function applySingleEventToNodes(nodes, byId, piboSessionId, storedEvent,
|
|
|
49
47
|
const existing = byId.get(node.id);
|
|
50
48
|
if (existing) {
|
|
51
49
|
mergeAssistantMessageEvent(existing, node);
|
|
52
|
-
closeParentTurnForFinalAssistant(byId, existing);
|
|
53
50
|
return;
|
|
54
51
|
}
|
|
55
|
-
closeParentTurnForFinalAssistant(byId, node);
|
|
56
52
|
}
|
|
57
53
|
if (node.type === "execution.compaction") {
|
|
58
54
|
const existing = findLatestCompactionNode(nodes);
|
|
@@ -68,6 +64,13 @@ export function applySingleEventToNodes(nodes, byId, piboSessionId, storedEvent,
|
|
|
68
64
|
return;
|
|
69
65
|
}
|
|
70
66
|
}
|
|
67
|
+
if (node.type === "agent.delegation" && !node.toolCallId && node.linkedPiboSessionId) {
|
|
68
|
+
const existing = findLegacySubagentLinkTarget([...byId.values()], node);
|
|
69
|
+
if (existing) {
|
|
70
|
+
mergeSubagentSessionLink(existing, node);
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
71
74
|
if (node.toolCallId) {
|
|
72
75
|
const existing = [...byId.values()].find((candidate) => candidate.toolCallId === node.toolCallId &&
|
|
73
76
|
(candidate.type === "tool.call" || candidate.type === "agent.delegation"));
|
|
@@ -102,6 +105,7 @@ function assistantMessageNodeFromEvent(piboSessionId, event, createdAt, eventSeq
|
|
|
102
105
|
title: "Agent Message",
|
|
103
106
|
status: "done",
|
|
104
107
|
startedAt: createdAt,
|
|
108
|
+
completedAt: createdAt,
|
|
105
109
|
summary: event.text,
|
|
106
110
|
output: event.text,
|
|
107
111
|
source: "event-log",
|
|
@@ -165,6 +169,28 @@ export function contentDeltaPatchNodeId(event) {
|
|
|
165
169
|
}
|
|
166
170
|
return undefined;
|
|
167
171
|
}
|
|
172
|
+
export function reconcileTranscriptUserMessageTimestamps(nodes, events) {
|
|
173
|
+
const transcriptUsers = nodes.filter((node) => node.type === "user.message" && node.source === "transcript");
|
|
174
|
+
let userCursor = 0;
|
|
175
|
+
for (const storedEvent of events) {
|
|
176
|
+
const event = storedEvent.payload;
|
|
177
|
+
if (event.type !== "message_queued" || event.source !== "user")
|
|
178
|
+
continue;
|
|
179
|
+
const eventId = typeof event.eventId === "string" ? event.eventId : storedEvent.eventId;
|
|
180
|
+
const text = typeof event.text === "string" ? event.text : undefined;
|
|
181
|
+
const matchIndex = transcriptUsers.findIndex((node, index) => {
|
|
182
|
+
if (index < userCursor)
|
|
183
|
+
return false;
|
|
184
|
+
if (eventId && (node.entryId === eventId || node.stableKey === `entry:${eventId}`))
|
|
185
|
+
return true;
|
|
186
|
+
return Boolean(text && traceNodeText(node) === text);
|
|
187
|
+
});
|
|
188
|
+
if (matchIndex === -1)
|
|
189
|
+
continue;
|
|
190
|
+
transcriptUsers[matchIndex].startedAt = storedEvent.createdAt;
|
|
191
|
+
userCursor = matchIndex + 1;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
168
194
|
export function isConfirmedUserMessageEcho(nodes, event) {
|
|
169
195
|
const payload = event.payload;
|
|
170
196
|
if (payload.type !== "message_queued" || payload.source !== "user")
|
|
@@ -266,6 +292,7 @@ function traceNodeFromEvent(piboSessionId, event, childByParent, linkedChildByTo
|
|
|
266
292
|
...base,
|
|
267
293
|
id: eventId ? messageTurnNodeId(eventId) : id,
|
|
268
294
|
type: "agent.turn",
|
|
295
|
+
startedAt: event.type === "message_started" ? createdAt : undefined,
|
|
269
296
|
title: "Agent Turn",
|
|
270
297
|
status: event.type === "message_finished" || sessionStatus !== "running" ? "done" : "running",
|
|
271
298
|
completedAt: event.type === "message_finished" ? createdAt : undefined,
|
|
@@ -296,6 +323,7 @@ function traceNodeFromEvent(piboSessionId, event, childByParent, linkedChildByTo
|
|
|
296
323
|
type: "assistant.message",
|
|
297
324
|
title: "Agent Message",
|
|
298
325
|
status: "done",
|
|
326
|
+
completedAt: createdAt,
|
|
299
327
|
summary: event.text,
|
|
300
328
|
output: event.text,
|
|
301
329
|
stableKey: assistantId ? `assistant:${assistantId}` : base.stableKey,
|
|
@@ -483,15 +511,6 @@ function mergeReasoningEvent(target, update) {
|
|
|
483
511
|
target.output = update.output ?? target.output;
|
|
484
512
|
target.completedAt = update.completedAt ?? target.completedAt;
|
|
485
513
|
}
|
|
486
|
-
function closeParentTurnForFinalAssistant(byId, assistant) {
|
|
487
|
-
if (!assistant.parentId || assistant.status !== "done")
|
|
488
|
-
return;
|
|
489
|
-
const parent = byId.get(assistant.parentId);
|
|
490
|
-
if (!parent || parent.type !== "agent.turn")
|
|
491
|
-
return;
|
|
492
|
-
parent.status = "done";
|
|
493
|
-
parent.completedAt = assistant.completedAt ?? assistant.startedAt ?? parent.completedAt;
|
|
494
|
-
}
|
|
495
514
|
function isInternalSessionOperation(action) {
|
|
496
515
|
return action === "session.fork" || action === "session.clone" || action === "session.switch";
|
|
497
516
|
}
|
|
@@ -512,6 +531,55 @@ function shouldKeepTranscriptEchoEvent(event, openTranscriptEventIds) {
|
|
|
512
531
|
function isStaleToolCallEchoEvent(event, sessionStatus) {
|
|
513
532
|
return sessionStatus !== "running" && event.type === "tool_call";
|
|
514
533
|
}
|
|
534
|
+
export function messageTurnTimingsFromEvents(events) {
|
|
535
|
+
const timings = new Map();
|
|
536
|
+
const completedEventIds = [];
|
|
537
|
+
const completedEventIdSet = new Set();
|
|
538
|
+
const ignoredEventIds = new Set();
|
|
539
|
+
for (const storedEvent of events) {
|
|
540
|
+
const event = storedEvent.payload;
|
|
541
|
+
if (event.type !== "message_started" && event.type !== "message_finished")
|
|
542
|
+
continue;
|
|
543
|
+
const eventId = typeof event.eventId === "string" ? event.eventId : undefined;
|
|
544
|
+
if (!eventId)
|
|
545
|
+
continue;
|
|
546
|
+
if (event.type === "message_started" && event.source === "service") {
|
|
547
|
+
ignoredEventIds.add(eventId);
|
|
548
|
+
continue;
|
|
549
|
+
}
|
|
550
|
+
if (ignoredEventIds.has(eventId))
|
|
551
|
+
continue;
|
|
552
|
+
const timing = timings.get(eventId) ?? {};
|
|
553
|
+
if (event.type === "message_started") {
|
|
554
|
+
timing.userText ??= event.text;
|
|
555
|
+
timing.startedAt ??= storedEvent.createdAt;
|
|
556
|
+
}
|
|
557
|
+
else {
|
|
558
|
+
timing.completedAt = storedEvent.createdAt;
|
|
559
|
+
if (!completedEventIdSet.has(eventId)) {
|
|
560
|
+
completedEventIds.push(eventId);
|
|
561
|
+
completedEventIdSet.add(eventId);
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
timings.set(eventId, timing);
|
|
565
|
+
}
|
|
566
|
+
return completedEventIds.flatMap((eventId) => {
|
|
567
|
+
const timing = timings.get(eventId);
|
|
568
|
+
if (!timing?.completedAt)
|
|
569
|
+
return [];
|
|
570
|
+
const startedAtMs = parseTimestamp(timing.startedAt);
|
|
571
|
+
const completedAtMs = parseTimestamp(timing.completedAt);
|
|
572
|
+
return [{
|
|
573
|
+
eventId,
|
|
574
|
+
userText: timing.userText,
|
|
575
|
+
startedAt: timing.startedAt,
|
|
576
|
+
completedAt: timing.completedAt,
|
|
577
|
+
durationMs: startedAtMs === undefined || completedAtMs === undefined
|
|
578
|
+
? undefined
|
|
579
|
+
: Math.max(0, completedAtMs - startedAtMs),
|
|
580
|
+
}];
|
|
581
|
+
});
|
|
582
|
+
}
|
|
515
583
|
export function findOpenTranscriptEventIds(events, sessionStatus) {
|
|
516
584
|
if (sessionStatus !== "running")
|
|
517
585
|
return new Set();
|
|
@@ -626,12 +694,48 @@ function thinkingEventNodeId(event) {
|
|
|
626
694
|
function mergeToolEvent(target, update) {
|
|
627
695
|
target.status = update.status;
|
|
628
696
|
target.summary = update.summary ?? target.summary;
|
|
629
|
-
target.input = update
|
|
697
|
+
target.input = mergeDelegationInput(target, update);
|
|
630
698
|
target.output = update.output ?? target.output;
|
|
631
699
|
target.error = update.error ?? target.error;
|
|
632
700
|
target.completedAt = update.completedAt ?? target.completedAt;
|
|
633
701
|
target.linkedPiboSessionId = update.linkedPiboSessionId ?? target.linkedPiboSessionId;
|
|
634
702
|
}
|
|
703
|
+
function mergeSubagentSessionLink(target, update) {
|
|
704
|
+
target.summary = update.summary ?? target.summary;
|
|
705
|
+
target.input = mergeDelegationInput(target, update);
|
|
706
|
+
target.linkedPiboSessionId = update.linkedPiboSessionId ?? target.linkedPiboSessionId;
|
|
707
|
+
}
|
|
708
|
+
function findLegacySubagentLinkTarget(nodes, update) {
|
|
709
|
+
const delegations = [...nodes].reverse().filter((candidate) => candidate.type === "agent.delegation");
|
|
710
|
+
const agentName = delegationAgentName(update);
|
|
711
|
+
const candidates = delegations.filter((candidate) => !candidate.linkedPiboSessionId && delegationAgentName(candidate) === agentName);
|
|
712
|
+
const threadKey = delegationThreadKey(update.input);
|
|
713
|
+
const matchingCandidate = threadKey
|
|
714
|
+
? candidates.find((candidate) => delegationThreadKey(candidate.input) === threadKey)
|
|
715
|
+
: candidates.length === 1 ? candidates[0] : undefined;
|
|
716
|
+
if (matchingCandidate)
|
|
717
|
+
return matchingCandidate;
|
|
718
|
+
return delegations.find((candidate) => candidate.linkedPiboSessionId === update.linkedPiboSessionId);
|
|
719
|
+
}
|
|
720
|
+
function delegationAgentName(node) {
|
|
721
|
+
const input = isObjectRecord(node.input) ? node.input : undefined;
|
|
722
|
+
const value = typeof input?.subagentName === "string" ? input.subagentName : node.summary ?? node.title;
|
|
723
|
+
return typeof value === "string" ? value.replace(/^pibo_subagent_/, "").trim().toLowerCase() || undefined : undefined;
|
|
724
|
+
}
|
|
725
|
+
function delegationThreadKey(value) {
|
|
726
|
+
if (!isObjectRecord(value) || typeof value.threadKey !== "string")
|
|
727
|
+
return undefined;
|
|
728
|
+
return value.threadKey.trim() || undefined;
|
|
729
|
+
}
|
|
730
|
+
function mergeDelegationInput(target, update) {
|
|
731
|
+
if (target.type !== "agent.delegation" || !isObjectRecord(target.input) || !isObjectRecord(update.input)) {
|
|
732
|
+
return update.input ?? target.input;
|
|
733
|
+
}
|
|
734
|
+
return Object.fromEntries(Object.entries({ ...target.input, ...update.input }).filter(([, value]) => value !== undefined));
|
|
735
|
+
}
|
|
736
|
+
function isObjectRecord(value) {
|
|
737
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
738
|
+
}
|
|
635
739
|
function findLatestCompactionNode(nodes) {
|
|
636
740
|
return flattenTraceNodes([...nodes]).reverse().find((node) => node.type === "execution.compaction" && node.status === "running");
|
|
637
741
|
}
|
|
@@ -656,6 +760,12 @@ function stringifyPreview(value) {
|
|
|
656
760
|
return String(value);
|
|
657
761
|
}
|
|
658
762
|
}
|
|
763
|
+
function parseTimestamp(value) {
|
|
764
|
+
if (!value)
|
|
765
|
+
return undefined;
|
|
766
|
+
const timestamp = new Date(value).getTime();
|
|
767
|
+
return Number.isFinite(timestamp) ? timestamp : undefined;
|
|
768
|
+
}
|
|
659
769
|
function cryptoSafeId(value) {
|
|
660
770
|
return base64UrlEncode(new TextEncoder().encode(JSON.stringify(value))).slice(0, 48);
|
|
661
771
|
}
|