@qearlyao/familiar 0.2.4 → 0.3.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 +4 -0
- package/config.example.toml +2 -2
- package/dist/agent/payload-normalizers.js +52 -0
- package/dist/agent/session-helpers.js +86 -0
- package/dist/agent/tool-descriptions.js +4 -0
- package/dist/agent/tools.js +30 -0
- package/dist/agent/transcript-log.js +93 -0
- package/dist/agent/types.js +1 -0
- package/dist/agent-core.js +82 -0
- package/dist/agent-work-queue.js +55 -0
- package/dist/agent.js +91 -322
- package/dist/browser-tools.js +80 -28
- package/dist/chat-log.js +15 -3
- package/dist/cli.js +36 -6
- package/dist/config/enums.js +35 -0
- package/dist/config/interpolate.js +15 -0
- package/dist/config/model-refs.js +11 -0
- package/dist/config/readers.js +116 -0
- package/dist/config/sections.js +113 -0
- package/dist/config/types.js +1 -0
- package/dist/config-registry.js +26 -7
- package/dist/config.js +8 -271
- package/dist/discord/channel.js +32 -0
- package/dist/discord/chunking.js +163 -0
- package/dist/discord/client.js +44 -0
- package/dist/discord/commands.js +181 -0
- package/dist/discord/inbound.js +44 -0
- package/dist/discord/send.js +106 -0
- package/dist/discord/turn.js +55 -0
- package/dist/discord.js +266 -1186
- package/dist/ids.js +11 -0
- package/dist/image-gen.js +90 -10
- package/dist/index.js +1 -0
- package/dist/memory/index/store.js +21 -17
- package/dist/memory/index/vector-codec.js +2 -2
- package/dist/memory/lcm/context-transformer.js +6 -2
- package/dist/memory/lcm/segment-manager.js +6 -2
- package/dist/memory/lcm/store/index-ids.js +6 -0
- package/dist/memory/lcm/store/inserts.js +31 -0
- package/dist/memory/lcm/store/normalizers.js +91 -0
- package/dist/memory/lcm/store/row-mappers.js +114 -0
- package/dist/memory/lcm/store/row-types.js +1 -0
- package/dist/memory/lcm/store/serialization.js +37 -0
- package/dist/memory/lcm/store/snapshots.js +73 -0
- package/dist/memory/lcm/store.js +20 -360
- package/dist/owner-identity.js +29 -0
- package/dist/runtime-manager.js +51 -0
- package/dist/runtime.js +89 -41
- package/dist/scheduler-runner.js +243 -0
- package/dist/scheduler.js +1 -1
- package/dist/service.js +1 -0
- package/dist/settings.js +3 -0
- package/dist/util/fs.js +1 -1
- package/dist/web/event-hub.js +246 -0
- package/dist/{web-http.js → web/http.js} +19 -5
- package/dist/web/memes.js +25 -0
- package/dist/web/messages.js +345 -0
- package/dist/web/multipart.js +80 -0
- package/dist/web/payloads.js +34 -0
- package/dist/{web-static.js → web/static.js} +19 -14
- package/dist/web/stream.js +69 -0
- package/dist/web-tools/cache.js +42 -0
- package/dist/web-tools/config.js +16 -0
- package/dist/web-tools/fetch-providers.js +119 -0
- package/dist/web-tools/format.js +88 -0
- package/dist/web-tools/http.js +81 -0
- package/dist/web-tools/routing.js +29 -0
- package/dist/web-tools/safety.js +73 -0
- package/dist/web-tools/search-providers.js +277 -0
- package/dist/web-tools/types.js +54 -0
- package/dist/web-tools/util.js +23 -0
- package/dist/web-tools.js +9 -798
- package/dist/web.js +416 -984
- package/npm-shrinkwrap.json +242 -201
- package/package.json +4 -4
- package/web/dist/assets/index-CSkxUQCr.js +63 -0
- package/web/dist/assets/index-DllM6RqL.css +2 -0
- package/web/dist/index.html +6 -3
- package/web/dist/assets/index-B23WT77N.js +0 -63
- package/web/dist/assets/index-D3MotFzN.css +0 -2
- /package/dist/{web-auth.js → web/auth.js} +0 -0
- /package/dist/{web-events.js → web/events.js} +0 -0
- /package/dist/{web-types.js → web/types.js} +0 -0
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
import { getContactNickname } from "../contact-note.js";
|
|
2
|
+
import { eventId, toUnixMs } from "../ids.js";
|
|
3
|
+
import { consumeSilentDelta, createSilentFilterState, finalizeSilentFilter } from "../silent-marker.js";
|
|
4
|
+
import { encodeFrame, replayEvents } from "./events.js";
|
|
5
|
+
import { toolFromStoredAgentEvent, webAttachments } from "./messages.js";
|
|
6
|
+
import { EVENT_REPLAY_LIMIT, WEB_USER_NAME } from "./types.js";
|
|
7
|
+
const IN_FLIGHT_TTL_MS = 10 * 60 * 1000;
|
|
8
|
+
export function createWebEventHub(config, personaName) {
|
|
9
|
+
const clients = new Set();
|
|
10
|
+
const eventsByChannel = new Map();
|
|
11
|
+
const runtimeSubscriptions = new Map();
|
|
12
|
+
const inFlightMessages = new Map();
|
|
13
|
+
const getOrCreateInFlight = (messageIdValue) => {
|
|
14
|
+
let entry = inFlightMessages.get(messageIdValue);
|
|
15
|
+
if (!entry) {
|
|
16
|
+
entry = { locallyStreamed: false, startedSilent: false, lastActiveAt: Date.now() };
|
|
17
|
+
inFlightMessages.set(messageIdValue, entry);
|
|
18
|
+
}
|
|
19
|
+
else {
|
|
20
|
+
entry.lastActiveAt = Date.now();
|
|
21
|
+
}
|
|
22
|
+
return entry;
|
|
23
|
+
};
|
|
24
|
+
const touchInFlight = (messageIdValue) => {
|
|
25
|
+
const entry = inFlightMessages.get(messageIdValue);
|
|
26
|
+
if (entry)
|
|
27
|
+
entry.lastActiveAt = Date.now();
|
|
28
|
+
};
|
|
29
|
+
const inFlightGcTimer = setInterval(() => {
|
|
30
|
+
const cutoff = Date.now() - IN_FLIGHT_TTL_MS;
|
|
31
|
+
for (const [id, entry] of inFlightMessages) {
|
|
32
|
+
if (entry.lastActiveAt < cutoff)
|
|
33
|
+
inFlightMessages.delete(id);
|
|
34
|
+
}
|
|
35
|
+
}, 60 * 1000);
|
|
36
|
+
inFlightGcTimer.unref?.();
|
|
37
|
+
const publish = (event) => {
|
|
38
|
+
const fullEvent = { ...event, eventId: eventId(), ts: event.ts ?? Date.now() };
|
|
39
|
+
const events = eventsByChannel.get(fullEvent.channelKey ?? "") ?? [];
|
|
40
|
+
events.push(fullEvent);
|
|
41
|
+
if (events.length > EVENT_REPLAY_LIMIT)
|
|
42
|
+
events.shift();
|
|
43
|
+
eventsByChannel.set(fullEvent.channelKey ?? "", events);
|
|
44
|
+
const frame = encodeFrame(JSON.stringify(fullEvent));
|
|
45
|
+
for (const client of clients) {
|
|
46
|
+
if (client.channelKey === fullEvent.channelKey && !client.socket.destroyed) {
|
|
47
|
+
if (client.authed) {
|
|
48
|
+
client.socket.write(frame);
|
|
49
|
+
}
|
|
50
|
+
else {
|
|
51
|
+
const pendingEvents = client.pendingEvents ?? [];
|
|
52
|
+
pendingEvents.push(fullEvent);
|
|
53
|
+
client.pendingEvents = pendingEvents;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return fullEvent;
|
|
58
|
+
};
|
|
59
|
+
const publishDelta = (channelKey, messageIdValue, part, text, ts) => publish({ type: "delta", channelKey, messageId: messageIdValue, part, content: text, text, ts });
|
|
60
|
+
const publishStoredAgentEvent = (channelKey, messageIdValue, storedEvent, ts) => {
|
|
61
|
+
touchInFlight(messageIdValue);
|
|
62
|
+
if (storedEvent.type === "message_start" && storedEvent.role === "assistant") {
|
|
63
|
+
const entry = getOrCreateInFlight(messageIdValue);
|
|
64
|
+
entry.locallyStreamed = true;
|
|
65
|
+
entry.silentFilter = createSilentFilterState();
|
|
66
|
+
entry.pendingStartTs = ts;
|
|
67
|
+
entry.startedSilent = false;
|
|
68
|
+
}
|
|
69
|
+
const startedSilentMessage = () => {
|
|
70
|
+
const entry = inFlightMessages.get(messageIdValue);
|
|
71
|
+
if (!entry || entry.startedSilent)
|
|
72
|
+
return false;
|
|
73
|
+
const startTs = entry.pendingStartTs;
|
|
74
|
+
entry.pendingStartTs = undefined;
|
|
75
|
+
entry.startedSilent = true;
|
|
76
|
+
publish({
|
|
77
|
+
type: "message_started",
|
|
78
|
+
channelKey,
|
|
79
|
+
messageId: messageIdValue,
|
|
80
|
+
role: "assistant",
|
|
81
|
+
who: personaName,
|
|
82
|
+
ts: startTs,
|
|
83
|
+
});
|
|
84
|
+
return true;
|
|
85
|
+
};
|
|
86
|
+
if (storedEvent.type === "message_update") {
|
|
87
|
+
const assistantEvent = storedEvent.assistantMessageEvent;
|
|
88
|
+
if (assistantEvent.type === "thinking_delta") {
|
|
89
|
+
startedSilentMessage();
|
|
90
|
+
publishDelta(channelKey, messageIdValue, "thinking", assistantEvent.delta, ts);
|
|
91
|
+
}
|
|
92
|
+
if (assistantEvent.type === "text_delta") {
|
|
93
|
+
const filter = inFlightMessages.get(messageIdValue)?.silentFilter;
|
|
94
|
+
if (!filter) {
|
|
95
|
+
startedSilentMessage();
|
|
96
|
+
publishDelta(channelKey, messageIdValue, "text", assistantEvent.delta, ts);
|
|
97
|
+
}
|
|
98
|
+
else {
|
|
99
|
+
const result = consumeSilentDelta(filter, assistantEvent.delta);
|
|
100
|
+
if (result.kind === "emit" && result.text) {
|
|
101
|
+
startedSilentMessage();
|
|
102
|
+
publishDelta(channelKey, messageIdValue, "text", result.text, ts);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
if (storedEvent.type === "tool_execution_start") {
|
|
108
|
+
startedSilentMessage();
|
|
109
|
+
}
|
|
110
|
+
if (storedEvent.type === "message_end" && storedEvent.role === "assistant") {
|
|
111
|
+
const entry = inFlightMessages.get(messageIdValue);
|
|
112
|
+
const filter = entry?.silentFilter;
|
|
113
|
+
let silent = false;
|
|
114
|
+
if (filter && entry) {
|
|
115
|
+
const final = finalizeSilentFilter(filter);
|
|
116
|
+
silent = final.silent;
|
|
117
|
+
if (!silent) {
|
|
118
|
+
startedSilentMessage();
|
|
119
|
+
if (final.flush) {
|
|
120
|
+
publishDelta(channelKey, messageIdValue, "text", final.flush, ts);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
else {
|
|
124
|
+
entry.startedSilent = true;
|
|
125
|
+
entry.pendingStartTs = undefined;
|
|
126
|
+
}
|
|
127
|
+
entry.silentFilter = undefined;
|
|
128
|
+
}
|
|
129
|
+
else {
|
|
130
|
+
startedSilentMessage();
|
|
131
|
+
}
|
|
132
|
+
publish({
|
|
133
|
+
type: "message_completed",
|
|
134
|
+
channelKey,
|
|
135
|
+
messageId: messageIdValue,
|
|
136
|
+
usage: storedEvent.usage,
|
|
137
|
+
silent: silent || undefined,
|
|
138
|
+
ts,
|
|
139
|
+
});
|
|
140
|
+
if (storedEvent.errorMessage) {
|
|
141
|
+
publish({
|
|
142
|
+
type: "model_error",
|
|
143
|
+
channelKey,
|
|
144
|
+
messageId: messageIdValue,
|
|
145
|
+
message: storedEvent.errorMessage,
|
|
146
|
+
ts,
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
const tool = toolFromStoredAgentEvent(storedEvent, ts ?? Date.now());
|
|
151
|
+
if (tool)
|
|
152
|
+
publish({ type: "tool_event", channelKey, messageId: messageIdValue, tool, ts });
|
|
153
|
+
};
|
|
154
|
+
const subscribeRuntime = (runtime) => {
|
|
155
|
+
if (runtimeSubscriptions.has(runtime.channelKey))
|
|
156
|
+
return;
|
|
157
|
+
const unsubscribeRecords = runtime.subscribe((record) => {
|
|
158
|
+
if (record.type === "inbound") {
|
|
159
|
+
publish({
|
|
160
|
+
type: "message_started",
|
|
161
|
+
channelKey: runtime.channelKey,
|
|
162
|
+
messageId: record.messageId,
|
|
163
|
+
role: "user",
|
|
164
|
+
who: record.authorName || getContactNickname(WEB_USER_NAME),
|
|
165
|
+
ts: toUnixMs(record.ts),
|
|
166
|
+
});
|
|
167
|
+
publishDelta(runtime.channelKey, record.messageId, "text", record.text, toUnixMs(record.ts));
|
|
168
|
+
publish({
|
|
169
|
+
type: "message_completed",
|
|
170
|
+
channelKey: runtime.channelKey,
|
|
171
|
+
messageId: record.messageId,
|
|
172
|
+
attachments: webAttachments(config, record.attachments),
|
|
173
|
+
ts: toUnixMs(record.ts),
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
if (record.type === "outbound" && !record.control) {
|
|
177
|
+
const outboundId = record.webMessageId || record.messageIds[0] || `out_${record.recordId}`;
|
|
178
|
+
const completion = {
|
|
179
|
+
type: "message_completed",
|
|
180
|
+
channelKey: runtime.channelKey,
|
|
181
|
+
messageId: outboundId,
|
|
182
|
+
thinkingMs: record.thinkingMs,
|
|
183
|
+
attachments: webAttachments(config, record.attachments),
|
|
184
|
+
silent: record.silent || undefined,
|
|
185
|
+
ts: toUnixMs(record.ts),
|
|
186
|
+
};
|
|
187
|
+
if (inFlightMessages.get(outboundId)?.locallyStreamed) {
|
|
188
|
+
inFlightMessages.delete(outboundId);
|
|
189
|
+
publish(completion);
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
if (!record.silent) {
|
|
193
|
+
publish({
|
|
194
|
+
type: "message_started",
|
|
195
|
+
channelKey: runtime.channelKey,
|
|
196
|
+
messageId: outboundId,
|
|
197
|
+
role: "assistant",
|
|
198
|
+
who: personaName,
|
|
199
|
+
ts: toUnixMs(record.ts),
|
|
200
|
+
});
|
|
201
|
+
if (record.thinking)
|
|
202
|
+
publishDelta(runtime.channelKey, outboundId, "thinking", record.thinking, toUnixMs(record.ts));
|
|
203
|
+
if (record.text)
|
|
204
|
+
publishDelta(runtime.channelKey, outboundId, "text", record.text, toUnixMs(record.ts));
|
|
205
|
+
}
|
|
206
|
+
publish(completion);
|
|
207
|
+
}
|
|
208
|
+
});
|
|
209
|
+
const unsubscribeAgentEvents = runtime.subscribeAgentEvents((agentEvent) => {
|
|
210
|
+
publishStoredAgentEvent(runtime.channelKey, agentEvent.messageId, agentEvent.event, agentEvent.ts);
|
|
211
|
+
});
|
|
212
|
+
runtimeSubscriptions.set(runtime.channelKey, () => {
|
|
213
|
+
unsubscribeRecords();
|
|
214
|
+
unsubscribeAgentEvents();
|
|
215
|
+
});
|
|
216
|
+
};
|
|
217
|
+
return {
|
|
218
|
+
publish,
|
|
219
|
+
publishDelta,
|
|
220
|
+
async appendAndPublishError(runtime, message) {
|
|
221
|
+
await runtime.appendError(message);
|
|
222
|
+
publish({ type: "error", channelKey: runtime.channelKey, code: "unknown", message });
|
|
223
|
+
},
|
|
224
|
+
markLocallyStreamed(messageIdValue) {
|
|
225
|
+
getOrCreateInFlight(messageIdValue).locallyStreamed = true;
|
|
226
|
+
},
|
|
227
|
+
subscribeRuntime,
|
|
228
|
+
replay(client, channelKey, lastEventId) {
|
|
229
|
+
const events = eventsByChannel.get(channelKey) ?? [];
|
|
230
|
+
replayEvents(client, events, lastEventId, () => publish({ type: "replay_window_lost", channelKey }));
|
|
231
|
+
},
|
|
232
|
+
registerClient(client) {
|
|
233
|
+
clients.add(client);
|
|
234
|
+
return () => clients.delete(client);
|
|
235
|
+
},
|
|
236
|
+
stop() {
|
|
237
|
+
clearInterval(inFlightGcTimer);
|
|
238
|
+
for (const client of clients)
|
|
239
|
+
client.socket.destroy();
|
|
240
|
+
clients.clear();
|
|
241
|
+
for (const unsubscribe of runtimeSubscriptions.values())
|
|
242
|
+
unsubscribe();
|
|
243
|
+
runtimeSubscriptions.clear();
|
|
244
|
+
},
|
|
245
|
+
};
|
|
246
|
+
}
|
|
@@ -1,4 +1,14 @@
|
|
|
1
1
|
export const MAX_BODY_BYTES = 64 * 1024;
|
|
2
|
+
// Thrown where a request is malformed so the dispatcher can answer with the right
|
|
3
|
+
// client-error status instead of a blanket 500.
|
|
4
|
+
export class HttpError extends Error {
|
|
5
|
+
status;
|
|
6
|
+
constructor(status, message) {
|
|
7
|
+
super(message);
|
|
8
|
+
this.status = status;
|
|
9
|
+
this.name = "HttpError";
|
|
10
|
+
}
|
|
11
|
+
}
|
|
2
12
|
export function sendJson(response, status, body, headers = {}) {
|
|
3
13
|
response.writeHead(status, {
|
|
4
14
|
"content-type": "application/json; charset=utf-8",
|
|
@@ -18,12 +28,16 @@ export async function readJsonBody(request) {
|
|
|
18
28
|
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
19
29
|
total += buffer.length;
|
|
20
30
|
if (total > MAX_BODY_BYTES)
|
|
21
|
-
throw new
|
|
31
|
+
throw new HttpError(413, "Request body too large");
|
|
22
32
|
chunks.push(buffer);
|
|
23
33
|
}
|
|
24
34
|
const raw = Buffer.concat(chunks).toString("utf8").trim();
|
|
25
|
-
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
|
|
35
|
+
if (!raw)
|
|
36
|
+
return {};
|
|
37
|
+
try {
|
|
38
|
+
return JSON.parse(raw);
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
throw new HttpError(400, "Request body must be valid JSON");
|
|
42
|
+
}
|
|
29
43
|
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
export function parseMemeCatalog(markdown) {
|
|
3
|
+
const families = [];
|
|
4
|
+
let currentFamily;
|
|
5
|
+
for (const line of markdown.split(/\r?\n/)) {
|
|
6
|
+
const familyMatch = line.match(/^## (.+)$/);
|
|
7
|
+
if (familyMatch) {
|
|
8
|
+
currentFamily = { name: familyMatch[1]?.trim() ?? "", memes: [] };
|
|
9
|
+
families.push(currentFamily);
|
|
10
|
+
continue;
|
|
11
|
+
}
|
|
12
|
+
if (!currentFamily || !line.startsWith("- ") || !line.includes(" — "))
|
|
13
|
+
continue;
|
|
14
|
+
const separator = line.indexOf(" — ");
|
|
15
|
+
const name = line.slice(2, separator).trim();
|
|
16
|
+
const suffix = line.slice(separator + " — ".length).trim();
|
|
17
|
+
if (!name || !suffix)
|
|
18
|
+
continue;
|
|
19
|
+
currentFamily.memes.push({ name, url: `https://files.catbox.moe/${suffix}` });
|
|
20
|
+
}
|
|
21
|
+
return families;
|
|
22
|
+
}
|
|
23
|
+
export function memeCatalogPath(config) {
|
|
24
|
+
return join(config.workspacePath, "skills", "memes", "SKILL.md");
|
|
25
|
+
}
|
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
import { hiddenWebMessageIds } from "../chat-log.js";
|
|
2
|
+
import { getContactNickname } from "../contact-note.js";
|
|
3
|
+
import { publicAttachmentPath } from "../generated-media.js";
|
|
4
|
+
import { toUnixMs } from "../ids.js";
|
|
5
|
+
import { isRecord } from "../util/guards.js";
|
|
6
|
+
import { WEB_USER_NAME } from "./types.js";
|
|
7
|
+
export function isUserVisibleRuntimeRecord(record) {
|
|
8
|
+
return record.type !== "runtime" || !["armed", "reset", "stopped"].includes(record.event);
|
|
9
|
+
}
|
|
10
|
+
export function webAttachments(config, attachments) {
|
|
11
|
+
if (!attachments?.length)
|
|
12
|
+
return undefined;
|
|
13
|
+
return attachments.map((attachment) => ({
|
|
14
|
+
id: attachment.id,
|
|
15
|
+
name: attachment.name,
|
|
16
|
+
kind: attachment.kind,
|
|
17
|
+
mimeType: attachment.mimeType,
|
|
18
|
+
size: attachment.size,
|
|
19
|
+
url: attachment.localPath ? publicAttachmentPath(config, attachment.localPath) : attachment.remoteUrl,
|
|
20
|
+
}));
|
|
21
|
+
}
|
|
22
|
+
export function attachmentDerivedText(attachment) {
|
|
23
|
+
if (attachment.derived?.text?.label === "preview")
|
|
24
|
+
return undefined;
|
|
25
|
+
return attachment.derived?.text?.text;
|
|
26
|
+
}
|
|
27
|
+
export function toolError(result) {
|
|
28
|
+
if (typeof result === "string")
|
|
29
|
+
return result;
|
|
30
|
+
if (!isRecord(result))
|
|
31
|
+
return undefined;
|
|
32
|
+
if (typeof result.error === "string")
|
|
33
|
+
return result.error;
|
|
34
|
+
if (typeof result.message === "string")
|
|
35
|
+
return result.message;
|
|
36
|
+
return undefined;
|
|
37
|
+
}
|
|
38
|
+
export function toolFromStoredAgentEvent(event, ts) {
|
|
39
|
+
if (event.type === "tool_execution_start") {
|
|
40
|
+
return {
|
|
41
|
+
id: event.toolCallId,
|
|
42
|
+
name: event.toolName,
|
|
43
|
+
status: "running",
|
|
44
|
+
args: event.args,
|
|
45
|
+
startedAt: ts,
|
|
46
|
+
updatedAt: ts,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
if (event.type === "tool_execution_update") {
|
|
50
|
+
return {
|
|
51
|
+
id: event.toolCallId,
|
|
52
|
+
name: event.toolName,
|
|
53
|
+
status: "running",
|
|
54
|
+
args: event.args,
|
|
55
|
+
partialResult: event.partialResult,
|
|
56
|
+
updatedAt: ts,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
if (event.type === "tool_execution_end") {
|
|
60
|
+
return {
|
|
61
|
+
id: event.toolCallId,
|
|
62
|
+
name: event.toolName,
|
|
63
|
+
status: event.isError ? "error" : "completed",
|
|
64
|
+
result: event.result,
|
|
65
|
+
error: event.isError ? toolError(event.result) : undefined,
|
|
66
|
+
completedAt: ts,
|
|
67
|
+
updatedAt: ts,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
if (event.type === "message_update" && event.assistantMessageEvent.type === "toolcall_end") {
|
|
71
|
+
return {
|
|
72
|
+
id: event.assistantMessageEvent.toolCall.id,
|
|
73
|
+
name: event.assistantMessageEvent.toolCall.name,
|
|
74
|
+
status: "pending",
|
|
75
|
+
args: event.assistantMessageEvent.toolCall.arguments,
|
|
76
|
+
updatedAt: ts,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
return undefined;
|
|
80
|
+
}
|
|
81
|
+
export function mergeToolEvent(existing, patch) {
|
|
82
|
+
const terminal = patch.status === "completed" || patch.status === "error";
|
|
83
|
+
return {
|
|
84
|
+
...existing,
|
|
85
|
+
...patch,
|
|
86
|
+
args: patch.args ?? existing?.args,
|
|
87
|
+
partialResult: terminal ? undefined : (patch.partialResult ?? existing?.partialResult),
|
|
88
|
+
result: patch.result ?? existing?.result,
|
|
89
|
+
error: patch.error ?? existing?.error,
|
|
90
|
+
startedAt: existing?.startedAt ?? patch.startedAt,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
export function stepId(messageId, kind, index) {
|
|
94
|
+
return `${messageId}-${kind}-${index}`;
|
|
95
|
+
}
|
|
96
|
+
export function closeOpenContentSteps(steps, now) {
|
|
97
|
+
for (const step of steps) {
|
|
98
|
+
if (step.kind === "thinking" && !step.complete) {
|
|
99
|
+
step.complete = true;
|
|
100
|
+
step.endedAt ??= now;
|
|
101
|
+
}
|
|
102
|
+
if (step.kind === "text" && !step.complete)
|
|
103
|
+
step.complete = true;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
export function appendDeltaStep(steps, messageId, part, content, now) {
|
|
107
|
+
const last = steps.at(-1);
|
|
108
|
+
if (part === "thinking") {
|
|
109
|
+
if (last?.kind === "thinking" && !last.complete) {
|
|
110
|
+
last.text += content;
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
closeOpenContentSteps(steps, now);
|
|
114
|
+
steps.push({
|
|
115
|
+
kind: "thinking",
|
|
116
|
+
id: stepId(messageId, "thinking", steps.length),
|
|
117
|
+
text: content,
|
|
118
|
+
startedAt: now,
|
|
119
|
+
});
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
if (last?.kind === "text" && !last.complete) {
|
|
123
|
+
last.text += content;
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
closeOpenContentSteps(steps, now);
|
|
127
|
+
steps.push({ kind: "text", id: stepId(messageId, "text", steps.length), text: content });
|
|
128
|
+
}
|
|
129
|
+
export function upsertToolStep(steps, tool, now) {
|
|
130
|
+
const index = steps.findIndex((step) => step.kind === "tool" && step.tool.id === tool.id);
|
|
131
|
+
if (index >= 0) {
|
|
132
|
+
const existing = steps[index];
|
|
133
|
+
if (existing?.kind === "tool")
|
|
134
|
+
existing.tool = mergeToolEvent(existing.tool, tool);
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
closeOpenContentSteps(steps, now);
|
|
138
|
+
steps.push({ kind: "tool", id: tool.id, tool });
|
|
139
|
+
}
|
|
140
|
+
export function applyStoredAgentEventToMessage(message, record, options) {
|
|
141
|
+
const event = record.event;
|
|
142
|
+
const ts = toUnixMs(record.ts);
|
|
143
|
+
message.steps ??= [];
|
|
144
|
+
const steps = message.steps;
|
|
145
|
+
if (event.type === "message_update") {
|
|
146
|
+
const assistantEvent = event.assistantMessageEvent;
|
|
147
|
+
if (assistantEvent.type === "text_delta") {
|
|
148
|
+
appendDeltaStep(steps, message.id, "text", assistantEvent.delta, ts);
|
|
149
|
+
if (options.applyTextDeltas)
|
|
150
|
+
message.text += assistantEvent.delta;
|
|
151
|
+
}
|
|
152
|
+
if (assistantEvent.type === "thinking_delta") {
|
|
153
|
+
appendDeltaStep(steps, message.id, "thinking", assistantEvent.delta, ts);
|
|
154
|
+
if (options.applyThinkingDeltas)
|
|
155
|
+
message.thinking = `${message.thinking ?? ""}${assistantEvent.delta}`;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
if (event.type === "message_end") {
|
|
159
|
+
closeOpenContentSteps(steps, ts);
|
|
160
|
+
if (event.usage)
|
|
161
|
+
message.usage = event.usage;
|
|
162
|
+
if (event.errorMessage) {
|
|
163
|
+
steps.push({ kind: "error", id: stepId(message.id, "error", steps.length), text: event.errorMessage });
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
const tool = toolFromStoredAgentEvent(event, ts);
|
|
167
|
+
if (tool) {
|
|
168
|
+
upsertToolStep(steps, tool, ts);
|
|
169
|
+
const tools = message.tools ?? [];
|
|
170
|
+
const index = tools.findIndex((candidate) => candidate.id === tool.id);
|
|
171
|
+
if (index >= 0) {
|
|
172
|
+
tools[index] = mergeToolEvent(tools[index], tool);
|
|
173
|
+
}
|
|
174
|
+
else {
|
|
175
|
+
tools.push(tool);
|
|
176
|
+
}
|
|
177
|
+
message.tools = tools;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
export function ensureFallbackSteps(message) {
|
|
181
|
+
if (message.steps?.length)
|
|
182
|
+
return;
|
|
183
|
+
const steps = [];
|
|
184
|
+
if (message.thinking || message.thinkingMs != null) {
|
|
185
|
+
const endedAt = message.ts;
|
|
186
|
+
steps.push({
|
|
187
|
+
kind: "thinking",
|
|
188
|
+
id: stepId(message.id, "thinking", steps.length),
|
|
189
|
+
text: message.thinking ?? "",
|
|
190
|
+
startedAt: endedAt - (message.thinkingMs ?? 0),
|
|
191
|
+
endedAt,
|
|
192
|
+
complete: true,
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
for (const tool of message.tools ?? [])
|
|
196
|
+
steps.push({ kind: "tool", id: tool.id, tool });
|
|
197
|
+
if (message.text) {
|
|
198
|
+
steps.push({ kind: "text", id: stepId(message.id, "text", steps.length), text: message.text, complete: true });
|
|
199
|
+
}
|
|
200
|
+
if (steps.length)
|
|
201
|
+
message.steps = steps;
|
|
202
|
+
}
|
|
203
|
+
export function webMessagesFromRecords(config, records, assistantName) {
|
|
204
|
+
const hidden = hiddenWebMessageIds(records);
|
|
205
|
+
const messages = [];
|
|
206
|
+
const messagesById = new Map();
|
|
207
|
+
const pendingAgentEvents = new Map();
|
|
208
|
+
for (const record of records) {
|
|
209
|
+
const message = webMessageFromRecord(config, record, assistantName);
|
|
210
|
+
if (message && hidden.has(message.id))
|
|
211
|
+
continue;
|
|
212
|
+
if (message) {
|
|
213
|
+
messages.push(message);
|
|
214
|
+
messagesById.set(message.id, message);
|
|
215
|
+
const pending = pendingAgentEvents.get(message.id) ?? [];
|
|
216
|
+
for (const pendingRecord of pending) {
|
|
217
|
+
applyStoredAgentEventToMessage(message, pendingRecord, {
|
|
218
|
+
applyTextDeltas: !message.text && !message.silent,
|
|
219
|
+
applyThinkingDeltas: !message.thinking,
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
pendingAgentEvents.delete(message.id);
|
|
223
|
+
}
|
|
224
|
+
if (record.type === "agent_event") {
|
|
225
|
+
if (hidden.has(record.messageId))
|
|
226
|
+
continue;
|
|
227
|
+
const existing = messagesById.get(record.messageId);
|
|
228
|
+
if (existing) {
|
|
229
|
+
applyStoredAgentEventToMessage(existing, record, {
|
|
230
|
+
applyTextDeltas: !existing.silent,
|
|
231
|
+
applyThinkingDeltas: true,
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
else {
|
|
235
|
+
const pending = pendingAgentEvents.get(record.messageId) ?? [];
|
|
236
|
+
pending.push(record);
|
|
237
|
+
pendingAgentEvents.set(record.messageId, pending);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
for (const message of messages)
|
|
242
|
+
ensureFallbackSteps(message);
|
|
243
|
+
return messages;
|
|
244
|
+
}
|
|
245
|
+
export function webHistoryPayload(config, records, assistantName, channelKey, options) {
|
|
246
|
+
const hidden = hiddenWebMessageIds(records);
|
|
247
|
+
let end = records.length;
|
|
248
|
+
if (options.before) {
|
|
249
|
+
let cursorIndex;
|
|
250
|
+
for (let index = records.length - 1; index >= 0; index -= 1) {
|
|
251
|
+
const message = webMessageFromRecord(config, records[index], assistantName);
|
|
252
|
+
if (message && hidden.has(message.id))
|
|
253
|
+
continue;
|
|
254
|
+
if (message?.id === options.before) {
|
|
255
|
+
cursorIndex = index;
|
|
256
|
+
break;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
end = cursorIndex ?? records.length;
|
|
260
|
+
}
|
|
261
|
+
const pageEntries = [];
|
|
262
|
+
let scanIndex = end - 1;
|
|
263
|
+
for (; scanIndex >= 0 && pageEntries.length < options.limit; scanIndex -= 1) {
|
|
264
|
+
const message = webMessageFromRecord(config, records[scanIndex], assistantName);
|
|
265
|
+
if (message && hidden.has(message.id))
|
|
266
|
+
continue;
|
|
267
|
+
if (message)
|
|
268
|
+
pageEntries.push({ message, recordIndex: scanIndex });
|
|
269
|
+
}
|
|
270
|
+
let hasMore = false;
|
|
271
|
+
let eventStart = 0;
|
|
272
|
+
for (; scanIndex >= 0; scanIndex -= 1) {
|
|
273
|
+
const message = webMessageFromRecord(config, records[scanIndex], assistantName);
|
|
274
|
+
if (message && hidden.has(message.id))
|
|
275
|
+
continue;
|
|
276
|
+
if (message) {
|
|
277
|
+
hasMore = true;
|
|
278
|
+
eventStart = scanIndex + 1;
|
|
279
|
+
break;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
const messagesById = new Map();
|
|
283
|
+
for (const entry of pageEntries)
|
|
284
|
+
messagesById.set(entry.message.id, entry);
|
|
285
|
+
for (let index = eventStart; index < end; index += 1) {
|
|
286
|
+
const record = records[index];
|
|
287
|
+
if (record.type !== "agent_event")
|
|
288
|
+
continue;
|
|
289
|
+
if (hidden.has(record.messageId))
|
|
290
|
+
continue;
|
|
291
|
+
const entry = messagesById.get(record.messageId);
|
|
292
|
+
if (!entry)
|
|
293
|
+
continue;
|
|
294
|
+
applyStoredAgentEventToMessage(entry.message, record, {
|
|
295
|
+
applyTextDeltas: index < entry.recordIndex ? !entry.message.text && !entry.message.silent : !entry.message.silent,
|
|
296
|
+
applyThinkingDeltas: index < entry.recordIndex ? !entry.message.thinking : true,
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
const page = pageEntries.reverse().map((entry) => {
|
|
300
|
+
ensureFallbackSteps(entry.message);
|
|
301
|
+
return entry.message;
|
|
302
|
+
});
|
|
303
|
+
return { messages: page, hasMore, channelKey };
|
|
304
|
+
}
|
|
305
|
+
export function webMessageFromRecord(config, record, assistantName) {
|
|
306
|
+
if (!isUserVisibleRuntimeRecord(record))
|
|
307
|
+
return undefined;
|
|
308
|
+
if (record.type === "inbound") {
|
|
309
|
+
const attachmentText = record.attachments
|
|
310
|
+
.map((attachment) => attachmentDerivedText(attachment))
|
|
311
|
+
.filter((text) => !!text)
|
|
312
|
+
.join("\n");
|
|
313
|
+
return {
|
|
314
|
+
id: record.messageId,
|
|
315
|
+
role: "user",
|
|
316
|
+
who: record.authorName || getContactNickname(WEB_USER_NAME),
|
|
317
|
+
text: [record.text, attachmentText].filter(Boolean).join("\n"),
|
|
318
|
+
attachments: webAttachments(config, record.attachments),
|
|
319
|
+
ts: toUnixMs(record.ts),
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
if (record.type === "outbound" && !record.control) {
|
|
323
|
+
return {
|
|
324
|
+
id: record.webMessageId || record.messageIds[0] || `out_${record.recordId}`,
|
|
325
|
+
role: "assistant",
|
|
326
|
+
who: assistantName,
|
|
327
|
+
text: record.text,
|
|
328
|
+
attachments: webAttachments(config, record.attachments),
|
|
329
|
+
thinking: record.thinking,
|
|
330
|
+
thinkingMs: record.thinkingMs,
|
|
331
|
+
silent: record.silent || undefined,
|
|
332
|
+
ts: toUnixMs(record.ts),
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
if (record.type === "runtime" || record.type === "error") {
|
|
336
|
+
return {
|
|
337
|
+
id: `sys_${record.recordId}`,
|
|
338
|
+
role: "system",
|
|
339
|
+
who: "system",
|
|
340
|
+
text: record.type === "runtime" ? record.detail || record.event : record.message,
|
|
341
|
+
ts: toUnixMs(record.ts),
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
return undefined;
|
|
345
|
+
}
|