@bivy/bivy 0.16.25-staging.30 → 0.16.25-staging.31
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.
|
@@ -29,6 +29,51 @@
|
|
|
29
29
|
import fs from "node:fs";
|
|
30
30
|
import path from "node:path";
|
|
31
31
|
import { normalizedIntermediateText, thinkingTextFromContent, mergeTranscript } from "./transcript-merge.js";
|
|
32
|
+
/**
|
|
33
|
+
* Decompose a transcript message into serialization-independent "atoms": the
|
|
34
|
+
* agent's own tool ids (unique + identical across serializations) and its text
|
|
35
|
+
* fragments. Used by mergeBases to recognize when a native-store reload re-states
|
|
36
|
+
* turns already in the streamed event-log base, even when the two serializations
|
|
37
|
+
* group blocks into messages differently (Claude keeps tool + text separate;
|
|
38
|
+
* OpenCode bundles them; a reload may drop tool blocks Bivy recorded as overlays).
|
|
39
|
+
*/
|
|
40
|
+
export function messageContentAtoms(message) {
|
|
41
|
+
const toolIds = [];
|
|
42
|
+
const texts = [];
|
|
43
|
+
if (!message)
|
|
44
|
+
return { toolIds, texts };
|
|
45
|
+
const content = message.content;
|
|
46
|
+
const role = typeof message.role === "string" ? message.role : "";
|
|
47
|
+
const pushText = (value) => {
|
|
48
|
+
if (typeof value !== "string")
|
|
49
|
+
return;
|
|
50
|
+
const text = value.trim();
|
|
51
|
+
if (text)
|
|
52
|
+
texts.push(`${role}#${text}`);
|
|
53
|
+
};
|
|
54
|
+
if (typeof content === "string") {
|
|
55
|
+
pushText(content);
|
|
56
|
+
return { toolIds, texts };
|
|
57
|
+
}
|
|
58
|
+
if (Array.isArray(content)) {
|
|
59
|
+
for (const block of content) {
|
|
60
|
+
if (!block || typeof block !== "object")
|
|
61
|
+
continue;
|
|
62
|
+
const b = block;
|
|
63
|
+
const type = String(b.type || "");
|
|
64
|
+
if (type === "tool_use" && b.id)
|
|
65
|
+
toolIds.push(`tu:${String(b.id)}`);
|
|
66
|
+
else if (type === "tool_result") {
|
|
67
|
+
const id = b.tool_use_id ?? b.toolUseId;
|
|
68
|
+
if (id)
|
|
69
|
+
toolIds.push(`tr:${String(id)}`);
|
|
70
|
+
}
|
|
71
|
+
else if (type === "text")
|
|
72
|
+
pushText(b.text);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return { toolIds, texts };
|
|
76
|
+
}
|
|
32
77
|
/**
|
|
33
78
|
* Union of a persisted base and a runtime's live transcript, such that the result
|
|
34
79
|
* is never shorter than either input alone. A live session's runtime transcript
|
|
@@ -53,32 +98,89 @@ export function mergeBases(logged, runtime) {
|
|
|
53
98
|
const keys = [];
|
|
54
99
|
const known = new Set();
|
|
55
100
|
const identities = new Map();
|
|
56
|
-
const
|
|
57
|
-
const key = JSON.stringify(m);
|
|
58
|
-
if (known.has(key))
|
|
59
|
-
return;
|
|
101
|
+
const identityOf = (m) => {
|
|
60
102
|
const identified = m;
|
|
61
|
-
|
|
103
|
+
return typeof identified.id === "string" && identified.id
|
|
62
104
|
? `id:${identified.id}`
|
|
63
105
|
: typeof identified.timestamp === "number" ? `time:${identified.role}:${identified.timestamp}` : "";
|
|
106
|
+
};
|
|
107
|
+
const add = (m) => {
|
|
108
|
+
const key = JSON.stringify(m);
|
|
109
|
+
if (known.has(key))
|
|
110
|
+
return false;
|
|
111
|
+
const identity = identityOf(m);
|
|
64
112
|
const index = identity ? identities.get(identity) : undefined;
|
|
65
113
|
if (index !== undefined) {
|
|
66
114
|
known.delete(keys[index]);
|
|
67
115
|
keys[index] = key;
|
|
68
116
|
known.add(key);
|
|
69
117
|
merged[index] = m;
|
|
70
|
-
return;
|
|
118
|
+
return false;
|
|
71
119
|
}
|
|
72
120
|
known.add(key);
|
|
73
121
|
keys.push(key);
|
|
74
122
|
merged.push(m);
|
|
75
123
|
if (identity)
|
|
76
124
|
identities.set(identity, merged.length - 1);
|
|
125
|
+
return true;
|
|
77
126
|
};
|
|
78
127
|
for (const m of logged)
|
|
79
128
|
add(m);
|
|
80
|
-
|
|
81
|
-
|
|
129
|
+
// Cross-serialization fold (the reopen case): a native-store agent (Claude,
|
|
130
|
+
// Codex, Pi, OpenCode) reloads its FULL transcript on reopen, but that reload
|
|
131
|
+
// reconstructs each turn with the agent's OWN timestamps and a possibly very
|
|
132
|
+
// different block shape than the event-log base Bivy streamed and stored live
|
|
133
|
+
// (e.g. OpenCode's reload is just user + final text, the tool blocks living
|
|
134
|
+
// only in Bivy's overlays). The id/timestamp fold above can't match those two
|
|
135
|
+
// serializations, so every turn double-appeared on reopen. Recognize a
|
|
136
|
+
// re-serialization by its serialization-independent atoms — the agent's own
|
|
137
|
+
// unique tool ids and its text fragments — and drop a runtime message whose
|
|
138
|
+
// atoms are ALL already covered by the logged base. Tool ids are globally
|
|
139
|
+
// unique (a covered id is literally the same call); text is matched through a
|
|
140
|
+
// consumed multiset so a genuinely repeated turn ("run tests" twice) and a
|
|
141
|
+
// disjoint resumed turn are both preserved rather than collapsed.
|
|
142
|
+
const loggedToolIds = new Set();
|
|
143
|
+
const loggedTexts = new Map();
|
|
144
|
+
for (const m of merged) {
|
|
145
|
+
const atoms = messageContentAtoms(m);
|
|
146
|
+
for (const id of atoms.toolIds)
|
|
147
|
+
loggedToolIds.add(id);
|
|
148
|
+
for (const text of atoms.texts)
|
|
149
|
+
loggedTexts.set(text, (loggedTexts.get(text) ?? 0) + 1);
|
|
150
|
+
}
|
|
151
|
+
const coveredByLogged = (m) => {
|
|
152
|
+
const atoms = messageContentAtoms(m);
|
|
153
|
+
if (!atoms.toolIds.length && !atoms.texts.length)
|
|
154
|
+
return false; // opaque → keep
|
|
155
|
+
for (const id of atoms.toolIds)
|
|
156
|
+
if (!loggedToolIds.has(id))
|
|
157
|
+
return false;
|
|
158
|
+
// Text must be available in the (remaining) logged multiset; tentatively
|
|
159
|
+
// consume so two runtime copies can't both match a single logged fragment.
|
|
160
|
+
const consumed = new Map();
|
|
161
|
+
for (const text of atoms.texts) {
|
|
162
|
+
const available = (loggedTexts.get(text) ?? 0) - (consumed.get(text) ?? 0);
|
|
163
|
+
if (available <= 0)
|
|
164
|
+
return false;
|
|
165
|
+
consumed.set(text, (consumed.get(text) ?? 0) + 1);
|
|
166
|
+
}
|
|
167
|
+
for (const [text, n] of consumed)
|
|
168
|
+
loggedTexts.set(text, (loggedTexts.get(text) ?? 0) - n);
|
|
169
|
+
return true;
|
|
170
|
+
};
|
|
171
|
+
for (const m of runtime) {
|
|
172
|
+
const key = JSON.stringify(m);
|
|
173
|
+
if (known.has(key))
|
|
174
|
+
continue;
|
|
175
|
+
const identity = identityOf(m);
|
|
176
|
+
if (identity && identities.has(identity)) {
|
|
177
|
+
add(m);
|
|
178
|
+
continue;
|
|
179
|
+
} // streaming refinement of a known turn
|
|
180
|
+
if (coveredByLogged(m))
|
|
181
|
+
continue; // reopen re-serialization already in the base
|
|
182
|
+
add(m); // a genuinely new (resumed) turn
|
|
183
|
+
}
|
|
82
184
|
return merged;
|
|
83
185
|
}
|
|
84
186
|
/** Content-block type carried by a folded outbound attachment. MUST match
|
|
@@ -152,7 +152,14 @@ export function createTranscriptPersistence(deps) {
|
|
|
152
152
|
return;
|
|
153
153
|
}
|
|
154
154
|
else {
|
|
155
|
-
|
|
155
|
+
// Carry the delegation/sub-agent parent id onto the persisted tool_use
|
|
156
|
+
// block so a reloaded transcript nests a sub-agent's calls under the
|
|
157
|
+
// delegation that spawned them (toolEntriesFromContent → toolParentId),
|
|
158
|
+
// exactly like the live stream does. Without this the parent id lived only
|
|
159
|
+
// on the in-flight event and reopening a session flattened sub-agent work.
|
|
160
|
+
// Generic: any runtime whose tool_call event carries parentToolUseId nests.
|
|
161
|
+
const parentToolUseId = typeof event.parentToolUseId === "string" && event.parentToolUseId ? event.parentToolUseId : undefined;
|
|
162
|
+
eventLog.append(record.id, { ...base, id: `bivy-tool-call-${callId}`, content: [{ type: "tool_use", id: callId, name, input, ...(parentToolUseId ? { parentToolUseId } : {}), ...(event.detail ? { detail: event.detail } : {}) }] });
|
|
156
163
|
}
|
|
157
164
|
}
|
|
158
165
|
function persistIntermediateFromEvent(record, event, final = false) {
|
package/package.json
CHANGED