@qearlyao/familiar 0.2.5 → 0.4.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/HEARTBEAT.md +1 -1
- package/README.md +33 -0
- package/config.example.toml +4 -2
- package/dist/{agent.js → agent/factory.js} +97 -328
- 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/cli.js +45 -15
- package/dist/config/enums.js +35 -0
- package/dist/{config.js → config/index.js} +9 -272
- package/dist/config/interpolate.js +15 -0
- package/dist/config/model-refs.js +11 -0
- package/dist/{config-overrides.js → config/overrides.js} +1 -1
- package/dist/config/readers.js +116 -0
- package/dist/{config-registry.js → config/registry.js} +27 -8
- package/dist/config/sections.js +113 -0
- package/dist/{settings.js → config/settings.js} +5 -2
- package/dist/config/types.js +1 -0
- package/dist/{chat-log.js → conversation/chat-log.js} +16 -4
- package/dist/{contact-note.js → conversation/contact-note.js} +1 -1
- package/dist/conversation/ids.js +11 -0
- package/dist/conversation/owner-identity.js +29 -0
- 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/daemon.js +379 -0
- package/dist/discord/inbound.js +44 -0
- package/dist/discord/send.js +115 -0
- package/dist/discord/turn.js +55 -0
- package/dist/index.js +12 -11
- package/dist/lifecycle/control.js +1 -0
- package/dist/{data-retention.js → lifecycle/data-retention.js} +1 -1
- package/dist/{hot-reload.js → lifecycle/hot-reload.js} +2 -2
- package/dist/{service.js → lifecycle/service.js} +1 -0
- package/dist/media/attachment-limits.js +3 -0
- package/dist/{generated-media.js → media/generated-media.js} +1 -1
- package/dist/{image-gen.js → media/image-gen.js} +2 -2
- package/dist/{inbound-attachments.js → media/inbound-attachments.js} +47 -43
- package/dist/media/media-understanding.js +215 -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/memory/lcm/summarizer.js +1 -1
- package/dist/{added-models.js → models/added-models.js} +1 -1
- package/dist/{persona.js → prompting/persona.js} +1 -1
- package/dist/runtime/agent-core.js +82 -0
- package/dist/{agent-events.js → runtime/agent-events.js} +1 -1
- package/dist/runtime/agent-work-queue.js +55 -0
- package/dist/{runtime.js → runtime/conversation-runtime.js} +91 -43
- package/dist/runtime/runtime-manager.js +51 -0
- package/dist/runtime/scheduler-runner.js +243 -0
- package/dist/{scheduler.js → runtime/scheduler.js} +4 -4
- package/dist/{browser-tools.js → tools/browser-tools.js} +24 -34
- package/dist/util/fs.js +2 -1
- package/dist/web/agent-routes.js +104 -0
- package/dist/web/auth-routes.js +39 -0
- package/dist/web/auth.js +205 -0
- package/dist/web/config-routes.js +55 -0
- package/dist/web/conversation-routes.js +122 -0
- package/dist/web/daemon.js +108 -0
- package/dist/web/diary-routes.js +88 -0
- package/dist/web/errors.js +3 -0
- 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 +348 -0
- package/dist/web/multipart.js +86 -0
- package/dist/web/payloads.js +34 -0
- package/dist/web/request-context.js +25 -0
- package/dist/web/route-helpers.js +9 -0
- package/dist/web/routes.js +37 -0
- package/dist/web/runtime-actions.js +231 -0
- package/dist/web/session-store.js +161 -0
- package/dist/{web-static.js → web/static.js} +19 -14
- package/dist/web/stream.js +78 -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/index.js +152 -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/npm-shrinkwrap.json +319 -201
- package/package.json +6 -4
- package/web/dist/assets/index-C-k4O5Dz.js +6 -0
- package/web/dist/assets/index-Dj-L9nX4.css +2 -0
- package/web/dist/assets/markdown-kaIeGxdv.js +14 -0
- package/web/dist/assets/react-Bi_azaFt.js +9 -0
- package/web/dist/assets/rolldown-runtime-S-ySWqyJ.js +1 -0
- package/web/dist/assets/ui-C12-nN_X.js +51 -0
- package/web/dist/assets/vendor-D1QXMhXm.js +16 -0
- package/web/dist/index.html +11 -3
- package/dist/discord.js +0 -1299
- package/dist/media-understanding.js +0 -120
- package/dist/web-auth.js +0 -111
- package/dist/web-tools.js +0 -941
- package/dist/web.js +0 -1209
- package/web/dist/assets/index-B23WT77N.js +0 -63
- package/web/dist/assets/index-D3MotFzN.css +0 -2
- /package/dist/{control.js → agent/types.js} +0 -0
- /package/dist/{image-derivatives.js → media/image-derivatives.js} +0 -0
- /package/dist/{tts.js → media/tts.js} +0 -0
- /package/dist/{models.js → models/index.js} +0 -0
- /package/dist/{skills.js → prompting/skills.js} +0 -0
- /package/dist/{silent-marker.js → runtime/silent-marker.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,348 @@
|
|
|
1
|
+
import { hiddenWebMessageIds, } from "../conversation/chat-log.js";
|
|
2
|
+
import { getContactNickname } from "../conversation/contact-note.js";
|
|
3
|
+
import { toUnixMs } from "../conversation/ids.js";
|
|
4
|
+
import { publicAttachmentPath } from "../media/generated-media.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
|
+
derivedText: attachmentDerivedText(attachment),
|
|
21
|
+
}));
|
|
22
|
+
}
|
|
23
|
+
export function attachmentDerivedText(attachment) {
|
|
24
|
+
if (attachment.derived?.text?.label === "preview")
|
|
25
|
+
return undefined;
|
|
26
|
+
const text = attachment.derived?.text?.text.trim();
|
|
27
|
+
if (!text)
|
|
28
|
+
return undefined;
|
|
29
|
+
return {
|
|
30
|
+
label: attachment.derived?.text?.label,
|
|
31
|
+
text,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
export function toolError(result) {
|
|
35
|
+
if (typeof result === "string")
|
|
36
|
+
return result;
|
|
37
|
+
if (!isRecord(result))
|
|
38
|
+
return undefined;
|
|
39
|
+
if (typeof result.error === "string")
|
|
40
|
+
return result.error;
|
|
41
|
+
if (typeof result.message === "string")
|
|
42
|
+
return result.message;
|
|
43
|
+
return undefined;
|
|
44
|
+
}
|
|
45
|
+
export function toolFromStoredAgentEvent(event, ts) {
|
|
46
|
+
if (event.type === "tool_execution_start") {
|
|
47
|
+
return {
|
|
48
|
+
id: event.toolCallId,
|
|
49
|
+
name: event.toolName,
|
|
50
|
+
status: "running",
|
|
51
|
+
args: event.args,
|
|
52
|
+
startedAt: ts,
|
|
53
|
+
updatedAt: ts,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
if (event.type === "tool_execution_update") {
|
|
57
|
+
return {
|
|
58
|
+
id: event.toolCallId,
|
|
59
|
+
name: event.toolName,
|
|
60
|
+
status: "running",
|
|
61
|
+
args: event.args,
|
|
62
|
+
partialResult: event.partialResult,
|
|
63
|
+
updatedAt: ts,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
if (event.type === "tool_execution_end") {
|
|
67
|
+
return {
|
|
68
|
+
id: event.toolCallId,
|
|
69
|
+
name: event.toolName,
|
|
70
|
+
status: event.isError ? "error" : "completed",
|
|
71
|
+
result: event.result,
|
|
72
|
+
error: event.isError ? toolError(event.result) : undefined,
|
|
73
|
+
completedAt: ts,
|
|
74
|
+
updatedAt: ts,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
if (event.type === "message_update" && event.assistantMessageEvent.type === "toolcall_end") {
|
|
78
|
+
return {
|
|
79
|
+
id: event.assistantMessageEvent.toolCall.id,
|
|
80
|
+
name: event.assistantMessageEvent.toolCall.name,
|
|
81
|
+
status: "pending",
|
|
82
|
+
args: event.assistantMessageEvent.toolCall.arguments,
|
|
83
|
+
updatedAt: ts,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
return undefined;
|
|
87
|
+
}
|
|
88
|
+
export function mergeToolEvent(existing, patch) {
|
|
89
|
+
const terminal = patch.status === "completed" || patch.status === "error";
|
|
90
|
+
return {
|
|
91
|
+
...existing,
|
|
92
|
+
...patch,
|
|
93
|
+
args: patch.args ?? existing?.args,
|
|
94
|
+
partialResult: terminal ? undefined : (patch.partialResult ?? existing?.partialResult),
|
|
95
|
+
result: patch.result ?? existing?.result,
|
|
96
|
+
error: patch.error ?? existing?.error,
|
|
97
|
+
startedAt: existing?.startedAt ?? patch.startedAt,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
export function stepId(messageId, kind, index) {
|
|
101
|
+
return `${messageId}-${kind}-${index}`;
|
|
102
|
+
}
|
|
103
|
+
export function closeOpenContentSteps(steps, now) {
|
|
104
|
+
for (const step of steps) {
|
|
105
|
+
if (step.kind === "thinking" && !step.complete) {
|
|
106
|
+
step.complete = true;
|
|
107
|
+
step.endedAt ??= now;
|
|
108
|
+
}
|
|
109
|
+
if (step.kind === "text" && !step.complete)
|
|
110
|
+
step.complete = true;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
export function appendDeltaStep(steps, messageId, part, content, now) {
|
|
114
|
+
const last = steps.at(-1);
|
|
115
|
+
if (part === "thinking") {
|
|
116
|
+
if (last?.kind === "thinking" && !last.complete) {
|
|
117
|
+
last.text += content;
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
closeOpenContentSteps(steps, now);
|
|
121
|
+
steps.push({
|
|
122
|
+
kind: "thinking",
|
|
123
|
+
id: stepId(messageId, "thinking", steps.length),
|
|
124
|
+
text: content,
|
|
125
|
+
startedAt: now,
|
|
126
|
+
});
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
if (last?.kind === "text" && !last.complete) {
|
|
130
|
+
last.text += content;
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
closeOpenContentSteps(steps, now);
|
|
134
|
+
steps.push({ kind: "text", id: stepId(messageId, "text", steps.length), text: content });
|
|
135
|
+
}
|
|
136
|
+
export function upsertToolStep(steps, tool, now) {
|
|
137
|
+
const index = steps.findIndex((step) => step.kind === "tool" && step.tool.id === tool.id);
|
|
138
|
+
if (index >= 0) {
|
|
139
|
+
const existing = steps[index];
|
|
140
|
+
if (existing?.kind === "tool")
|
|
141
|
+
existing.tool = mergeToolEvent(existing.tool, tool);
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
closeOpenContentSteps(steps, now);
|
|
145
|
+
steps.push({ kind: "tool", id: tool.id, tool });
|
|
146
|
+
}
|
|
147
|
+
export function applyStoredAgentEventToMessage(message, record, options) {
|
|
148
|
+
const event = record.event;
|
|
149
|
+
const ts = toUnixMs(record.ts);
|
|
150
|
+
message.steps ??= [];
|
|
151
|
+
const steps = message.steps;
|
|
152
|
+
if (event.type === "message_update") {
|
|
153
|
+
const assistantEvent = event.assistantMessageEvent;
|
|
154
|
+
if (assistantEvent.type === "text_delta") {
|
|
155
|
+
appendDeltaStep(steps, message.id, "text", assistantEvent.delta, ts);
|
|
156
|
+
if (options.applyTextDeltas)
|
|
157
|
+
message.text += assistantEvent.delta;
|
|
158
|
+
}
|
|
159
|
+
if (assistantEvent.type === "thinking_delta") {
|
|
160
|
+
appendDeltaStep(steps, message.id, "thinking", assistantEvent.delta, ts);
|
|
161
|
+
if (options.applyThinkingDeltas)
|
|
162
|
+
message.thinking = `${message.thinking ?? ""}${assistantEvent.delta}`;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
if (event.type === "message_end") {
|
|
166
|
+
closeOpenContentSteps(steps, ts);
|
|
167
|
+
if (event.usage)
|
|
168
|
+
message.usage = event.usage;
|
|
169
|
+
if (event.errorMessage) {
|
|
170
|
+
steps.push({ kind: "error", id: stepId(message.id, "error", steps.length), text: event.errorMessage });
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
const tool = toolFromStoredAgentEvent(event, ts);
|
|
174
|
+
if (tool) {
|
|
175
|
+
upsertToolStep(steps, tool, ts);
|
|
176
|
+
const tools = message.tools ?? [];
|
|
177
|
+
const index = tools.findIndex((candidate) => candidate.id === tool.id);
|
|
178
|
+
if (index >= 0) {
|
|
179
|
+
tools[index] = mergeToolEvent(tools[index], tool);
|
|
180
|
+
}
|
|
181
|
+
else {
|
|
182
|
+
tools.push(tool);
|
|
183
|
+
}
|
|
184
|
+
message.tools = tools;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
export function ensureFallbackSteps(message) {
|
|
188
|
+
if (message.steps?.length)
|
|
189
|
+
return;
|
|
190
|
+
const steps = [];
|
|
191
|
+
if (message.thinking || message.thinkingMs != null) {
|
|
192
|
+
const endedAt = message.ts;
|
|
193
|
+
steps.push({
|
|
194
|
+
kind: "thinking",
|
|
195
|
+
id: stepId(message.id, "thinking", steps.length),
|
|
196
|
+
text: message.thinking ?? "",
|
|
197
|
+
startedAt: endedAt - (message.thinkingMs ?? 0),
|
|
198
|
+
endedAt,
|
|
199
|
+
complete: true,
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
for (const tool of message.tools ?? [])
|
|
203
|
+
steps.push({ kind: "tool", id: tool.id, tool });
|
|
204
|
+
if (message.text) {
|
|
205
|
+
steps.push({ kind: "text", id: stepId(message.id, "text", steps.length), text: message.text, complete: true });
|
|
206
|
+
}
|
|
207
|
+
if (steps.length)
|
|
208
|
+
message.steps = steps;
|
|
209
|
+
}
|
|
210
|
+
export function webMessagesFromRecords(config, records, assistantName) {
|
|
211
|
+
const hidden = hiddenWebMessageIds(records);
|
|
212
|
+
const messages = [];
|
|
213
|
+
const messagesById = new Map();
|
|
214
|
+
const pendingAgentEvents = new Map();
|
|
215
|
+
for (const record of records) {
|
|
216
|
+
const message = webMessageFromRecord(config, record, assistantName);
|
|
217
|
+
if (message && hidden.has(message.id))
|
|
218
|
+
continue;
|
|
219
|
+
if (message) {
|
|
220
|
+
messages.push(message);
|
|
221
|
+
messagesById.set(message.id, message);
|
|
222
|
+
const pending = pendingAgentEvents.get(message.id) ?? [];
|
|
223
|
+
for (const pendingRecord of pending) {
|
|
224
|
+
applyStoredAgentEventToMessage(message, pendingRecord, {
|
|
225
|
+
applyTextDeltas: !message.text && !message.silent,
|
|
226
|
+
applyThinkingDeltas: !message.thinking,
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
pendingAgentEvents.delete(message.id);
|
|
230
|
+
}
|
|
231
|
+
if (record.type === "agent_event") {
|
|
232
|
+
if (hidden.has(record.messageId))
|
|
233
|
+
continue;
|
|
234
|
+
const existing = messagesById.get(record.messageId);
|
|
235
|
+
if (existing) {
|
|
236
|
+
applyStoredAgentEventToMessage(existing, record, {
|
|
237
|
+
applyTextDeltas: !existing.silent,
|
|
238
|
+
applyThinkingDeltas: true,
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
else {
|
|
242
|
+
const pending = pendingAgentEvents.get(record.messageId) ?? [];
|
|
243
|
+
pending.push(record);
|
|
244
|
+
pendingAgentEvents.set(record.messageId, pending);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
for (const message of messages)
|
|
249
|
+
ensureFallbackSteps(message);
|
|
250
|
+
return messages;
|
|
251
|
+
}
|
|
252
|
+
export function webHistoryPayload(config, records, assistantName, channelKey, options) {
|
|
253
|
+
const hidden = hiddenWebMessageIds(records);
|
|
254
|
+
let end = records.length;
|
|
255
|
+
if (options.before) {
|
|
256
|
+
let cursorIndex;
|
|
257
|
+
for (let index = records.length - 1; index >= 0; index -= 1) {
|
|
258
|
+
const message = webMessageFromRecord(config, records[index], assistantName);
|
|
259
|
+
if (message && hidden.has(message.id))
|
|
260
|
+
continue;
|
|
261
|
+
if (message?.id === options.before) {
|
|
262
|
+
cursorIndex = index;
|
|
263
|
+
break;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
end = cursorIndex ?? records.length;
|
|
267
|
+
}
|
|
268
|
+
const pageEntries = [];
|
|
269
|
+
let scanIndex = end - 1;
|
|
270
|
+
for (; scanIndex >= 0 && pageEntries.length < options.limit; scanIndex -= 1) {
|
|
271
|
+
const message = webMessageFromRecord(config, records[scanIndex], assistantName);
|
|
272
|
+
if (message && hidden.has(message.id))
|
|
273
|
+
continue;
|
|
274
|
+
if (message)
|
|
275
|
+
pageEntries.push({ message, recordIndex: scanIndex });
|
|
276
|
+
}
|
|
277
|
+
let hasMore = false;
|
|
278
|
+
let eventStart = 0;
|
|
279
|
+
for (; scanIndex >= 0; scanIndex -= 1) {
|
|
280
|
+
const message = webMessageFromRecord(config, records[scanIndex], assistantName);
|
|
281
|
+
if (message && hidden.has(message.id))
|
|
282
|
+
continue;
|
|
283
|
+
if (message) {
|
|
284
|
+
hasMore = true;
|
|
285
|
+
eventStart = scanIndex + 1;
|
|
286
|
+
break;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
const messagesById = new Map();
|
|
290
|
+
for (const entry of pageEntries)
|
|
291
|
+
messagesById.set(entry.message.id, entry);
|
|
292
|
+
for (let index = eventStart; index < end; index += 1) {
|
|
293
|
+
const record = records[index];
|
|
294
|
+
if (record.type !== "agent_event")
|
|
295
|
+
continue;
|
|
296
|
+
if (hidden.has(record.messageId))
|
|
297
|
+
continue;
|
|
298
|
+
const entry = messagesById.get(record.messageId);
|
|
299
|
+
if (!entry)
|
|
300
|
+
continue;
|
|
301
|
+
applyStoredAgentEventToMessage(entry.message, record, {
|
|
302
|
+
applyTextDeltas: index < entry.recordIndex ? !entry.message.text && !entry.message.silent : !entry.message.silent,
|
|
303
|
+
applyThinkingDeltas: index < entry.recordIndex ? !entry.message.thinking : true,
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
const page = pageEntries.reverse().map((entry) => {
|
|
307
|
+
ensureFallbackSteps(entry.message);
|
|
308
|
+
return entry.message;
|
|
309
|
+
});
|
|
310
|
+
return { messages: page, hasMore, channelKey };
|
|
311
|
+
}
|
|
312
|
+
export function webMessageFromRecord(config, record, assistantName) {
|
|
313
|
+
if (!isUserVisibleRuntimeRecord(record))
|
|
314
|
+
return undefined;
|
|
315
|
+
if (record.type === "inbound") {
|
|
316
|
+
return {
|
|
317
|
+
id: record.messageId,
|
|
318
|
+
role: "user",
|
|
319
|
+
who: record.authorName || getContactNickname(WEB_USER_NAME),
|
|
320
|
+
text: record.text,
|
|
321
|
+
attachments: webAttachments(config, record.attachments),
|
|
322
|
+
ts: toUnixMs(record.ts),
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
if (record.type === "outbound" && !record.control) {
|
|
326
|
+
return {
|
|
327
|
+
id: record.webMessageId || record.messageIds[0] || `out_${record.recordId}`,
|
|
328
|
+
role: "assistant",
|
|
329
|
+
who: assistantName,
|
|
330
|
+
text: record.text,
|
|
331
|
+
attachments: webAttachments(config, record.attachments),
|
|
332
|
+
thinking: record.thinking,
|
|
333
|
+
thinkingMs: record.thinkingMs,
|
|
334
|
+
silent: record.silent || undefined,
|
|
335
|
+
ts: toUnixMs(record.ts),
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
if (record.type === "runtime" || record.type === "error") {
|
|
339
|
+
return {
|
|
340
|
+
id: `sys_${record.recordId}`,
|
|
341
|
+
role: "system",
|
|
342
|
+
who: "system",
|
|
343
|
+
text: record.type === "runtime" ? record.detail || record.event : record.message,
|
|
344
|
+
ts: toUnixMs(record.ts),
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
return undefined;
|
|
348
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { MAX_INBOUND_TOTAL_BYTES } from "../media/attachment-limits.js";
|
|
2
|
+
export function isWebUploadAttachment(value) {
|
|
3
|
+
return !!value && typeof value === "object" && Buffer.isBuffer(value.buffer);
|
|
4
|
+
}
|
|
5
|
+
export function isMultipartContentType(contentType) {
|
|
6
|
+
return Array.isArray(contentType)
|
|
7
|
+
? contentType.some((value) => value.includes("multipart/form-data"))
|
|
8
|
+
: contentType.includes("multipart/form-data");
|
|
9
|
+
}
|
|
10
|
+
export async function readRawBody(request, maxBytes) {
|
|
11
|
+
const chunks = [];
|
|
12
|
+
let total = 0;
|
|
13
|
+
for await (const chunk of request) {
|
|
14
|
+
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
15
|
+
total += buffer.length;
|
|
16
|
+
if (total > maxBytes)
|
|
17
|
+
throw new Error("Request body too large");
|
|
18
|
+
chunks.push(buffer);
|
|
19
|
+
}
|
|
20
|
+
return Buffer.concat(chunks);
|
|
21
|
+
}
|
|
22
|
+
export function multipartBoundary(contentType) {
|
|
23
|
+
const header = Array.isArray(contentType) ? contentType.find((value) => value.includes("boundary=")) : contentType;
|
|
24
|
+
const match = header?.match(/boundary=(?:"([^"]+)"|([^;]+))/i);
|
|
25
|
+
if (!match?.[1] && !match?.[2])
|
|
26
|
+
throw new Error("Missing multipart boundary");
|
|
27
|
+
return match[1] ?? match[2] ?? "";
|
|
28
|
+
}
|
|
29
|
+
export function parseContentDisposition(header) {
|
|
30
|
+
const parts = header.split(";").map((part) => part.trim());
|
|
31
|
+
const values = {};
|
|
32
|
+
for (const part of parts.slice(1)) {
|
|
33
|
+
const [key, rawValue] = part.split("=");
|
|
34
|
+
if (!key || rawValue === undefined)
|
|
35
|
+
continue;
|
|
36
|
+
values[key.toLowerCase()] = rawValue.replace(/^"|"$/g, "");
|
|
37
|
+
}
|
|
38
|
+
return values;
|
|
39
|
+
}
|
|
40
|
+
export async function readMultipartBody(request, contentType) {
|
|
41
|
+
const boundary = multipartBoundary(contentType);
|
|
42
|
+
const raw = await readRawBody(request, MAX_INBOUND_TOTAL_BYTES);
|
|
43
|
+
const binary = raw.toString("binary");
|
|
44
|
+
const marker = `--${boundary}`;
|
|
45
|
+
const attachments = [];
|
|
46
|
+
const body = { text: "" };
|
|
47
|
+
for (const section of binary.split(marker).slice(1)) {
|
|
48
|
+
if (!section || section === "--\r\n" || section === "--")
|
|
49
|
+
continue;
|
|
50
|
+
const trimmed = section.replace(/^\r\n/, "").replace(/\r\n--$/, "");
|
|
51
|
+
const headerEnd = trimmed.indexOf("\r\n\r\n");
|
|
52
|
+
if (headerEnd < 0)
|
|
53
|
+
continue;
|
|
54
|
+
const headerText = trimmed.slice(0, headerEnd);
|
|
55
|
+
let contentBinary = trimmed.slice(headerEnd + 4);
|
|
56
|
+
if (contentBinary.endsWith("\r\n"))
|
|
57
|
+
contentBinary = contentBinary.slice(0, -2);
|
|
58
|
+
const headers = Object.fromEntries(headerText.split("\r\n").map((line) => {
|
|
59
|
+
const colon = line.indexOf(":");
|
|
60
|
+
return colon >= 0
|
|
61
|
+
? [line.slice(0, colon).trim().toLowerCase(), line.slice(colon + 1).trim()]
|
|
62
|
+
: [line.toLowerCase(), ""];
|
|
63
|
+
}));
|
|
64
|
+
const disposition = parseContentDisposition(headers["content-disposition"] ?? "");
|
|
65
|
+
const name = disposition.name;
|
|
66
|
+
if (!name)
|
|
67
|
+
continue;
|
|
68
|
+
if (name === "text" || name === "channelKey" || name === "clientId") {
|
|
69
|
+
body[name] = Buffer.from(contentBinary, "binary").toString("utf8");
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
if (name !== "attachments")
|
|
73
|
+
continue;
|
|
74
|
+
const buffer = Buffer.from(contentBinary, "binary");
|
|
75
|
+
if (buffer.length === 0)
|
|
76
|
+
continue;
|
|
77
|
+
attachments.push({
|
|
78
|
+
name: disposition.filename,
|
|
79
|
+
mimeType: headers["content-type"],
|
|
80
|
+
size: buffer.length,
|
|
81
|
+
buffer,
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
body.attachments = attachments;
|
|
85
|
+
return body;
|
|
86
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { supportedThinkingLevels } from "../models/index.js";
|
|
2
|
+
import { isRecord } from "../util/guards.js";
|
|
3
|
+
export function commandArgs(command, args) {
|
|
4
|
+
if (!isRecord(args))
|
|
5
|
+
return "";
|
|
6
|
+
if (command === "model")
|
|
7
|
+
return typeof args.model === "string" ? args.model : "";
|
|
8
|
+
if (command === "thinking")
|
|
9
|
+
return typeof args.level === "string" ? args.level : "";
|
|
10
|
+
if (command === "channel-trigger")
|
|
11
|
+
return typeof args.trigger === "string" ? args.trigger : "";
|
|
12
|
+
return "";
|
|
13
|
+
}
|
|
14
|
+
export function agentSettingsPayload(familiarAgent, channelKey, personaName) {
|
|
15
|
+
const { model } = familiarAgent.resolveChannelModel(channelKey);
|
|
16
|
+
return {
|
|
17
|
+
model: familiarAgent.getModel(channelKey),
|
|
18
|
+
thinking: familiarAgent.getThinkingLevel(channelKey),
|
|
19
|
+
supportedThinking: supportedThinkingLevels(model),
|
|
20
|
+
persona: { name: personaName },
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
export function sessionDto(session) {
|
|
24
|
+
return {
|
|
25
|
+
key: session.key,
|
|
26
|
+
label: session.label,
|
|
27
|
+
service: session.channel.service,
|
|
28
|
+
scope: session.channel.scope,
|
|
29
|
+
channelId: session.channel.channelId,
|
|
30
|
+
channelName: session.channel.channelName,
|
|
31
|
+
threadId: session.channel.threadId,
|
|
32
|
+
isDefault: session.isDefault,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
function normalizeRemoteAddress(address) {
|
|
2
|
+
if (!address)
|
|
3
|
+
return undefined;
|
|
4
|
+
if (address.startsWith("::ffff:"))
|
|
5
|
+
return address.slice("::ffff:".length);
|
|
6
|
+
return address;
|
|
7
|
+
}
|
|
8
|
+
function isLoopbackAddress(address) {
|
|
9
|
+
const normalized = normalizeRemoteAddress(address);
|
|
10
|
+
return normalized === "127.0.0.1" || normalized === "::1" || normalized === "localhost";
|
|
11
|
+
}
|
|
12
|
+
function firstHeaderValue(value) {
|
|
13
|
+
return Array.isArray(value) ? value[0] : value;
|
|
14
|
+
}
|
|
15
|
+
export function requestAuthContext(request, now = Date.now()) {
|
|
16
|
+
const remoteAddress = normalizeRemoteAddress(request.socket.remoteAddress);
|
|
17
|
+
const trustedProxy = isLoopbackAddress(remoteAddress);
|
|
18
|
+
const forwardedFor = trustedProxy ? firstHeaderValue(request.headers["x-forwarded-for"]) : undefined;
|
|
19
|
+
const forwardedProto = trustedProxy ? firstHeaderValue(request.headers["x-forwarded-proto"]) : undefined;
|
|
20
|
+
const clientIp = forwardedFor?.split(",")[0]?.trim() || remoteAddress || "unknown";
|
|
21
|
+
const socket = request.socket;
|
|
22
|
+
const secure = forwardedProto?.split(",")[0]?.trim().toLowerCase() === "https" || socket.encrypted === true;
|
|
23
|
+
const userAgent = firstHeaderValue(request.headers["user-agent"]);
|
|
24
|
+
return { clientIp, secure, now, ...(userAgent ? { userAgent } : {}) };
|
|
25
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { isRecord } from "../util/guards.js";
|
|
2
|
+
export function getChannelKeyFromRequest(url, body) {
|
|
3
|
+
const queryKey = url.searchParams.get("channelKey");
|
|
4
|
+
if (queryKey)
|
|
5
|
+
return queryKey;
|
|
6
|
+
if (isRecord(body) && typeof body.channelKey === "string")
|
|
7
|
+
return body.channelKey;
|
|
8
|
+
return undefined;
|
|
9
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { errorMessage } from "./errors.js";
|
|
2
|
+
import { HttpError, sendJson } from "./http.js";
|
|
3
|
+
import { serveAttachment } from "./static.js";
|
|
4
|
+
export function createWebRouteRegistry(config, auth) {
|
|
5
|
+
const webRoutes = new Map();
|
|
6
|
+
const route = (method, pathname, handler) => {
|
|
7
|
+
webRoutes.set(`${method} ${pathname}`, handler);
|
|
8
|
+
};
|
|
9
|
+
const handleApi = async (request, response, url) => {
|
|
10
|
+
if (!url.pathname.startsWith("/api/web/"))
|
|
11
|
+
return false;
|
|
12
|
+
if (!(await auth.authorize(request, url.pathname))) {
|
|
13
|
+
sendJson(response, 401, { error: "unauthorized" });
|
|
14
|
+
return true;
|
|
15
|
+
}
|
|
16
|
+
try {
|
|
17
|
+
if (request.method === "GET" && url.pathname.startsWith("/api/web/attachments/")) {
|
|
18
|
+
return serveAttachment(config, response, url.pathname, request.headers.range);
|
|
19
|
+
}
|
|
20
|
+
const handler = webRoutes.get(`${request.method} ${url.pathname}`);
|
|
21
|
+
// await is load-bearing: it keeps handler rejections inside this try so the catch maps HttpError to a status.
|
|
22
|
+
if (handler) {
|
|
23
|
+
await handler(request, response, url);
|
|
24
|
+
return true;
|
|
25
|
+
}
|
|
26
|
+
sendJson(response, 404, { error: "not found" });
|
|
27
|
+
return true;
|
|
28
|
+
}
|
|
29
|
+
catch (error) {
|
|
30
|
+
const status = error instanceof HttpError ? error.status : 500;
|
|
31
|
+
const message = errorMessage(error);
|
|
32
|
+
sendJson(response, status, { error: message });
|
|
33
|
+
return true;
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
return { route, handleApi };
|
|
37
|
+
}
|