@coffer-org/server 7.1.0 → 7.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/dist/auth-api.d.ts +4 -0
- package/dist/auth-api.js +53 -0
- package/dist/auth-store.d.ts +2 -0
- package/dist/auth-store.js +1 -0
- package/dist/background-scheduler.d.ts +1 -0
- package/dist/background-scheduler.js +2 -1
- package/dist/entity-schema.d.ts +2 -0
- package/dist/entity-schema.js +26 -0
- package/dist/identity-link.d.ts +15 -0
- package/dist/identity-link.js +76 -0
- package/dist/index.js +8 -1
- package/dist/mcp-contract/schema.d.ts +34 -1
- package/dist/mcp-contract/schema.js +32 -9
- package/dist/mcp-contract/tools.js +3 -1
- package/dist/mcp-http.js +7 -3
- package/dist/mcp-tools.d.ts +7 -5
- package/dist/mcp-tools.js +95 -91
- package/dist/media/image.d.ts +23 -0
- package/dist/media/image.js +103 -0
- package/dist/media/index.d.ts +1 -0
- package/dist/media/index.js +1 -0
- package/dist/migrations.js +1 -1
- package/dist/orchestrator/agent-capabilities.d.ts +2 -2
- package/dist/orchestrator/agent-capabilities.js +3 -3
- package/dist/orchestrator/allow.d.ts +1 -16
- package/dist/orchestrator/allow.js +3 -53
- package/dist/orchestrator/config.js +0 -1
- package/dist/orchestrator/context-facts.d.ts +27 -0
- package/dist/orchestrator/context-facts.js +89 -0
- package/dist/orchestrator/conversation-access.d.ts +9 -0
- package/dist/orchestrator/conversation-access.js +12 -0
- package/dist/orchestrator/environment.d.ts +1 -0
- package/dist/orchestrator/environment.js +10 -0
- package/dist/orchestrator/file-inspection.d.ts +2 -2
- package/dist/orchestrator/file-inspection.js +41 -19
- package/dist/orchestrator/index.d.ts +16 -9
- package/dist/orchestrator/index.js +14 -7
- package/dist/orchestrator/live-message.d.ts +7 -4
- package/dist/orchestrator/live-message.js +48 -31
- package/dist/orchestrator/pipeline.d.ts +25 -4
- package/dist/orchestrator/pipeline.js +214 -94
- package/dist/orchestrator/registry.d.ts +4 -2
- package/dist/orchestrator/registry.js +10 -1
- package/dist/orchestrator/system-areas.d.ts +12 -0
- package/dist/orchestrator/system-areas.js +63 -0
- package/dist/orchestrator/system-capabilities.js +1 -1
- package/dist/orchestrator/turn-context.d.ts +18 -0
- package/dist/orchestrator/turn-context.js +39 -0
- package/dist/orchestrator/types.d.ts +106 -44
- package/dist/plugin-hooks.d.ts +26 -0
- package/dist/plugin-http-mounts.d.ts +18 -0
- package/dist/plugin-http-mounts.js +94 -0
- package/dist/plugin-runtime.js +2 -2
- package/dist/records-api.js +15 -3
- package/dist/system-settings.js +0 -1
- package/dist/thread-state.d.ts +14 -0
- package/dist/thread-state.js +71 -11
- package/dist/thread-store.d.ts +5 -3
- package/dist/thread-store.js +12 -9
- package/dist/turn-gate.d.ts +8 -0
- package/dist/turn-gate.js +39 -0
- package/package.json +7 -2
|
@@ -1,23 +1,32 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { makeThrottle } from "./allow.js";
|
|
2
2
|
import { loadAgentId, loadGatePolicy } from "./config.js";
|
|
3
|
-
import {
|
|
3
|
+
import { assembleSystem, mergeAreas } from "./system-areas.js";
|
|
4
4
|
import { attachmentMaterializer } from "./attachments.js";
|
|
5
5
|
import { makeAttachmentCapabilities } from "./agent-capabilities.js";
|
|
6
6
|
import { makeSystemCapabilities } from "./system-capabilities.js";
|
|
7
|
-
import { makeSuggestionCapabilities } from "./suggestion-capabilities.js";
|
|
8
7
|
import { resolveAgent } from "./registry.js";
|
|
9
8
|
import { recordDiagnostic } from "./diagnostics.js";
|
|
10
9
|
import { getLogger } from '@coffer-org/sdk/logger';
|
|
10
|
+
import { findLinkedUser } from "../identity-link.js";
|
|
11
|
+
import { buildContextFacts } from "./context-facts.js";
|
|
12
|
+
import { systemTimeZone } from "./environment.js";
|
|
13
|
+
import { discoverPlugins } from "../plugin-discovery.js";
|
|
14
|
+
import { SchemaCache } from "../mcp-contract/schema.js";
|
|
15
|
+
import { LocalClient } from "../mcp-local.js";
|
|
16
|
+
import { loadComposedLocales } from "../locale-registry.js";
|
|
17
|
+
import { buildDomainAreas } from "../mcp-tools.js";
|
|
18
|
+
import { getActiveRegistry } from "../registry-context.js";
|
|
11
19
|
const log = getLogger('orchestrator');
|
|
12
20
|
let logDb;
|
|
13
21
|
export function setLogDb(db) {
|
|
14
22
|
logDb = db;
|
|
15
23
|
}
|
|
24
|
+
export const NOTICE_THROTTLE = { max: 5, windowMs: 60_000 };
|
|
16
25
|
const throttleByConnector = new Map();
|
|
17
26
|
function passThrottle(connectorId) {
|
|
18
27
|
let t = throttleByConnector.get(connectorId);
|
|
19
28
|
if (!t) {
|
|
20
|
-
t = makeThrottle(
|
|
29
|
+
t = makeThrottle(NOTICE_THROTTLE.max, NOTICE_THROTTLE.windowMs);
|
|
21
30
|
throttleByConnector.set(connectorId, t);
|
|
22
31
|
}
|
|
23
32
|
return t;
|
|
@@ -32,14 +41,75 @@ function cachedAgentBase() {
|
|
|
32
41
|
}
|
|
33
42
|
return cachedBase;
|
|
34
43
|
}
|
|
35
|
-
|
|
44
|
+
let cachedWorld;
|
|
45
|
+
export function liveWorld() {
|
|
46
|
+
if (!cachedWorld) {
|
|
47
|
+
cachedWorld = (async () => {
|
|
48
|
+
const [plugins, locales] = await Promise.all([discoverPlugins(), loadComposedLocales()]);
|
|
49
|
+
const shelves = await new SchemaCache(new LocalClient(), locales).index();
|
|
50
|
+
const pluginIds = new Set(plugins.map((p) => p.id));
|
|
51
|
+
const shelfKeys = new Set(shelves.map((s) => `${s.library}/${s.shelf}`));
|
|
52
|
+
let libraryIds;
|
|
53
|
+
try {
|
|
54
|
+
libraryIds = new Set(getActiveRegistry().libraries.map((v) => v.meta.id));
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
libraryIds = new Set(shelves.map((s) => s.library));
|
|
58
|
+
}
|
|
59
|
+
return {
|
|
60
|
+
hasPlugin: (id) => pluginIds.has(id),
|
|
61
|
+
hasLibrary: (id) => libraryIds.has(id),
|
|
62
|
+
hasShelf: (library, shelf) => shelfKeys.has(`${library}/${shelf}`),
|
|
63
|
+
};
|
|
64
|
+
})();
|
|
65
|
+
}
|
|
66
|
+
return cachedWorld;
|
|
67
|
+
}
|
|
68
|
+
let cachedDomain;
|
|
69
|
+
function cachedDomainAreas() {
|
|
70
|
+
if (!cachedDomain)
|
|
71
|
+
cachedDomain = buildDomainAreas();
|
|
72
|
+
return cachedDomain;
|
|
73
|
+
}
|
|
74
|
+
function foldContextFacts(messages) {
|
|
75
|
+
const acc = new Map();
|
|
76
|
+
for (const m of messages) {
|
|
77
|
+
for (const fact of m.context ?? []) {
|
|
78
|
+
if (fact.name === 'cleared')
|
|
79
|
+
acc.delete(fact.value);
|
|
80
|
+
else
|
|
81
|
+
acc.set(fact.name, fact);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return [...acc.values()];
|
|
85
|
+
}
|
|
86
|
+
const COFFER_USER_ID = /^[1-9][0-9]*$/;
|
|
87
|
+
const storeLookup = async (id) => (await import("../auth-store.js")).findUserById(id);
|
|
88
|
+
export async function defaultResolveUser(id, lookup = storeLookup) {
|
|
89
|
+
if (!COFFER_USER_ID.test(id) || !Number.isSafeInteger(Number(id)))
|
|
90
|
+
return null;
|
|
91
|
+
const user = await lookup(Number(id));
|
|
92
|
+
return user && !user.disabled ? user : null;
|
|
93
|
+
}
|
|
94
|
+
export async function defaultResolveLinkedUser(connectorId, externalId, lookup = storeLookup) {
|
|
95
|
+
const userId = await findLinkedUser(connectorId, externalId);
|
|
96
|
+
if (userId === null)
|
|
97
|
+
return null;
|
|
98
|
+
return await lookup(userId);
|
|
99
|
+
}
|
|
100
|
+
export async function resolveSpeaker(connectorId, sender, deps) {
|
|
101
|
+
return sender.idKind === 'coffer-user'
|
|
102
|
+
? await (deps?.resolveUser ?? defaultResolveUser)(sender.id)
|
|
103
|
+
: await (deps?.resolveLinkedUser ?? defaultResolveLinkedUser)(connectorId, sender.id);
|
|
104
|
+
}
|
|
105
|
+
function openSink(connector, envelope) {
|
|
36
106
|
try {
|
|
37
|
-
return connector.
|
|
107
|
+
return connector.open(envelope);
|
|
38
108
|
}
|
|
39
109
|
catch (err) {
|
|
40
110
|
const message = err instanceof Error ? err.message : String(err);
|
|
41
|
-
log.error(`connector error in
|
|
42
|
-
recordDiagnostic('error', 'connector.
|
|
111
|
+
log.error(`connector error in open: ${message}`);
|
|
112
|
+
recordDiagnostic('error', 'connector.open', message);
|
|
43
113
|
return null;
|
|
44
114
|
}
|
|
45
115
|
}
|
|
@@ -54,62 +124,83 @@ async function safeCall(fn, fallback, label) {
|
|
|
54
124
|
return fallback;
|
|
55
125
|
}
|
|
56
126
|
}
|
|
57
|
-
export async function handleIncoming(connector,
|
|
127
|
+
export async function handleIncoming(connector, turn, deps) {
|
|
58
128
|
const policy = deps?.policy ?? (await loadGatePolicy());
|
|
59
|
-
const selectedAgentId =
|
|
129
|
+
const selectedAgentId = turn.agentId ?? policy.agentId ?? (deps?.runAgent ? undefined : await loadAgentId());
|
|
60
130
|
const runtime = deps?.runAgent ? undefined : resolveAgent(selectedAgentId);
|
|
61
131
|
const agent = deps?.runAgent ?? runtime.run.bind(runtime);
|
|
132
|
+
const agentMedia = () => deps?.media ?? runtime.media;
|
|
62
133
|
const db = deps && 'logDb' in deps ? deps.logDb : logDb;
|
|
63
134
|
const agentBase = deps?.agentBase ?? (() => (selectedAgentId ? runtime.systemBase() : cachedAgentBase()));
|
|
64
|
-
const { connectorId, chatId
|
|
135
|
+
const { connectorId, chatId } = turn.envelope;
|
|
136
|
+
const { sender } = turn;
|
|
137
|
+
const messages = turn.body.messages;
|
|
65
138
|
const last = messages[messages.length - 1];
|
|
66
139
|
if (!last || last.role !== 'user')
|
|
67
140
|
return;
|
|
68
141
|
const incomingMsgId = last.msgId;
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
await safeCall(() => ch.finish({ text: '✅ Access granted. Send your requests.', reasoning: null, suggestions: null }), null, 'finish(enroll)');
|
|
79
|
-
}
|
|
80
|
-
else if (passThrottle(connectorId)(sender.id)) {
|
|
81
|
-
const ch = openChannel(connector, chatId, { parentMsgId: incomingMsgId });
|
|
82
|
-
if (ch)
|
|
83
|
-
await safeCall(() => ch.finish({
|
|
84
|
-
text: '🔒 Access locked. Send the password to gain access.',
|
|
85
|
-
reasoning: null,
|
|
86
|
-
suggestions: null,
|
|
87
|
-
}), null, 'finish(locked)');
|
|
142
|
+
if (policy.triggerPrefix && !last.content.toLowerCase().startsWith(policy.triggerPrefix.toLowerCase()))
|
|
143
|
+
return;
|
|
144
|
+
const speakerRow = await resolveSpeaker(connectorId, sender, deps);
|
|
145
|
+
if (speakerRow === null) {
|
|
146
|
+
if (passThrottle(connectorId)(sender.id)) {
|
|
147
|
+
const sink = openSink(connector, turn.envelope);
|
|
148
|
+
if (sink) {
|
|
149
|
+
sink.emit({ kind: 'notice', text: connector.enrolmentNotice });
|
|
150
|
+
await safeCall(() => sink.done(), undefined, 'done(unidentified)');
|
|
88
151
|
}
|
|
89
|
-
return;
|
|
90
152
|
}
|
|
153
|
+
return;
|
|
91
154
|
}
|
|
92
|
-
if (
|
|
155
|
+
if (turn.signal?.aborted)
|
|
93
156
|
return;
|
|
94
157
|
const queryText = policy.triggerPrefix ? last.content.slice(policy.triggerPrefix.length).trim() : last.content.trim();
|
|
95
158
|
if (!queryText) {
|
|
96
|
-
if (
|
|
97
|
-
await safeCall(() =>
|
|
159
|
+
if (turn.prepareAttachments && !policy.triggerPrefix) {
|
|
160
|
+
await safeCall(() => turn.prepareAttachments(attachmentMaterializer), messages, 'prepareAttachments');
|
|
98
161
|
}
|
|
99
162
|
return;
|
|
100
163
|
}
|
|
101
|
-
const preparedMessages =
|
|
102
|
-
? await safeCall(() =>
|
|
164
|
+
const preparedMessages = turn.prepareAttachments
|
|
165
|
+
? await safeCall(() => turn.prepareAttachments(attachmentMaterializer), messages, 'prepareAttachments')
|
|
103
166
|
: messages;
|
|
104
|
-
const
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
const
|
|
112
|
-
const
|
|
167
|
+
const speaker = {
|
|
168
|
+
id: sender.id,
|
|
169
|
+
name: speakerRow.displayName ?? sender.displayName ?? sender.id,
|
|
170
|
+
role: speakerRow.role,
|
|
171
|
+
};
|
|
172
|
+
const stated = preparedMessages.filter((m) => m.context?.length);
|
|
173
|
+
const lastStated = stated[stated.length - 1];
|
|
174
|
+
const now = deps?.now ? deps.now() : new Date();
|
|
175
|
+
const facts = buildContextFacts({
|
|
176
|
+
connectorFacts: turn.turnContext ?? [],
|
|
177
|
+
speaker,
|
|
178
|
+
now,
|
|
179
|
+
timeZone: deps?.timeZone ?? systemTimeZone(),
|
|
180
|
+
previous: stated.length ? foldContextFacts(preparedMessages) : null,
|
|
181
|
+
previousAt: lastStated ? new Date(lastStated.ts * 1000) : null,
|
|
182
|
+
});
|
|
183
|
+
const withText = preparedMessages.map((m, i) => i === preparedMessages.length - 1 && policy.triggerPrefix ? { ...m, content: queryText } : m);
|
|
184
|
+
const userMsg = withText[withText.length - 1];
|
|
185
|
+
const agentMessages = facts.length ? [...withText.slice(0, -1), { ...userMsg, context: facts }] : withText;
|
|
186
|
+
const persistContext = async () => {
|
|
187
|
+
if (!facts.length)
|
|
188
|
+
return;
|
|
189
|
+
await safeCall(() => connector.recordContext({
|
|
190
|
+
chatId,
|
|
191
|
+
userMsgId: incomingMsgId,
|
|
192
|
+
facts,
|
|
193
|
+
ts: userMsg.ts - 1,
|
|
194
|
+
}), undefined, 'recordContext');
|
|
195
|
+
};
|
|
196
|
+
const authored = turn.body.systemPrompt;
|
|
197
|
+
const domain = await (deps?.domainAreas ?? cachedDomainAreas)();
|
|
198
|
+
const areas = mergeAreas({ root: [await agentBase()] }, domain, authored);
|
|
199
|
+
const system = assembleSystem(areas, deps?.world ?? (await liveWorld()));
|
|
200
|
+
if (turn.signal?.aborted) {
|
|
201
|
+
await persistContext();
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
113
204
|
db?.logTurn({
|
|
114
205
|
connector: connectorId,
|
|
115
206
|
chatId,
|
|
@@ -120,79 +211,108 @@ export async function handleIncoming(connector, conversation, deps) {
|
|
|
120
211
|
tokensOut: null,
|
|
121
212
|
ms: null,
|
|
122
213
|
agentId: selectedAgentId ?? null,
|
|
123
|
-
presetId:
|
|
214
|
+
presetId: turn.presetId ?? null,
|
|
124
215
|
});
|
|
125
216
|
const startedAt = Date.now();
|
|
126
|
-
const
|
|
127
|
-
if (!
|
|
217
|
+
const sink = openSink(connector, turn.envelope);
|
|
218
|
+
if (!sink)
|
|
128
219
|
return;
|
|
129
|
-
let
|
|
130
|
-
|
|
220
|
+
let usage;
|
|
221
|
+
let answerText = null;
|
|
222
|
+
let runFailed = false;
|
|
223
|
+
let agentTurn;
|
|
131
224
|
try {
|
|
132
|
-
const attachmentTools = await makeAttachmentCapabilities(agentMessages.flatMap((m) => m.attachments ?? []));
|
|
225
|
+
const attachmentTools = await makeAttachmentCapabilities(agentMessages.flatMap((m) => m.attachments ?? []), agentMedia());
|
|
133
226
|
const toolProvider = {
|
|
134
|
-
tools: [...attachmentTools.tools, ...makeSystemCapabilities().tools
|
|
227
|
+
tools: [...attachmentTools.tools, ...makeSystemCapabilities().tools],
|
|
135
228
|
};
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
messages: agentMessages,
|
|
229
|
+
agentTurn = {
|
|
230
|
+
envelope: turn.envelope,
|
|
231
|
+
body: { system, messages: agentMessages, userTurns: turn.body.userTurns },
|
|
139
232
|
toolProvider,
|
|
140
|
-
...(
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
233
|
+
...(turn.presetId ? { presetId: turn.presetId } : {}),
|
|
234
|
+
senderRole: speaker.role,
|
|
235
|
+
...(turn.signal ? { signal: turn.signal } : {}),
|
|
236
|
+
};
|
|
237
|
+
await agent(agentTurn, (event) => {
|
|
238
|
+
try {
|
|
239
|
+
if (event.kind === 'usage') {
|
|
240
|
+
usage = { tokensIn: event.tokensIn, tokensOut: event.tokensOut, presetId: event.presetId };
|
|
144
241
|
}
|
|
145
|
-
|
|
146
|
-
|
|
242
|
+
else if (event.kind === 'answer') {
|
|
243
|
+
answerText = event.text;
|
|
147
244
|
}
|
|
148
|
-
|
|
149
|
-
|
|
245
|
+
sink.emit(event);
|
|
246
|
+
}
|
|
247
|
+
catch (err) {
|
|
248
|
+
log.error(`connector error in ${event.kind}: ${err instanceof Error ? err.message : String(err)}`);
|
|
249
|
+
}
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
catch (err) {
|
|
253
|
+
runFailed = true;
|
|
254
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
255
|
+
log.error(`agent error: ${message}`);
|
|
256
|
+
recordDiagnostic('error', 'agent.run', message);
|
|
257
|
+
try {
|
|
258
|
+
sink.emit({ kind: 'error', message });
|
|
259
|
+
}
|
|
260
|
+
catch (sinkErr) {
|
|
261
|
+
log.error(`connector error in error: ${sinkErr instanceof Error ? sinkErr.message : String(sinkErr)}`);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
await safeCall(() => sink.done(), undefined, 'done');
|
|
265
|
+
if (!runFailed && answerText && agentTurn) {
|
|
266
|
+
const events = turn.body.capabilities.events;
|
|
267
|
+
const want = { suggestions: events.includes('suggestions'), title: events.includes('title') };
|
|
268
|
+
const afterwordFn = deps?.afterword ?? runtime?.afterword;
|
|
269
|
+
if ((want.suggestions || want.title) && afterwordFn) {
|
|
270
|
+
let extra;
|
|
271
|
+
try {
|
|
272
|
+
extra = await afterwordFn(agentTurn, answerText, want);
|
|
273
|
+
}
|
|
274
|
+
catch (err) {
|
|
275
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
276
|
+
recordDiagnostic('error', 'agent.afterword', message);
|
|
277
|
+
}
|
|
278
|
+
if (extra?.suggestions?.length) {
|
|
150
279
|
try {
|
|
151
|
-
|
|
280
|
+
sink.emit({ kind: 'suggestions', items: extra.suggestions });
|
|
152
281
|
}
|
|
153
282
|
catch (err) {
|
|
154
|
-
log.error(`connector error in
|
|
283
|
+
log.error(`connector error in suggestions: ${err instanceof Error ? err.message : String(err)}`);
|
|
155
284
|
}
|
|
156
|
-
}
|
|
157
|
-
|
|
285
|
+
}
|
|
286
|
+
if (extra?.title) {
|
|
158
287
|
try {
|
|
159
|
-
|
|
288
|
+
sink.emit({ kind: 'title', text: extra.title });
|
|
160
289
|
}
|
|
161
290
|
catch (err) {
|
|
162
|
-
log.error(`connector error in
|
|
291
|
+
log.error(`connector error in title: ${err instanceof Error ? err.message : String(err)}`);
|
|
163
292
|
}
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
293
|
+
}
|
|
294
|
+
if (usage && extra && (extra.tokensIn !== undefined || extra.tokensOut !== undefined)) {
|
|
295
|
+
usage = {
|
|
296
|
+
...usage,
|
|
297
|
+
tokensIn: usage.tokensIn === null ? null : usage.tokensIn + (extra.tokensIn ?? 0),
|
|
298
|
+
tokensOut: usage.tokensOut === null ? null : usage.tokensOut + (extra.tokensOut ?? 0),
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
}
|
|
171
302
|
}
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
const botMsgId = await safeCall(() => ch.finish({
|
|
175
|
-
text: result?.text ?? null,
|
|
176
|
-
reasoning: result?.reasoning ?? null,
|
|
177
|
-
suggestions: result?.suggestions ?? null,
|
|
178
|
-
}), null, 'finish');
|
|
179
|
-
if (result?.text)
|
|
303
|
+
await safeCall(() => sink.done(), undefined, 'done');
|
|
304
|
+
if (!runFailed && usage && answerText)
|
|
180
305
|
db?.logTurn({
|
|
181
306
|
connector: connectorId,
|
|
182
307
|
chatId,
|
|
183
308
|
userId: sender.id,
|
|
184
309
|
role: 'assistant',
|
|
185
|
-
text:
|
|
186
|
-
tokensIn:
|
|
187
|
-
tokensOut:
|
|
310
|
+
text: answerText,
|
|
311
|
+
tokensIn: usage.tokensIn,
|
|
312
|
+
tokensOut: usage.tokensOut,
|
|
188
313
|
ms: Date.now() - startedAt,
|
|
189
314
|
agentId: selectedAgentId ?? null,
|
|
190
|
-
presetId:
|
|
315
|
+
presetId: usage.presetId,
|
|
191
316
|
});
|
|
192
|
-
await
|
|
193
|
-
parentMsgId: incomingMsgId,
|
|
194
|
-
botMsgId,
|
|
195
|
-
text: result?.text ?? '',
|
|
196
|
-
reasoning: result?.reasoning ?? null,
|
|
197
|
-
}), undefined, 'recordAssistant');
|
|
317
|
+
await persistContext();
|
|
198
318
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { AgentCatalogEntry, AgentRuntime, ConnectorRegistration } from './types.ts';
|
|
2
2
|
export declare function registerAgent(runtime: AgentRuntime, opts?: {
|
|
3
3
|
default?: boolean;
|
|
4
4
|
}): () => void;
|
|
@@ -8,6 +8,8 @@ export declare function registerConnector(registration: ConnectorRegistration):
|
|
|
8
8
|
export declare function isConnectorRegistered(id: string): boolean;
|
|
9
9
|
export declare function listRegisteredAgents(): string[];
|
|
10
10
|
export declare function listRegisteredConnectors(): string[];
|
|
11
|
-
export declare function
|
|
11
|
+
export declare function listLinkableConnectors(): string[];
|
|
12
|
+
export declare function connectorLinkUrl(connectorId: string, code: string): string | undefined;
|
|
13
|
+
export declare function listAgentCatalog(): Promise<AgentCatalogEntry[]>;
|
|
12
14
|
export declare function getDefaultAgentId(): string | undefined;
|
|
13
15
|
export declare function clearRuntimeRegistries(): void;
|
|
@@ -48,6 +48,15 @@ export function listRegisteredAgents() {
|
|
|
48
48
|
export function listRegisteredConnectors() {
|
|
49
49
|
return [...connectors.keys()].sort();
|
|
50
50
|
}
|
|
51
|
+
export function listLinkableConnectors() {
|
|
52
|
+
return [...connectors.entries()]
|
|
53
|
+
.filter(([, registration]) => typeof registration.linkUrl === 'function')
|
|
54
|
+
.map(([id]) => id)
|
|
55
|
+
.sort();
|
|
56
|
+
}
|
|
57
|
+
export function connectorLinkUrl(connectorId, code) {
|
|
58
|
+
return connectors.get(connectorId)?.linkUrl?.(code);
|
|
59
|
+
}
|
|
51
60
|
export async function listAgentCatalog() {
|
|
52
61
|
const ids = [...agents.keys()].sort();
|
|
53
62
|
const out = [];
|
|
@@ -56,7 +65,7 @@ export async function listAgentCatalog() {
|
|
|
56
65
|
if (!runtime)
|
|
57
66
|
continue;
|
|
58
67
|
try {
|
|
59
|
-
out.push(await runtime.describe());
|
|
68
|
+
out.push({ ...(await runtime.describe()), media: runtime.media });
|
|
60
69
|
}
|
|
61
70
|
catch (err) {
|
|
62
71
|
log.error(`agent describe failed for ${id}: ${err instanceof Error ? err.message : String(err)}`);
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export type SystemAreas = Record<string, string[]>;
|
|
2
|
+
export interface AreaWorld {
|
|
3
|
+
hasPlugin(id: string): boolean;
|
|
4
|
+
hasLibrary(id: string): boolean;
|
|
5
|
+
hasShelf(library: string, shelf: string): boolean;
|
|
6
|
+
}
|
|
7
|
+
export declare const KIND_ORDER: readonly ["root", "plugin", "library", "shelf", "channel"];
|
|
8
|
+
export declare const UNCONDITIONAL: ReadonlySet<string>;
|
|
9
|
+
export declare function rank(key: string): number;
|
|
10
|
+
export declare const ALL_PRESENT: AreaWorld;
|
|
11
|
+
export declare function mergeAreas(...areas: SystemAreas[]): SystemAreas;
|
|
12
|
+
export declare function assembleSystem(areas: SystemAreas, world: AreaWorld): string[];
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { getLogger } from '@coffer-org/sdk/logger';
|
|
2
|
+
const log = getLogger('orchestrator');
|
|
3
|
+
export const KIND_ORDER = ['root', 'plugin', 'library', 'shelf', 'channel'];
|
|
4
|
+
export const UNCONDITIONAL = new Set(['root', 'channel']);
|
|
5
|
+
function keep(key, world) {
|
|
6
|
+
if (UNCONDITIONAL.has(key))
|
|
7
|
+
return true;
|
|
8
|
+
const colon = key.indexOf(':');
|
|
9
|
+
if (colon < 0)
|
|
10
|
+
return null;
|
|
11
|
+
const kind = key.slice(0, colon);
|
|
12
|
+
const id = key.slice(colon + 1);
|
|
13
|
+
if (!id)
|
|
14
|
+
return null;
|
|
15
|
+
if (kind === 'plugin')
|
|
16
|
+
return world.hasPlugin(id);
|
|
17
|
+
if (kind === 'library')
|
|
18
|
+
return world.hasLibrary(id);
|
|
19
|
+
if (kind === 'shelf') {
|
|
20
|
+
const slash = id.indexOf('/');
|
|
21
|
+
if (slash < 1 || slash === id.length - 1 || id.indexOf('/', slash + 1) >= 0)
|
|
22
|
+
return null;
|
|
23
|
+
return world.hasShelf(id.slice(0, slash), id.slice(slash + 1));
|
|
24
|
+
}
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
export function rank(key) {
|
|
28
|
+
const colon = key.indexOf(':');
|
|
29
|
+
const kind = colon < 0 ? key : key.slice(0, colon);
|
|
30
|
+
const i = KIND_ORDER.indexOf(kind);
|
|
31
|
+
return i < 0 ? KIND_ORDER.length : i;
|
|
32
|
+
}
|
|
33
|
+
export const ALL_PRESENT = {
|
|
34
|
+
hasPlugin: () => true,
|
|
35
|
+
hasLibrary: () => true,
|
|
36
|
+
hasShelf: () => true,
|
|
37
|
+
};
|
|
38
|
+
export function mergeAreas(...areas) {
|
|
39
|
+
const out = {};
|
|
40
|
+
for (const a of areas) {
|
|
41
|
+
for (const [key, lines] of Object.entries(a)) {
|
|
42
|
+
out[key] = [...(out[key] ?? []), ...lines];
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return out;
|
|
46
|
+
}
|
|
47
|
+
export function assembleSystem(areas, world) {
|
|
48
|
+
const kept = [];
|
|
49
|
+
for (const key of Object.keys(areas)) {
|
|
50
|
+
const verdict = keep(key, world);
|
|
51
|
+
if (verdict === null) {
|
|
52
|
+
log.warn(`unknown system-prompt area '${key}' — dropped`);
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
if (verdict)
|
|
56
|
+
kept.push(key);
|
|
57
|
+
}
|
|
58
|
+
kept.sort((a, b) => rank(a) - rank(b) || (a < b ? -1 : a > b ? 1 : 0));
|
|
59
|
+
return kept
|
|
60
|
+
.flatMap((key) => areas[key] ?? [])
|
|
61
|
+
.map((line) => line.trim())
|
|
62
|
+
.filter(Boolean);
|
|
63
|
+
}
|
|
@@ -42,7 +42,7 @@ export function makeSystemCapabilities() {
|
|
|
42
42
|
'platform, memory, and the database file path with its size and free disk space. ' +
|
|
43
43
|
'Use it for questions about the instance itself ("which version am I running", "is the disk filling up", ' +
|
|
44
44
|
'"how long has the server been up"). It returns nothing about the user\'s records — use count_records for those. ' +
|
|
45
|
-
|
|
45
|
+
"The current date and time are already given in each turn's context; do not call this tool to find them.",
|
|
46
46
|
inputSchema: {},
|
|
47
47
|
handler: async () => ({
|
|
48
48
|
coffer_version: await runtimeVersion(),
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { AuthRole } from '../plugin-hooks.ts';
|
|
2
|
+
export declare const PAUSE_THRESHOLD_MS: number;
|
|
3
|
+
export interface TurnSpeaker {
|
|
4
|
+
name: string;
|
|
5
|
+
role: AuthRole;
|
|
6
|
+
}
|
|
7
|
+
export interface TurnContextInput {
|
|
8
|
+
connectorLines: readonly string[];
|
|
9
|
+
speaker: TurnSpeaker;
|
|
10
|
+
previousSpeakerName: string | null;
|
|
11
|
+
now: Date;
|
|
12
|
+
timeZone: string;
|
|
13
|
+
previousMessageTs: number | null;
|
|
14
|
+
}
|
|
15
|
+
export declare const MAX_SPEAKER_NAME = 80;
|
|
16
|
+
export declare function oneLine(value: string, maxLen?: number): string;
|
|
17
|
+
export declare function humanizePause(ms: number): string;
|
|
18
|
+
export declare function buildTurnContext(input: TurnContextInput): string;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { currentMoment } from "./environment.js";
|
|
2
|
+
export const PAUSE_THRESHOLD_MS = 10 * 60_000;
|
|
3
|
+
export const MAX_SPEAKER_NAME = 80;
|
|
4
|
+
const FORGERY_CHARS = /[\p{Cc}\p{Zl}\p{Zp}\p{Bidi_Control}]+/gu;
|
|
5
|
+
export function oneLine(value, maxLen) {
|
|
6
|
+
const flat = value.replace(FORGERY_CHARS, ' ').replace(/\s+/g, ' ').trim();
|
|
7
|
+
return maxLen !== undefined && flat.length > maxLen ? `${flat.slice(0, maxLen - 1).trimEnd()}…` : flat;
|
|
8
|
+
}
|
|
9
|
+
export function humanizePause(ms) {
|
|
10
|
+
const minutes = Math.round(ms / 60_000);
|
|
11
|
+
if (minutes < 90)
|
|
12
|
+
return `about ${minutes} minutes`;
|
|
13
|
+
const hours = Math.round(ms / 3_600_000);
|
|
14
|
+
if (hours < 48)
|
|
15
|
+
return `about ${hours} hours`;
|
|
16
|
+
return `about ${Math.round(ms / 86_400_000)} days`;
|
|
17
|
+
}
|
|
18
|
+
export function buildTurnContext(input) {
|
|
19
|
+
const name = oneLine(input.speaker.name, MAX_SPEAKER_NAME) || 'unknown';
|
|
20
|
+
const previousName = input.previousSpeakerName
|
|
21
|
+
? oneLine(input.previousSpeakerName, MAX_SPEAKER_NAME) || 'unknown'
|
|
22
|
+
: null;
|
|
23
|
+
const who = `${name} (${input.speaker.role})`;
|
|
24
|
+
const lines = [
|
|
25
|
+
previousName ? `Speaking now: ${who} — the previous message was from ${previousName}.` : `Speaking now: ${who}.`,
|
|
26
|
+
`Current time: ${currentMoment(input.now, input.timeZone)} (${input.timeZone}).`,
|
|
27
|
+
];
|
|
28
|
+
if (input.previousMessageTs !== null) {
|
|
29
|
+
const gap = input.now.getTime() - input.previousMessageTs * 1000;
|
|
30
|
+
if (gap >= PAUSE_THRESHOLD_MS)
|
|
31
|
+
lines.push(`Time since the previous message: ${humanizePause(gap)}.`);
|
|
32
|
+
}
|
|
33
|
+
for (const raw of input.connectorLines) {
|
|
34
|
+
const line = oneLine(raw);
|
|
35
|
+
if (line)
|
|
36
|
+
lines.push(line);
|
|
37
|
+
}
|
|
38
|
+
return lines.join('\n');
|
|
39
|
+
}
|