@qearlyao/familiar 0.2.5 → 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 +7 -8
- 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/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/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
package/dist/agent.js
CHANGED
|
@@ -1,257 +1,24 @@
|
|
|
1
|
-
import { createHash } from "node:crypto";
|
|
2
|
-
import { appendFile, mkdir, readdir, readFile } from "node:fs/promises";
|
|
3
|
-
import { dirname, resolve } from "node:path";
|
|
4
1
|
import { Agent } from "@earendil-works/pi-agent-core";
|
|
5
2
|
import { streamSimple } from "@earendil-works/pi-ai";
|
|
6
|
-
import { createBashTool, createEditTool, createReadTool, createWriteTool } from "@earendil-works/pi-coding-agent";
|
|
7
3
|
import { setAddedModelsPath } from "./added-models.js";
|
|
8
|
-
import {
|
|
4
|
+
import { normalizeProviderPayload } from "./agent/payload-normalizers.js";
|
|
5
|
+
import { assertModelAllowed, deriveSessionId, formatModel, getLastAssistantText, getRequestApiKey, installProviderDebugFilter, isNoisyProviderDebug, logUsage, resolveModelName, userTextMessage, } from "./agent/session-helpers.js";
|
|
6
|
+
import { createFamiliarTools, setReferenceAttachments } from "./agent/tools.js";
|
|
7
|
+
import { loadStoredMessages, writePayloadLog, writeTranscriptLog } from "./agent/transcript-log.js";
|
|
9
8
|
import { setConfigOverridesPath } from "./config-overrides.js";
|
|
10
9
|
import { applyConfigOverridesToConfig } from "./config-registry.js";
|
|
11
10
|
import { createGeneratedMediaSink } from "./generated-media.js";
|
|
12
|
-
import {
|
|
13
|
-
import { assertModelCanAuthenticate, clampConfiguredThinkingLevel, createConfiguredModel, isAllowedModel, isThinkingLevel, parseModelRef, resolveModel, resolveModelApiKey, supportedThinkingLevels, } from "./models.js";
|
|
11
|
+
import { clampConfiguredThinkingLevel, createConfiguredModel, isThinkingLevel, parseModelRef, resolveModel, supportedThinkingLevels, } from "./models.js";
|
|
14
12
|
import { buildSystemPrompt, loadPersona } from "./persona.js";
|
|
15
13
|
import { formatFamiliarSkillsForPrompt, loadFamiliarSkills, logSkillDiagnostics } from "./skills.js";
|
|
16
|
-
import { createTtsTool } from "./tts.js";
|
|
17
|
-
import { isEnoent } from "./util/fs.js";
|
|
18
|
-
import { isRecord } from "./util/guards.js";
|
|
19
|
-
import { createWebTools } from "./web-tools.js";
|
|
20
|
-
const BASH_DESCRIPTION = "run a bash command. defaults to the workspace; absolute paths and `~/...` reach anywhere else. returns stdout and stderr. output truncates to the last 2000 lines or 50KB, whichever hits first; full output lands in a temp file if cut. timeout in seconds optional.";
|
|
21
|
-
const READ_DESCRIPTION = "read a file. paths resolve from the workspace, but absolute paths and `~/...` work too. text and images (jpg, png, gif, webp); images come back as attachments. text output truncates to 2000 lines or 50KB, whichever hits first — use offset and limit for long files, and keep paging until you have what you need.";
|
|
22
|
-
const WRITE_DESCRIPTION = "write a file from scratch or replace it wholesale. creates the file if missing, overwrites if not, and makes parent directories as needed. paths resolve from the workspace; absolute and `~/...` also accepted.";
|
|
23
|
-
const EDIT_DESCRIPTION = "edit a file with exact text replacement. each edits[].oldText must match a unique, non-overlapping slice of the original — overlapping or nested edits are rejected, so merge nearby changes into one entry rather than chaining them. don't pad oldText with large unchanged regions just to connect distant changes. paths resolve from the workspace; absolute and `~/...` also work.";
|
|
24
|
-
function deriveSessionId(workspacePath, sessionKey) {
|
|
25
|
-
const digest = createHash("sha256").update(`${workspacePath}\0${sessionKey}`).digest("hex").slice(0, 32);
|
|
26
|
-
return `familiar-${digest}`;
|
|
27
|
-
}
|
|
28
|
-
function dailyLogPath(dataDir, streamName, now = new Date()) {
|
|
29
|
-
const date = now.toISOString().slice(0, 10);
|
|
30
|
-
return resolve(dataDir, streamName, `${date}.jsonl`);
|
|
31
|
-
}
|
|
32
|
-
async function appendJsonl(path, record) {
|
|
33
|
-
await mkdir(dirname(path), { recursive: true });
|
|
34
|
-
await appendFile(path, `${JSON.stringify(record)}\n`, "utf8");
|
|
35
|
-
}
|
|
36
|
-
function writePayloadLog(config, record) {
|
|
37
|
-
appendJsonl(dailyLogPath(config.workspace.dataDir, "payloads"), record).catch((err) => console.error("payload log write failed", err));
|
|
38
|
-
}
|
|
39
|
-
function writeTranscriptLog(config, record) {
|
|
40
|
-
appendJsonl(dailyLogPath(config.workspace.dataDir, "transcripts"), record).catch((err) => console.error("transcript log write failed", err));
|
|
41
|
-
}
|
|
42
|
-
function clonePayload(payload) {
|
|
43
|
-
if (typeof structuredClone === "function")
|
|
44
|
-
return structuredClone(payload);
|
|
45
|
-
return JSON.parse(JSON.stringify(payload));
|
|
46
|
-
}
|
|
47
|
-
// TODO: remove once pi-ai handles store:false reasoning replay upstream.
|
|
48
|
-
function stripOpenAIStoredReasoningItems(payload, model) {
|
|
49
|
-
if (model.api !== "openai-responses" && model.api !== "azure-openai-responses")
|
|
50
|
-
return payload;
|
|
51
|
-
const nextPayload = clonePayload(payload);
|
|
52
|
-
if (!isRecord(nextPayload))
|
|
53
|
-
return nextPayload;
|
|
54
|
-
const request = nextPayload;
|
|
55
|
-
if (request.store !== false)
|
|
56
|
-
return nextPayload;
|
|
57
|
-
const input = request.input;
|
|
58
|
-
if (!Array.isArray(input))
|
|
59
|
-
return nextPayload;
|
|
60
|
-
request.input = input.filter((item) => {
|
|
61
|
-
if (!item || typeof item !== "object" || Array.isArray(item))
|
|
62
|
-
return true;
|
|
63
|
-
return item.type !== "reasoning";
|
|
64
|
-
});
|
|
65
|
-
return nextPayload;
|
|
66
|
-
}
|
|
67
|
-
function moveAnthropicCacheControlBeforeInjectedMemory(payload, model) {
|
|
68
|
-
if (model.api !== "anthropic-messages")
|
|
69
|
-
return payload;
|
|
70
|
-
if (!isRecord(payload) || !Array.isArray(payload.messages))
|
|
71
|
-
return payload;
|
|
72
|
-
const messages = payload.messages;
|
|
73
|
-
const lastMessage = messages.at(-1);
|
|
74
|
-
if (!isRecord(lastMessage) || lastMessage.role !== "user")
|
|
75
|
-
return payload;
|
|
76
|
-
const content = lastMessage.content;
|
|
77
|
-
if (!Array.isArray(content) || content.length < 2)
|
|
78
|
-
return payload;
|
|
79
|
-
const injectedBlock = content.at(-1);
|
|
80
|
-
const stableBlock = content.at(-2);
|
|
81
|
-
if (!isInjectedMemoryTextBlock(injectedBlock) || !isRecord(stableBlock))
|
|
82
|
-
return payload;
|
|
83
|
-
const cacheControl = injectedBlock.cache_control;
|
|
84
|
-
if (!cacheControl)
|
|
85
|
-
return payload;
|
|
86
|
-
delete injectedBlock.cache_control;
|
|
87
|
-
stableBlock.cache_control = cacheControl;
|
|
88
|
-
return payload;
|
|
89
|
-
}
|
|
90
|
-
function isInjectedMemoryTextBlock(value) {
|
|
91
|
-
if (!isRecord(value) || value.type !== "text" || typeof value.text !== "string")
|
|
92
|
-
return false;
|
|
93
|
-
return value.text.trim().startsWith("<injected_memory>");
|
|
94
|
-
}
|
|
95
|
-
function normalizeProviderPayload(payload, model) {
|
|
96
|
-
return moveAnthropicCacheControlBeforeInjectedMemory(stripOpenAIStoredReasoningItems(payload, model), model);
|
|
97
|
-
}
|
|
98
14
|
export const __agentTest = {
|
|
15
|
+
isNoisyProviderDebug,
|
|
99
16
|
normalizeProviderPayload,
|
|
100
17
|
};
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
return false;
|
|
104
|
-
const record = value;
|
|
105
|
-
return typeof record.ts === "string" && typeof record.sessionId === "string" && !!record.message;
|
|
106
|
-
}
|
|
107
|
-
function isStoredResetRecord(value) {
|
|
108
|
-
if (!value || typeof value !== "object")
|
|
109
|
-
return false;
|
|
110
|
-
const record = value;
|
|
111
|
-
return record.type === "reset" && typeof record.ts === "string" && typeof record.sessionId === "string";
|
|
112
|
-
}
|
|
113
|
-
async function loadStoredMessages(dataDir, sessionId) {
|
|
114
|
-
const transcriptsDir = resolve(dataDir, "transcripts");
|
|
115
|
-
let files;
|
|
116
|
-
try {
|
|
117
|
-
files = await readdir(transcriptsDir);
|
|
118
|
-
}
|
|
119
|
-
catch (error) {
|
|
120
|
-
if (isEnoent(error))
|
|
121
|
-
return [];
|
|
122
|
-
console.error("transcript history read failed", error);
|
|
123
|
-
return [];
|
|
124
|
-
}
|
|
125
|
-
const records = [];
|
|
126
|
-
for (const file of files.filter((entry) => entry.endsWith(".jsonl")).sort()) {
|
|
127
|
-
const path = resolve(transcriptsDir, file);
|
|
128
|
-
let contents;
|
|
129
|
-
try {
|
|
130
|
-
contents = await readFile(path, "utf8");
|
|
131
|
-
}
|
|
132
|
-
catch (error) {
|
|
133
|
-
console.error(`transcript file read failed: ${path}`, error);
|
|
134
|
-
continue;
|
|
135
|
-
}
|
|
136
|
-
for (const [index, line] of contents.split(/\r?\n/).entries()) {
|
|
137
|
-
if (!line.trim())
|
|
138
|
-
continue;
|
|
139
|
-
try {
|
|
140
|
-
const parsed = JSON.parse(line);
|
|
141
|
-
if (!isStoredMessageRecord(parsed) && !isStoredResetRecord(parsed)) {
|
|
142
|
-
console.error(`skipping malformed transcript line: ${path}:${index + 1}`);
|
|
143
|
-
continue;
|
|
144
|
-
}
|
|
145
|
-
if (parsed.sessionId !== sessionId)
|
|
146
|
-
continue;
|
|
147
|
-
records.push(parsed);
|
|
148
|
-
}
|
|
149
|
-
catch (error) {
|
|
150
|
-
console.error(`skipping unparsable transcript line: ${path}:${index + 1}`, error);
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
records.sort((a, b) => a.ts.localeCompare(b.ts));
|
|
155
|
-
let lastResetIndex = -1;
|
|
156
|
-
for (let index = records.length - 1; index >= 0; index--) {
|
|
157
|
-
const record = records[index];
|
|
158
|
-
if (record && "type" in record && record.type === "reset") {
|
|
159
|
-
lastResetIndex = index;
|
|
160
|
-
break;
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
const activeRecords = lastResetIndex >= 0 ? records.slice(lastResetIndex + 1) : records;
|
|
164
|
-
return activeRecords.flatMap((record) => ("message" in record ? [record.message] : []));
|
|
165
|
-
}
|
|
166
|
-
function getRequestApiKey(config, model) {
|
|
167
|
-
const apiKey = resolveModelApiKey(config, model);
|
|
168
|
-
assertModelCanAuthenticate(config, model);
|
|
169
|
-
return apiKey;
|
|
170
|
-
}
|
|
171
|
-
function formatModel(model) {
|
|
172
|
-
return `${model.provider}/${model.id}`;
|
|
173
|
-
}
|
|
174
|
-
function resolveModelName(value, fallback) {
|
|
175
|
-
return value ?? formatModel(fallback);
|
|
176
|
-
}
|
|
177
|
-
function assertModelAllowed(config, ref) {
|
|
178
|
-
if (!isAllowedModel(config, ref))
|
|
179
|
-
throw new Error(`Model is not allowlisted: ${ref.key}`);
|
|
180
|
-
}
|
|
181
|
-
function extractText(message) {
|
|
182
|
-
if (!message || typeof message !== "object")
|
|
183
|
-
return "";
|
|
184
|
-
const record = message;
|
|
185
|
-
if (record.stopReason === "error" && typeof record.errorMessage === "string" && record.errorMessage.trim()) {
|
|
186
|
-
return `Model error: ${record.errorMessage}`;
|
|
187
|
-
}
|
|
188
|
-
if (!("content" in record))
|
|
189
|
-
return "";
|
|
190
|
-
const content = record.content;
|
|
191
|
-
if (typeof content === "string")
|
|
192
|
-
return content;
|
|
193
|
-
if (!Array.isArray(content))
|
|
194
|
-
return "";
|
|
195
|
-
return content
|
|
196
|
-
.filter((item) => {
|
|
197
|
-
return !!item && typeof item === "object" && item.type === "text";
|
|
198
|
-
})
|
|
199
|
-
.map((item) => item.text)
|
|
200
|
-
.join("");
|
|
201
|
-
}
|
|
202
|
-
function getLastAssistantText(agent) {
|
|
203
|
-
for (let i = agent.state.messages.length - 1; i >= 0; i--) {
|
|
204
|
-
const message = agent.state.messages[i];
|
|
205
|
-
if (message.role === "assistant")
|
|
206
|
-
return extractText(message);
|
|
207
|
-
}
|
|
208
|
-
return "";
|
|
209
|
-
}
|
|
210
|
-
function logUsage(event) {
|
|
211
|
-
if (event.type !== "message_end" || event.message.role !== "assistant")
|
|
212
|
-
return;
|
|
213
|
-
const usage = event.message.usage;
|
|
214
|
-
console.log(JSON.stringify({
|
|
215
|
-
type: "usage",
|
|
216
|
-
input: usage.input,
|
|
217
|
-
output: usage.output,
|
|
218
|
-
cacheRead: usage.cacheRead,
|
|
219
|
-
cacheWrite: usage.cacheWrite,
|
|
220
|
-
cost: usage.cost.total,
|
|
221
|
-
}));
|
|
222
|
-
}
|
|
223
|
-
function userTextMessage(text, timestamp = Date.now()) {
|
|
224
|
-
return {
|
|
225
|
-
role: "user",
|
|
226
|
-
content: [{ type: "text", text }],
|
|
227
|
-
timestamp,
|
|
228
|
-
};
|
|
229
|
-
}
|
|
230
|
-
function createFamiliarTools(config, mediaSink, referenceAttachments = () => [], memoryService) {
|
|
231
|
-
const bashTool = createBashTool(config.workspacePath);
|
|
232
|
-
bashTool.description = BASH_DESCRIPTION;
|
|
233
|
-
const readTool = createReadTool(config.workspacePath);
|
|
234
|
-
readTool.description = READ_DESCRIPTION;
|
|
235
|
-
const writeTool = createWriteTool(config.workspacePath);
|
|
236
|
-
writeTool.description = WRITE_DESCRIPTION;
|
|
237
|
-
const editTool = createEditTool(config.workspacePath);
|
|
238
|
-
editTool.description = EDIT_DESCRIPTION;
|
|
239
|
-
return [
|
|
240
|
-
bashTool,
|
|
241
|
-
readTool,
|
|
242
|
-
writeTool,
|
|
243
|
-
editTool,
|
|
244
|
-
createTtsTool(config, mediaSink),
|
|
245
|
-
...(config.imageGen.enabled ? [createImageGenTool(config, mediaSink, { referenceAttachments })] : []),
|
|
246
|
-
...createWebTools(config),
|
|
247
|
-
...createBrowserTools(config, mediaSink),
|
|
248
|
-
...(memoryService?.memoryTools() ?? []),
|
|
249
|
-
];
|
|
250
|
-
}
|
|
251
|
-
function setReferenceAttachments(session, attachments = []) {
|
|
252
|
-
session.referenceAttachments.splice(0, session.referenceAttachments.length, ...attachments);
|
|
253
|
-
}
|
|
18
|
+
const PROVIDER_MAX_RETRIES = 2;
|
|
19
|
+
const PROVIDER_MAX_RETRY_DELAY_MS = 60_000;
|
|
254
20
|
export async function createFamiliarAgent(config, settings, memoryService, options = {}) {
|
|
21
|
+
installProviderDebugFilter();
|
|
255
22
|
setAddedModelsPath(config.workspace.dataDir);
|
|
256
23
|
setConfigOverridesPath(config.workspace.dataDir);
|
|
257
24
|
applyConfigOverridesToConfig(config);
|
|
@@ -269,21 +36,8 @@ export async function createFamiliarAgent(config, settings, memoryService, optio
|
|
|
269
36
|
// activePromptOptions covers the synchronous promptMessage window; skipAmbientMessages
|
|
270
37
|
// tags message identities so followUpMessage's fire-and-forget path also opts out.
|
|
271
38
|
const activePromptOptions = new Map();
|
|
272
|
-
const softStopRequested = new Map();
|
|
273
39
|
const skipAmbientMessages = new WeakSet();
|
|
274
40
|
let reloadInProgress;
|
|
275
|
-
const installSoftStopHook = (sessionKey, agent) => {
|
|
276
|
-
const agentWithLoopConfig = agent;
|
|
277
|
-
const createLoopConfig = agentWithLoopConfig.createLoopConfig?.bind(agent);
|
|
278
|
-
if (!createLoopConfig)
|
|
279
|
-
return;
|
|
280
|
-
agentWithLoopConfig.createLoopConfig = ((options) => {
|
|
281
|
-
return {
|
|
282
|
-
...createLoopConfig(options),
|
|
283
|
-
shouldStopAfterTurn: async () => softStopRequested.get(sessionKey) === true,
|
|
284
|
-
};
|
|
285
|
-
});
|
|
286
|
-
};
|
|
287
41
|
const resolveChannelModel = (sessionKey) => {
|
|
288
42
|
const override = settings.getChannelModel(sessionKey);
|
|
289
43
|
const modelName = resolveModelName(override.value, defaultModel);
|
|
@@ -344,6 +98,8 @@ export async function createFamiliarAgent(config, settings, memoryService, optio
|
|
|
344
98
|
...options,
|
|
345
99
|
apiKey: getRequestApiKey(config, streamModel),
|
|
346
100
|
cacheRetention: config.agent.cacheRetention,
|
|
101
|
+
maxRetries: options?.maxRetries ?? PROVIDER_MAX_RETRIES,
|
|
102
|
+
maxRetryDelayMs: options?.maxRetryDelayMs ?? PROVIDER_MAX_RETRY_DELAY_MS,
|
|
347
103
|
onPayload: (payload, payloadModel) => {
|
|
348
104
|
const requestPayload = normalizeProviderPayload(payload, payloadModel);
|
|
349
105
|
writePayloadLog(config, {
|
|
@@ -381,7 +137,6 @@ export async function createFamiliarAgent(config, settings, memoryService, optio
|
|
|
381
137
|
}
|
|
382
138
|
: undefined,
|
|
383
139
|
});
|
|
384
|
-
installSoftStopHook(sessionKey, agent);
|
|
385
140
|
agent.subscribe((event) => {
|
|
386
141
|
logUsage(event);
|
|
387
142
|
if (event.type === "message_end") {
|
|
@@ -465,25 +220,90 @@ export async function createFamiliarAgent(config, settings, memoryService, optio
|
|
|
465
220
|
};
|
|
466
221
|
}));
|
|
467
222
|
};
|
|
223
|
+
// Shared serialization wrapper for prompt/promptMessage: run after the prior turn
|
|
224
|
+
// settles on the session's promptQueue, keep the stored tail non-rejecting, and run
|
|
225
|
+
// teardown (onTurnEnd → reference reset → enterTurn's exit → unsubscribe) in a fixed
|
|
226
|
+
// order regardless of how the turn ends. enterTurn returns its own cleanup so callers
|
|
227
|
+
// can scope per-turn state (e.g. activePromptOptions) across the exact finally window.
|
|
228
|
+
const runPromptTurn = async (sessionKey, options, eventHandler, dispatch, enterTurn) => {
|
|
229
|
+
const session = await getSession(sessionKey);
|
|
230
|
+
const run = session.promptQueue.then(async () => {
|
|
231
|
+
session.mediaSink.drain();
|
|
232
|
+
setReferenceAttachments(session, options.referenceAttachments);
|
|
233
|
+
const unsubscribe = eventHandler ? session.agent.subscribe((event) => eventHandler(event)) : undefined;
|
|
234
|
+
const exitTurn = enterTurn?.();
|
|
235
|
+
try {
|
|
236
|
+
await dispatch(session);
|
|
237
|
+
}
|
|
238
|
+
finally {
|
|
239
|
+
try {
|
|
240
|
+
await options.onTurnEnd?.();
|
|
241
|
+
}
|
|
242
|
+
catch (error) {
|
|
243
|
+
console.error("turn end callback failed", error);
|
|
244
|
+
}
|
|
245
|
+
finally {
|
|
246
|
+
setReferenceAttachments(session);
|
|
247
|
+
exitTurn?.();
|
|
248
|
+
unsubscribe?.();
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
return {
|
|
252
|
+
text: getLastAssistantText(session.agent),
|
|
253
|
+
attachments: session.mediaSink.drain(),
|
|
254
|
+
};
|
|
255
|
+
});
|
|
256
|
+
session.promptQueue = run.then(() => undefined, () => undefined);
|
|
257
|
+
return run;
|
|
258
|
+
};
|
|
259
|
+
const popLastAssistant = (session, action) => {
|
|
260
|
+
const messages = session.agent.state.messages;
|
|
261
|
+
const message = messages.at(-1);
|
|
262
|
+
if (!message || message.role !== "assistant") {
|
|
263
|
+
throw new Error(`No assistant message to ${action}`);
|
|
264
|
+
}
|
|
265
|
+
if (action === "retry" && message.stopReason === "aborted") {
|
|
266
|
+
throw new Error("Cannot retry an aborted assistant message");
|
|
267
|
+
}
|
|
268
|
+
session.agent.state.messages = messages.slice(0, -1);
|
|
269
|
+
writeTranscriptLog(config, {
|
|
270
|
+
ts: new Date().toISOString(),
|
|
271
|
+
sessionId: session.sessionId,
|
|
272
|
+
type: "supersede",
|
|
273
|
+
messageTimestamp: message.timestamp,
|
|
274
|
+
});
|
|
275
|
+
};
|
|
468
276
|
return {
|
|
469
|
-
abort(sessionKey) {
|
|
277
|
+
async abort(sessionKey) {
|
|
470
278
|
const session = sessions.get(sessionKey);
|
|
471
|
-
|
|
472
|
-
|
|
279
|
+
if (!session)
|
|
280
|
+
return;
|
|
281
|
+
try {
|
|
282
|
+
const resolved = await session;
|
|
473
283
|
resolved.agent.abort();
|
|
474
284
|
resolved.agent.clearAllQueues();
|
|
475
|
-
|
|
476
|
-
|
|
285
|
+
await resolved.agent.waitForIdle();
|
|
286
|
+
}
|
|
287
|
+
catch (error) {
|
|
288
|
+
console.error(`failed to abort familiar session ${sessionKey}`, error);
|
|
289
|
+
}
|
|
290
|
+
},
|
|
291
|
+
async retryLastAssistant(sessionKey, eventHandler, options = {}) {
|
|
292
|
+
return runPromptTurn(sessionKey, options, eventHandler, async (session) => {
|
|
293
|
+
popLastAssistant(session, "retry");
|
|
294
|
+
await session.agent.continue();
|
|
295
|
+
});
|
|
477
296
|
},
|
|
478
|
-
|
|
479
|
-
|
|
297
|
+
async deleteLastAssistant(sessionKey) {
|
|
298
|
+
const session = await getSession(sessionKey);
|
|
299
|
+
await session.promptQueue;
|
|
300
|
+
popLastAssistant(session, "delete");
|
|
480
301
|
},
|
|
481
302
|
async reset(sessionKey) {
|
|
482
303
|
const existing = sessions.get(sessionKey);
|
|
483
304
|
if (!existing)
|
|
484
305
|
return;
|
|
485
306
|
const session = await existing;
|
|
486
|
-
softStopRequested.set(sessionKey, false);
|
|
487
307
|
resetSession(session);
|
|
488
308
|
},
|
|
489
309
|
async reload() {
|
|
@@ -577,74 +397,23 @@ export async function createFamiliarAgent(config, settings, memoryService, optio
|
|
|
577
397
|
return `Thinking set to ${clamped}${suffix} for this channel\nSupported: ${supportedThinkingLevels(model).join(", ")}`;
|
|
578
398
|
},
|
|
579
399
|
async prompt(sessionKey, input, imagesOrOnEvent, onEvent, options = {}) {
|
|
580
|
-
const session = await getSession(sessionKey);
|
|
581
400
|
const images = Array.isArray(imagesOrOnEvent) ? imagesOrOnEvent : undefined;
|
|
582
401
|
const eventHandler = Array.isArray(imagesOrOnEvent) ? onEvent : imagesOrOnEvent;
|
|
583
|
-
|
|
584
|
-
softStopRequested.set(sessionKey, false);
|
|
585
|
-
session.mediaSink.drain();
|
|
586
|
-
setReferenceAttachments(session, options.referenceAttachments);
|
|
587
|
-
const unsubscribe = eventHandler ? session.agent.subscribe((event) => eventHandler(event)) : undefined;
|
|
588
|
-
try {
|
|
589
|
-
await session.agent.prompt(input, images);
|
|
590
|
-
}
|
|
591
|
-
finally {
|
|
592
|
-
try {
|
|
593
|
-
await options.onTurnEnd?.();
|
|
594
|
-
}
|
|
595
|
-
catch (error) {
|
|
596
|
-
console.error("turn end callback failed", error);
|
|
597
|
-
}
|
|
598
|
-
finally {
|
|
599
|
-
setReferenceAttachments(session);
|
|
600
|
-
unsubscribe?.();
|
|
601
|
-
}
|
|
602
|
-
}
|
|
603
|
-
return {
|
|
604
|
-
text: getLastAssistantText(session.agent),
|
|
605
|
-
attachments: session.mediaSink.drain(),
|
|
606
|
-
};
|
|
607
|
-
});
|
|
608
|
-
session.promptQueue = run.then(() => undefined, () => undefined);
|
|
609
|
-
return run;
|
|
402
|
+
return runPromptTurn(sessionKey, options, eventHandler, (session) => session.agent.prompt(input, images));
|
|
610
403
|
},
|
|
611
404
|
async promptMessage(sessionKey, message, onEvent, options = {}) {
|
|
612
|
-
|
|
613
|
-
const run = session.promptQueue.then(async () => {
|
|
614
|
-
softStopRequested.set(sessionKey, false);
|
|
615
|
-
session.mediaSink.drain();
|
|
616
|
-
setReferenceAttachments(session, options.referenceAttachments);
|
|
617
|
-
const unsubscribe = onEvent ? session.agent.subscribe((event) => onEvent(event)) : undefined;
|
|
405
|
+
return runPromptTurn(sessionKey, options, onEvent, (session) => session.agent.prompt(message), () => {
|
|
618
406
|
const previousOptions = activePromptOptions.get(sessionKey);
|
|
619
407
|
activePromptOptions.set(sessionKey, options);
|
|
620
408
|
if (options.skipAmbient)
|
|
621
409
|
skipAmbientMessages.add(message);
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
await options.onTurnEnd?.();
|
|
628
|
-
}
|
|
629
|
-
catch (error) {
|
|
630
|
-
console.error("turn end callback failed", error);
|
|
631
|
-
}
|
|
632
|
-
finally {
|
|
633
|
-
setReferenceAttachments(session);
|
|
634
|
-
if (previousOptions)
|
|
635
|
-
activePromptOptions.set(sessionKey, previousOptions);
|
|
636
|
-
else
|
|
637
|
-
activePromptOptions.delete(sessionKey);
|
|
638
|
-
unsubscribe?.();
|
|
639
|
-
}
|
|
640
|
-
}
|
|
641
|
-
return {
|
|
642
|
-
text: getLastAssistantText(session.agent),
|
|
643
|
-
attachments: session.mediaSink.drain(),
|
|
410
|
+
return () => {
|
|
411
|
+
if (previousOptions)
|
|
412
|
+
activePromptOptions.set(sessionKey, previousOptions);
|
|
413
|
+
else
|
|
414
|
+
activePromptOptions.delete(sessionKey);
|
|
644
415
|
};
|
|
645
416
|
});
|
|
646
|
-
session.promptQueue = run.then(() => undefined, () => undefined);
|
|
647
|
-
return run;
|
|
648
417
|
},
|
|
649
418
|
steer(sessionKey, input) {
|
|
650
419
|
const session = sessions.get(sessionKey);
|
package/dist/browser-tools.js
CHANGED
|
@@ -142,6 +142,7 @@ function defaultBrowserRunner() {
|
|
|
142
142
|
reject(new Error(`Browser command timed out after ${options.timeoutMs}ms.`));
|
|
143
143
|
}, options.timeoutMs);
|
|
144
144
|
const abort = () => {
|
|
145
|
+
clearTimeout(timeout);
|
|
145
146
|
child.kill("SIGTERM");
|
|
146
147
|
reject(new Error("Browser command aborted."));
|
|
147
148
|
};
|
|
@@ -608,7 +609,7 @@ async function loadSiteCommand(site, command, config, runner, signal) {
|
|
|
608
609
|
throw new Error(`OpenCLI site command is not available: ${site} ${command}`);
|
|
609
610
|
return info;
|
|
610
611
|
}
|
|
611
|
-
function
|
|
612
|
+
function resolveSiteCommand(input, config) {
|
|
612
613
|
const site = stringArg(input.site);
|
|
613
614
|
const command = stringArg(input.command);
|
|
614
615
|
if (!site || !command)
|
|
@@ -616,6 +617,10 @@ function buildSiteArgs(input, config, commandInfo) {
|
|
|
616
617
|
assertSafeName(site, "browser.site");
|
|
617
618
|
assertSafeName(command, "browser.command");
|
|
618
619
|
assertSiteAllowed(config, site);
|
|
620
|
+
return { site, command };
|
|
621
|
+
}
|
|
622
|
+
function buildSiteArgs(input, config, commandInfo) {
|
|
623
|
+
const { site, command } = resolveSiteCommand(input, config);
|
|
619
624
|
if (commandInfo.access === "write" && !config.browser.readWrite) {
|
|
620
625
|
throw new Error(`OpenCLI write command is disabled until browser.read_write is true: ${site} ${command}`);
|
|
621
626
|
}
|
|
@@ -648,13 +653,7 @@ function buildSiteArgs(input, config, commandInfo) {
|
|
|
648
653
|
return args;
|
|
649
654
|
}
|
|
650
655
|
async function buildSiteRunSpec(input, config, runner, signal) {
|
|
651
|
-
const site =
|
|
652
|
-
const command = stringArg(input.command);
|
|
653
|
-
if (!site || !command)
|
|
654
|
-
throw new Error("browser site mode requires site and command.");
|
|
655
|
-
assertSafeName(site, "browser.site");
|
|
656
|
-
assertSafeName(command, "browser.command");
|
|
657
|
-
assertSiteAllowed(config, site);
|
|
656
|
+
const { site, command } = resolveSiteCommand(input, config);
|
|
658
657
|
const commandInfo = await loadSiteCommand(site, command, config, runner, signal);
|
|
659
658
|
return openCliSpec(config, buildSiteArgs(input, config, commandInfo));
|
|
660
659
|
}
|
package/dist/chat-log.js
CHANGED
|
@@ -1,6 +1,15 @@
|
|
|
1
1
|
import { appendFile, mkdir, open, readdir, readFile, rm } from "node:fs/promises";
|
|
2
2
|
import { dirname, resolve } from "node:path";
|
|
3
3
|
import { isEnoent, readFileOrNull } from "./util/fs.js";
|
|
4
|
+
export function hiddenWebMessageIds(records) {
|
|
5
|
+
return new Set(records.flatMap((record) => {
|
|
6
|
+
if (record.type === "assistant_retry")
|
|
7
|
+
return [record.oldMessageId];
|
|
8
|
+
if (record.type === "message_delete")
|
|
9
|
+
return [record.messageId];
|
|
10
|
+
return [];
|
|
11
|
+
}));
|
|
12
|
+
}
|
|
4
13
|
function sanitizeSegment(value) {
|
|
5
14
|
return value.replace(/[^A-Za-z0-9._=-]+/g, "_").slice(0, 120) || "unknown";
|
|
6
15
|
}
|
|
@@ -62,10 +71,13 @@ export function createChatLog(config, channel) {
|
|
|
62
71
|
return [];
|
|
63
72
|
throw error;
|
|
64
73
|
}
|
|
65
|
-
const
|
|
66
|
-
|
|
74
|
+
const jsonlFiles = files.filter((entry) => entry.endsWith(".jsonl")).sort();
|
|
75
|
+
const contents = await Promise.all(jsonlFiles.map(async (file) => {
|
|
67
76
|
const filePath = resolve(dir, file);
|
|
68
|
-
|
|
77
|
+
return { filePath, content: await readFile(filePath, "utf8") };
|
|
78
|
+
}));
|
|
79
|
+
const records = [];
|
|
80
|
+
for (const { filePath, content } of contents) {
|
|
69
81
|
for (const [index, line] of content.split(/\r?\n/).entries()) {
|
|
70
82
|
if (!line.trim())
|
|
71
83
|
continue;
|
package/dist/cli.js
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { existsSync } from "node:fs";
|
|
3
|
-
import { copyFile, cp, mkdir } from "node:fs/promises";
|
|
3
|
+
import { copyFile, cp, mkdir, readFile } from "node:fs/promises";
|
|
4
4
|
import { homedir } from "node:os";
|
|
5
5
|
import { dirname, resolve } from "node:path";
|
|
6
6
|
import { fileURLToPath } from "node:url";
|
|
7
7
|
import { config as loadDotenv } from "dotenv";
|
|
8
8
|
import { createFamiliarAgent } from "./agent.js";
|
|
9
|
+
import { createAgentCore } from "./agent-core.js";
|
|
9
10
|
import { loadConfig } from "./config.js";
|
|
10
11
|
import { runDataRetention } from "./data-retention.js";
|
|
11
12
|
import { startDiscordDaemon } from "./discord.js";
|
|
@@ -13,6 +14,7 @@ import { cleanupGeneratedAttachments } from "./generated-media.js";
|
|
|
13
14
|
import { startWorkspaceHotReload } from "./hot-reload.js";
|
|
14
15
|
import { memoryHelp, runMemoryOperator } from "./memory/operator.js";
|
|
15
16
|
import { createMemoryService } from "./memory/service.js";
|
|
17
|
+
import { loadOwnerIdentity } from "./owner-identity.js";
|
|
16
18
|
import { formatServiceResult, installService, serviceStatus, uninstallService, upgradeFamiliar } from "./service.js";
|
|
17
19
|
import { loadSettingsStore } from "./settings.js";
|
|
18
20
|
import { startWebDaemon } from "./web.js";
|
|
@@ -76,6 +78,11 @@ function isMemoryHelp(args) {
|
|
|
76
78
|
const command = args[0];
|
|
77
79
|
return !command || command === "help" || command === "--help";
|
|
78
80
|
}
|
|
81
|
+
async function packageVersion() {
|
|
82
|
+
const raw = await readFile(resolve(PROJECT_ROOT, "package.json"), "utf8");
|
|
83
|
+
const packageJson = JSON.parse(raw);
|
|
84
|
+
return typeof packageJson.version === "string" ? packageJson.version : "0.0.0";
|
|
85
|
+
}
|
|
79
86
|
async function initWorkspace(workspaceInput) {
|
|
80
87
|
const workspacePath = resolveWorkspaceInput(workspaceInput);
|
|
81
88
|
await mkdir(workspacePath, { recursive: true });
|
|
@@ -119,6 +126,7 @@ async function runDaemon(workspaceInput) {
|
|
|
119
126
|
memoryService.watchDiaries();
|
|
120
127
|
const familiarAgent = await createFamiliarAgent(config, settings, memoryService, { reloadConfig });
|
|
121
128
|
const hotReload = startWorkspaceHotReload({ workspacePath: config.workspacePath, familiarAgent });
|
|
129
|
+
const agentCore = createAgentCore({ config, familiarAgent, memoryService });
|
|
122
130
|
let stopping = false;
|
|
123
131
|
let discordDaemon;
|
|
124
132
|
let webDaemon;
|
|
@@ -129,6 +137,7 @@ async function runDaemon(workspaceInput) {
|
|
|
129
137
|
console.log("Stopping familiar");
|
|
130
138
|
hotReload.close();
|
|
131
139
|
await Promise.all([webDaemon?.stop(), discordDaemon?.stop()]);
|
|
140
|
+
await agentCore.stop();
|
|
132
141
|
memoryService.close();
|
|
133
142
|
process.exit(exitCode);
|
|
134
143
|
};
|
|
@@ -137,10 +146,21 @@ async function runDaemon(workspaceInput) {
|
|
|
137
146
|
setTimeout(() => void stop(75), RESTART_EXIT_DELAY_MS);
|
|
138
147
|
return "Restart requested. If Familiar is managed by launchd/systemd, it should come back automatically; otherwise run familiar run again.";
|
|
139
148
|
};
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
149
|
+
const identity = await loadOwnerIdentity(config.workspace.dataDir);
|
|
150
|
+
const token = config.discord.token;
|
|
151
|
+
if (!identity && !token) {
|
|
152
|
+
throw new Error("First-time setup needs a DISCORD_TOKEN to establish owner identity. Set DISCORD_TOKEN and run again.");
|
|
153
|
+
}
|
|
154
|
+
// The scheduler starts with the first session source to arrive: the cached identity
|
|
155
|
+
// here, or the live Discord connection below when there is no cache yet.
|
|
156
|
+
if (identity)
|
|
157
|
+
await agentCore.useCachedIdentity(identity);
|
|
158
|
+
webDaemon = await startWebDaemon(config, familiarAgent, agentCore, { restart: requestRestart });
|
|
159
|
+
if (token) {
|
|
160
|
+
discordDaemon = startDiscordDaemon(config, token, familiarAgent, settings, memoryService, agentCore, {
|
|
161
|
+
restart: requestRestart,
|
|
162
|
+
});
|
|
163
|
+
}
|
|
144
164
|
console.log(`familiar running for workspace ${config.workspacePath}`);
|
|
145
165
|
console.log("agent sessions are created per channel");
|
|
146
166
|
console.log(`settings=${settings.path}`);
|
|
@@ -151,6 +171,8 @@ async function runDaemon(workspaceInput) {
|
|
|
151
171
|
function usage() {
|
|
152
172
|
return [
|
|
153
173
|
"Usage:",
|
|
174
|
+
" familiar --help",
|
|
175
|
+
" familiar --version",
|
|
154
176
|
" familiar init [workspace]",
|
|
155
177
|
" familiar run [workspace]",
|
|
156
178
|
" familiar memory [workspace] <subcommand>",
|
|
@@ -164,6 +186,14 @@ function usage() {
|
|
|
164
186
|
}
|
|
165
187
|
async function main() {
|
|
166
188
|
const [, , command, workspace, ...rest] = process.argv;
|
|
189
|
+
if (!command || command === "--help" || command === "-h") {
|
|
190
|
+
console.log(usage());
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
if (command === "--version" || command === "-v") {
|
|
194
|
+
console.log(await packageVersion());
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
167
197
|
if (command === "init") {
|
|
168
198
|
await initWorkspace(workspace);
|
|
169
199
|
return;
|
|
@@ -200,7 +230,7 @@ async function main() {
|
|
|
200
230
|
return;
|
|
201
231
|
}
|
|
202
232
|
if (command === "upgrade") {
|
|
203
|
-
console.log("Upgrading @qearlyao/familiar globally...");
|
|
233
|
+
console.log("Upgrading @qearlyao/familiar and OpenCLI globally...");
|
|
204
234
|
await upgradeFamiliar(resolveWorkspaceInput(workspace));
|
|
205
235
|
return;
|
|
206
236
|
}
|