@coffer-org/server 7.2.0 → 7.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/dist/auth-api.d.ts +4 -0
- package/dist/auth-api.js +52 -0
- package/dist/auth-store.d.ts +2 -0
- package/dist/auth-store.js +1 -0
- package/dist/compute-unit.js +0 -3
- package/dist/conversation-store.d.ts +61 -0
- package/dist/conversation-store.js +223 -0
- package/dist/entity-schema.d.ts +5 -2
- package/dist/entity-schema.js +35 -18
- package/dist/identity-link.d.ts +15 -0
- package/dist/identity-link.js +76 -0
- package/dist/identity-providers.d.ts +17 -0
- package/dist/identity-providers.js +14 -0
- package/dist/index.js +8 -1
- package/dist/mcp-contract/schema.d.ts +0 -1
- package/dist/mcp-contract/schema.js +0 -2
- package/dist/mcp-http.js +7 -3
- package/dist/mcp-tools.d.ts +4 -3
- package/dist/mcp-tools.js +84 -84
- 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/mutate.js +0 -15
- 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/draft-message.d.ts +18 -0
- package/dist/orchestrator/draft-message.js +85 -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 +14 -9
- package/dist/orchestrator/index.js +6 -7
- package/dist/orchestrator/live-message.d.ts +7 -4
- package/dist/orchestrator/live-message.js +52 -32
- package/dist/orchestrator/pipeline.d.ts +22 -4
- package/dist/orchestrator/pipeline.js +316 -115
- package/dist/orchestrator/registry.d.ts +2 -2
- package/dist/orchestrator/registry.js +1 -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 +100 -49
- package/dist/plugin-hooks.d.ts +38 -1
- package/dist/plugin-hooks.js +4 -0
- package/dist/plugin-http-mounts.d.ts +18 -0
- package/dist/plugin-http-mounts.js +94 -0
- package/dist/plugin-runtime.d.ts +4 -0
- package/dist/plugin-runtime.js +9 -5
- package/dist/plugins-api.d.ts +8 -2
- package/dist/plugins-api.js +4 -2
- package/dist/records-api.js +15 -3
- package/dist/settings-write.d.ts +1 -2
- package/dist/settings-write.js +1 -2
- package/dist/system-settings.js +0 -1
- package/dist/temporal.js +8 -3
- 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,27 +1,30 @@
|
|
|
1
|
-
import { loadAllowed, saveAllowed, isAllowed, addAllowed, makeThrottle, allowFileFor } from "./allow.js";
|
|
2
1
|
import { loadAgentId, loadGatePolicy } from "./config.js";
|
|
3
|
-
import {
|
|
2
|
+
import { assembleSystem, mergeAreas } from "./system-areas.js";
|
|
4
3
|
import { attachmentMaterializer } from "./attachments.js";
|
|
5
4
|
import { makeAttachmentCapabilities } from "./agent-capabilities.js";
|
|
6
5
|
import { makeSystemCapabilities } from "./system-capabilities.js";
|
|
7
|
-
import { makeSuggestionCapabilities } from "./suggestion-capabilities.js";
|
|
8
6
|
import { resolveAgent } from "./registry.js";
|
|
9
7
|
import { recordDiagnostic } from "./diagnostics.js";
|
|
10
8
|
import { getLogger } from '@coffer-org/sdk/logger';
|
|
9
|
+
import { buildContextFacts } from "./context-facts.js";
|
|
10
|
+
import { systemTimeZone } from "./environment.js";
|
|
11
|
+
import { discoverPlugins } from "../plugin-discovery.js";
|
|
12
|
+
import { SchemaCache } from "../mcp-contract/schema.js";
|
|
13
|
+
import { LocalClient } from "../mcp-local.js";
|
|
14
|
+
import { loadComposedLocales } from "../locale-registry.js";
|
|
15
|
+
import { buildDomainAreas } from "../mcp-tools.js";
|
|
16
|
+
import { getActiveRegistry } from "../registry-context.js";
|
|
17
|
+
import { DEFAULT_MAX_DEPTH, countUserMessages, newMessageId, putMessage, readConversation, readAll, } from "../conversation-store.js";
|
|
11
18
|
const log = getLogger('orchestrator');
|
|
19
|
+
const DEFAULT_MEDIA = {
|
|
20
|
+
image: { maxBytes: 5 * 1024 * 1024, maxEdge: 1568, accepts: ['image/*'], encode: ['image/jpeg', 'image/png'] },
|
|
21
|
+
document: { maxBytes: 10 * 1024 * 1024, accepts: ['application/pdf'] },
|
|
22
|
+
text: { maxBytes: 512 * 1024, accepts: ['text/*'] },
|
|
23
|
+
};
|
|
12
24
|
let logDb;
|
|
13
25
|
export function setLogDb(db) {
|
|
14
26
|
logDb = db;
|
|
15
27
|
}
|
|
16
|
-
const throttleByConnector = new Map();
|
|
17
|
-
function passThrottle(connectorId) {
|
|
18
|
-
let t = throttleByConnector.get(connectorId);
|
|
19
|
-
if (!t) {
|
|
20
|
-
t = makeThrottle(5, 60_000);
|
|
21
|
-
throttleByConnector.set(connectorId, t);
|
|
22
|
-
}
|
|
23
|
-
return t;
|
|
24
|
-
}
|
|
25
28
|
let cachedBase;
|
|
26
29
|
let cachedBaseAgentId;
|
|
27
30
|
function cachedAgentBase() {
|
|
@@ -32,14 +35,125 @@ function cachedAgentBase() {
|
|
|
32
35
|
}
|
|
33
36
|
return cachedBase;
|
|
34
37
|
}
|
|
35
|
-
|
|
38
|
+
let cachedWorld;
|
|
39
|
+
export function liveWorld() {
|
|
40
|
+
if (!cachedWorld) {
|
|
41
|
+
cachedWorld = (async () => {
|
|
42
|
+
const [plugins, locales] = await Promise.all([discoverPlugins(), loadComposedLocales()]);
|
|
43
|
+
const shelves = await new SchemaCache(new LocalClient(), locales).index();
|
|
44
|
+
const pluginIds = new Set(plugins.map((p) => p.id));
|
|
45
|
+
const shelfKeys = new Set(shelves.map((s) => `${s.library}/${s.shelf}`));
|
|
46
|
+
let libraryIds;
|
|
47
|
+
try {
|
|
48
|
+
libraryIds = new Set(getActiveRegistry().libraries.map((v) => v.meta.id));
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
libraryIds = new Set(shelves.map((s) => s.library));
|
|
52
|
+
}
|
|
53
|
+
return {
|
|
54
|
+
hasPlugin: (id) => pluginIds.has(id),
|
|
55
|
+
hasLibrary: (id) => libraryIds.has(id),
|
|
56
|
+
hasShelf: (library, shelf) => shelfKeys.has(`${library}/${shelf}`),
|
|
57
|
+
};
|
|
58
|
+
})();
|
|
59
|
+
}
|
|
60
|
+
return cachedWorld;
|
|
61
|
+
}
|
|
62
|
+
let cachedDomain;
|
|
63
|
+
function cachedDomainAreas() {
|
|
64
|
+
if (!cachedDomain)
|
|
65
|
+
cachedDomain = buildDomainAreas();
|
|
66
|
+
return cachedDomain;
|
|
67
|
+
}
|
|
68
|
+
export function foldContextFacts(messages) {
|
|
69
|
+
const acc = new Map();
|
|
70
|
+
for (const m of messages) {
|
|
71
|
+
let facts;
|
|
72
|
+
if ('role' in m && m.role === 'context') {
|
|
73
|
+
try {
|
|
74
|
+
const parsed = JSON.parse(m.text);
|
|
75
|
+
if (Array.isArray(parsed))
|
|
76
|
+
facts = parsed;
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
else if ('context' in m && Array.isArray(m.context)) {
|
|
82
|
+
facts = m.context;
|
|
83
|
+
}
|
|
84
|
+
if (facts) {
|
|
85
|
+
for (const fact of facts) {
|
|
86
|
+
if (fact.name === 'cleared')
|
|
87
|
+
acc.delete(fact.value);
|
|
88
|
+
else
|
|
89
|
+
acc.set(fact.name, fact);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return [...acc.values()];
|
|
94
|
+
}
|
|
95
|
+
export function lastContextAt(messages) {
|
|
96
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
97
|
+
const m = messages[i];
|
|
98
|
+
if ('role' in m && m.role === 'context') {
|
|
99
|
+
try {
|
|
100
|
+
const parsed = JSON.parse(m.text);
|
|
101
|
+
if (Array.isArray(parsed) && parsed.length > 0) {
|
|
102
|
+
return new Date(m.ts * 1000);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
if ('context' in m && m.context && m.context.length > 0) {
|
|
109
|
+
return new Date(m.ts * 1000);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
export function toConvMessages(stored, facts, userMsgId, queryText, contextSource = stored) {
|
|
115
|
+
const contextMap = new Map();
|
|
116
|
+
for (const m of contextSource) {
|
|
117
|
+
if (m.role === 'context') {
|
|
118
|
+
try {
|
|
119
|
+
const parsed = JSON.parse(m.text);
|
|
120
|
+
if (Array.isArray(parsed)) {
|
|
121
|
+
contextMap.set(m.msgId, parsed);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
catch {
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
const out = [];
|
|
129
|
+
for (const m of stored) {
|
|
130
|
+
if (m.role !== 'user' && m.role !== 'assistant')
|
|
131
|
+
continue;
|
|
132
|
+
const isCurrentUser = m.msgId === userMsgId;
|
|
133
|
+
const itemContext = isCurrentUser ? (facts.length ? facts : undefined) : contextMap.get(`${m.msgId}~c`);
|
|
134
|
+
out.push({
|
|
135
|
+
role: m.role,
|
|
136
|
+
content: isCurrentUser ? queryText : m.text,
|
|
137
|
+
sender: m.sender,
|
|
138
|
+
...(m.attachments ? { attachments: m.attachments } : {}),
|
|
139
|
+
msgId: m.msgId,
|
|
140
|
+
ts: m.ts,
|
|
141
|
+
...(itemContext?.length ? { context: itemContext } : {}),
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
while (out.length && out[0]?.role !== 'user')
|
|
145
|
+
out.shift();
|
|
146
|
+
return out;
|
|
147
|
+
}
|
|
148
|
+
const storeLookup = async (id) => (await import("../auth-store.js")).findUserById(id);
|
|
149
|
+
function openSink(connector, envelope, msgId) {
|
|
36
150
|
try {
|
|
37
|
-
return connector.
|
|
151
|
+
return connector.open(envelope, msgId);
|
|
38
152
|
}
|
|
39
153
|
catch (err) {
|
|
40
154
|
const message = err instanceof Error ? err.message : String(err);
|
|
41
|
-
log.error(`connector error in
|
|
42
|
-
recordDiagnostic('error', 'connector.
|
|
155
|
+
log.error(`connector error in open: ${message}`);
|
|
156
|
+
recordDiagnostic('error', 'connector.open', message);
|
|
43
157
|
return null;
|
|
44
158
|
}
|
|
45
159
|
}
|
|
@@ -54,145 +168,232 @@ async function safeCall(fn, fallback, label) {
|
|
|
54
168
|
return fallback;
|
|
55
169
|
}
|
|
56
170
|
}
|
|
57
|
-
export async function handleIncoming(connector,
|
|
171
|
+
export async function handleIncoming(connector, turn, deps) {
|
|
58
172
|
const policy = deps?.policy ?? (await loadGatePolicy());
|
|
59
|
-
const selectedAgentId =
|
|
173
|
+
const selectedAgentId = turn.agentId ?? policy.agentId ?? (deps?.runAgent ? undefined : await loadAgentId());
|
|
60
174
|
const runtime = deps?.runAgent ? undefined : resolveAgent(selectedAgentId);
|
|
61
175
|
const agent = deps?.runAgent ?? runtime.run.bind(runtime);
|
|
176
|
+
const agentMedia = () => deps?.media ?? runtime?.media ?? DEFAULT_MEDIA;
|
|
62
177
|
const db = deps && 'logDb' in deps ? deps.logDb : logDb;
|
|
63
178
|
const agentBase = deps?.agentBase ?? (() => (selectedAgentId ? runtime.systemBase() : cachedAgentBase()));
|
|
64
|
-
const { connectorId
|
|
65
|
-
const
|
|
66
|
-
|
|
179
|
+
const { connectorId } = turn.envelope;
|
|
180
|
+
const conversationId = turn.envelope.conversationId;
|
|
181
|
+
const { sender } = turn;
|
|
182
|
+
const text = turn.message?.text ?? '';
|
|
183
|
+
const replyToId = turn.message?.replyToMsgId ?? null;
|
|
184
|
+
const rawAttachments = turn.prepareAttachments
|
|
185
|
+
? await safeCall(() => turn.prepareAttachments(attachmentMaterializer), [], 'prepareAttachments')
|
|
186
|
+
: turn.message?.attachments;
|
|
187
|
+
const attachments = Array.isArray(rawAttachments) && rawAttachments.length > 0 && 'role' in rawAttachments[0]
|
|
188
|
+
? rawAttachments.at(-1)?.attachments
|
|
189
|
+
: rawAttachments;
|
|
190
|
+
const userMsgId = newMessageId();
|
|
191
|
+
const now = deps?.now ? deps.now() : new Date();
|
|
192
|
+
const ts = Math.floor(now.getTime() / 1000);
|
|
193
|
+
await putMessage({
|
|
194
|
+
conversationId,
|
|
195
|
+
msgId: userMsgId,
|
|
196
|
+
role: 'user',
|
|
197
|
+
sender: String(sender.userId),
|
|
198
|
+
text,
|
|
199
|
+
...(attachments ? { attachments } : {}),
|
|
200
|
+
ts,
|
|
201
|
+
replyToId,
|
|
202
|
+
});
|
|
203
|
+
if (policy.triggerPrefix && !text.toLowerCase().startsWith(policy.triggerPrefix.toLowerCase()))
|
|
67
204
|
return;
|
|
68
|
-
const
|
|
69
|
-
|
|
70
|
-
if (policy.accessPassword) {
|
|
71
|
-
let allow = loadAllowed(allowFile);
|
|
72
|
-
if (!isAllowed(allow, sender.id)) {
|
|
73
|
-
if (last.content.trim() === policy.accessPassword) {
|
|
74
|
-
allow = addAllowed(allow, sender.id, Date.now());
|
|
75
|
-
saveAllowed(allowFile, allow);
|
|
76
|
-
const ch = openChannel(connector, chatId, { parentMsgId: incomingMsgId });
|
|
77
|
-
if (ch)
|
|
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)');
|
|
88
|
-
}
|
|
89
|
-
return;
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
if (policy.triggerPrefix && !last.content.toLowerCase().startsWith(policy.triggerPrefix.toLowerCase()))
|
|
205
|
+
const speakerRow = await (deps?.lookupUser ?? storeLookup)(sender.userId);
|
|
206
|
+
if (!speakerRow || speakerRow.disabled)
|
|
93
207
|
return;
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
}
|
|
208
|
+
if (turn.signal?.aborted)
|
|
209
|
+
return;
|
|
210
|
+
const queryText = policy.triggerPrefix ? text.slice(policy.triggerPrefix.length).trim() : text.trim();
|
|
211
|
+
if (!queryText)
|
|
99
212
|
return;
|
|
213
|
+
const speaker = {
|
|
214
|
+
id: String(sender.userId),
|
|
215
|
+
name: speakerRow.displayName ?? sender.displayName ?? String(sender.userId),
|
|
216
|
+
role: speakerRow.role,
|
|
217
|
+
};
|
|
218
|
+
const stored = await readConversation(conversationId, { limit: DEFAULT_MAX_DEPTH });
|
|
219
|
+
const all = await readAll(conversationId);
|
|
220
|
+
const facts = buildContextFacts({
|
|
221
|
+
connectorFacts: turn.turnContext ?? [],
|
|
222
|
+
speaker,
|
|
223
|
+
now,
|
|
224
|
+
timeZone: deps?.timeZone ?? systemTimeZone(),
|
|
225
|
+
previous: foldContextFacts(all),
|
|
226
|
+
previousAt: lastContextAt(all),
|
|
227
|
+
});
|
|
228
|
+
if (facts.length) {
|
|
229
|
+
await putMessage({
|
|
230
|
+
conversationId,
|
|
231
|
+
msgId: `${userMsgId}~c`,
|
|
232
|
+
role: 'context',
|
|
233
|
+
text: JSON.stringify(facts),
|
|
234
|
+
ts: ts - 1,
|
|
235
|
+
});
|
|
100
236
|
}
|
|
101
|
-
const
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
const
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
...(conversation.turnContext ? { context: conversation.turnContext } : {}),
|
|
109
|
-
}
|
|
110
|
-
: m);
|
|
111
|
-
const base = await agentBase();
|
|
112
|
-
const system = buildSystem({ base, channelSystem: conversation.channelSystem });
|
|
237
|
+
const agentMessages = toConvMessages(stored, facts, userMsgId, queryText, all);
|
|
238
|
+
const authored = turn.systemPrompt ?? {};
|
|
239
|
+
const domain = await (deps?.domainAreas ?? cachedDomainAreas)();
|
|
240
|
+
const areas = mergeAreas({ root: [await agentBase()] }, domain, authored);
|
|
241
|
+
const system = assembleSystem(areas, deps?.world ?? (await liveWorld()));
|
|
242
|
+
if (turn.signal?.aborted)
|
|
243
|
+
return;
|
|
113
244
|
db?.logTurn({
|
|
114
245
|
connector: connectorId,
|
|
115
|
-
chatId,
|
|
116
|
-
userId: sender.
|
|
246
|
+
chatId: conversationId,
|
|
247
|
+
userId: String(sender.userId),
|
|
117
248
|
role: 'user',
|
|
118
249
|
text: queryText,
|
|
119
250
|
tokensIn: null,
|
|
120
251
|
tokensOut: null,
|
|
121
252
|
ms: null,
|
|
122
253
|
agentId: selectedAgentId ?? null,
|
|
123
|
-
presetId:
|
|
254
|
+
presetId: turn.presetId ?? null,
|
|
124
255
|
});
|
|
125
256
|
const startedAt = Date.now();
|
|
126
|
-
const
|
|
127
|
-
|
|
257
|
+
const botMsgId = newMessageId();
|
|
258
|
+
const sink = openSink(connector, turn.envelope, botMsgId);
|
|
259
|
+
if (!sink)
|
|
128
260
|
return;
|
|
129
|
-
let
|
|
130
|
-
|
|
261
|
+
let usage;
|
|
262
|
+
let answerText = null;
|
|
263
|
+
let reasoningText = null;
|
|
264
|
+
let runFailed = false;
|
|
265
|
+
let agentTurn;
|
|
131
266
|
try {
|
|
132
|
-
const attachmentTools = await makeAttachmentCapabilities(agentMessages.flatMap((m) => m.attachments ?? []));
|
|
267
|
+
const attachmentTools = await makeAttachmentCapabilities(agentMessages.flatMap((m) => m.attachments ?? []), agentMedia());
|
|
133
268
|
const toolProvider = {
|
|
134
|
-
tools: [...attachmentTools.tools, ...makeSystemCapabilities().tools
|
|
269
|
+
tools: [...attachmentTools.tools, ...makeSystemCapabilities().tools],
|
|
135
270
|
};
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
271
|
+
const userMessageCount = await countUserMessages(conversationId);
|
|
272
|
+
agentTurn = {
|
|
273
|
+
envelope: turn.envelope,
|
|
274
|
+
body: { system, messages: agentMessages, userMessageCount },
|
|
139
275
|
toolProvider,
|
|
140
|
-
...(
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
276
|
+
...(turn.presetId ? { presetId: turn.presetId } : {}),
|
|
277
|
+
senderRole: speaker.role,
|
|
278
|
+
...(turn.signal ? { signal: turn.signal } : {}),
|
|
279
|
+
};
|
|
280
|
+
await agent(agentTurn, (event) => {
|
|
281
|
+
try {
|
|
282
|
+
if (event.kind === 'usage') {
|
|
283
|
+
usage = { tokensIn: event.tokensIn, tokensOut: event.tokensOut, presetId: event.presetId };
|
|
144
284
|
}
|
|
145
|
-
|
|
146
|
-
|
|
285
|
+
else if (event.kind === 'reasoning') {
|
|
286
|
+
reasoningText = event.text;
|
|
147
287
|
}
|
|
148
|
-
|
|
149
|
-
|
|
288
|
+
else if (event.kind === 'answer') {
|
|
289
|
+
answerText = event.text;
|
|
290
|
+
if (event.reasoning !== null) {
|
|
291
|
+
reasoningText = event.reasoning;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
sink.emit(event);
|
|
295
|
+
}
|
|
296
|
+
catch (err) {
|
|
297
|
+
log.error(`connector error in ${event.kind}: ${err instanceof Error ? err.message : String(err)}`);
|
|
298
|
+
}
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
catch (err) {
|
|
302
|
+
runFailed = true;
|
|
303
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
304
|
+
log.error(`agent error: ${message}`);
|
|
305
|
+
recordDiagnostic('error', 'agent.run', message);
|
|
306
|
+
try {
|
|
307
|
+
sink.emit({ kind: 'error', message });
|
|
308
|
+
}
|
|
309
|
+
catch (sinkErr) {
|
|
310
|
+
log.error(`connector error in error: ${sinkErr instanceof Error ? sinkErr.message : String(sinkErr)}`);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
await safeCall(() => sink.done(), undefined, 'done');
|
|
314
|
+
const capabilities = turn.capabilities ?? { events: [], privateChats: false };
|
|
315
|
+
const botTs = Math.max(ts + 2, Math.floor(Date.now() / 1000));
|
|
316
|
+
if (answerText !== null) {
|
|
317
|
+
await putMessage({
|
|
318
|
+
conversationId,
|
|
319
|
+
msgId: botMsgId,
|
|
320
|
+
role: 'assistant',
|
|
321
|
+
sender: null,
|
|
322
|
+
text: answerText,
|
|
323
|
+
ts: botTs,
|
|
324
|
+
replyToId: userMsgId,
|
|
325
|
+
});
|
|
326
|
+
if (reasoningText && capabilities.events.includes('reasoning')) {
|
|
327
|
+
await putMessage({
|
|
328
|
+
conversationId,
|
|
329
|
+
msgId: `${botMsgId}~r`,
|
|
330
|
+
role: 'reasoning',
|
|
331
|
+
text: reasoningText,
|
|
332
|
+
ts: botTs - 1,
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
let extraSuggestions;
|
|
337
|
+
if (!runFailed && answerText && agentTurn) {
|
|
338
|
+
const events = capabilities.events;
|
|
339
|
+
const want = { suggestions: events.includes('suggestions'), title: events.includes('title') };
|
|
340
|
+
const afterwordFn = deps?.afterword ?? runtime?.afterword;
|
|
341
|
+
if ((want.suggestions || want.title) && afterwordFn) {
|
|
342
|
+
let extra;
|
|
343
|
+
try {
|
|
344
|
+
extra = await afterwordFn(agentTurn, answerText, want);
|
|
345
|
+
}
|
|
346
|
+
catch (err) {
|
|
347
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
348
|
+
recordDiagnostic('error', 'agent.afterword', message);
|
|
349
|
+
}
|
|
350
|
+
if (extra?.suggestions?.length) {
|
|
351
|
+
extraSuggestions = extra.suggestions;
|
|
150
352
|
try {
|
|
151
|
-
|
|
353
|
+
sink.emit({ kind: 'suggestions', items: extra.suggestions });
|
|
152
354
|
}
|
|
153
355
|
catch (err) {
|
|
154
|
-
log.error(`connector error in
|
|
356
|
+
log.error(`connector error in suggestions: ${err instanceof Error ? err.message : String(err)}`);
|
|
155
357
|
}
|
|
156
|
-
}
|
|
157
|
-
|
|
358
|
+
}
|
|
359
|
+
if (extra?.title) {
|
|
158
360
|
try {
|
|
159
|
-
|
|
361
|
+
sink.emit({ kind: 'title', text: extra.title });
|
|
160
362
|
}
|
|
161
363
|
catch (err) {
|
|
162
|
-
log.error(`connector error in
|
|
364
|
+
log.error(`connector error in title: ${err instanceof Error ? err.message : String(err)}`);
|
|
163
365
|
}
|
|
164
|
-
}
|
|
165
|
-
|
|
366
|
+
}
|
|
367
|
+
if (usage && extra && (extra.tokensIn !== undefined || extra.tokensOut !== undefined)) {
|
|
368
|
+
usage = {
|
|
369
|
+
...usage,
|
|
370
|
+
tokensIn: usage.tokensIn === null ? null : usage.tokensIn + (extra.tokensIn ?? 0),
|
|
371
|
+
tokensOut: usage.tokensOut === null ? null : usage.tokensOut + (extra.tokensOut ?? 0),
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
}
|
|
166
375
|
}
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
376
|
+
await safeCall(() => sink.done(), undefined, 'done');
|
|
377
|
+
if (extraSuggestions?.length && capabilities.events.includes('suggestions')) {
|
|
378
|
+
await putMessage({
|
|
379
|
+
conversationId,
|
|
380
|
+
msgId: `${botMsgId}~s`,
|
|
381
|
+
role: 'suggestions',
|
|
382
|
+
text: JSON.stringify(extraSuggestions),
|
|
383
|
+
ts: botTs - 1,
|
|
384
|
+
});
|
|
171
385
|
}
|
|
172
|
-
if (
|
|
173
|
-
result.suggestions = suggestionCaps.getCaptured();
|
|
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)
|
|
386
|
+
if (!runFailed && usage && answerText)
|
|
180
387
|
db?.logTurn({
|
|
181
388
|
connector: connectorId,
|
|
182
|
-
chatId,
|
|
183
|
-
userId: sender.
|
|
389
|
+
chatId: conversationId,
|
|
390
|
+
userId: String(sender.userId),
|
|
184
391
|
role: 'assistant',
|
|
185
|
-
text:
|
|
186
|
-
tokensIn:
|
|
187
|
-
tokensOut:
|
|
392
|
+
text: answerText,
|
|
393
|
+
tokensIn: usage.tokensIn,
|
|
394
|
+
tokensOut: usage.tokensOut,
|
|
188
395
|
ms: Date.now() - startedAt,
|
|
189
396
|
agentId: selectedAgentId ?? null,
|
|
190
|
-
presetId:
|
|
397
|
+
presetId: usage.presetId,
|
|
191
398
|
});
|
|
192
|
-
await safeCall(() => connector.recordAssistant({
|
|
193
|
-
parentMsgId: incomingMsgId,
|
|
194
|
-
botMsgId,
|
|
195
|
-
text: result?.text ?? '',
|
|
196
|
-
reasoning: result?.reasoning ?? null,
|
|
197
|
-
}), undefined, 'recordAssistant');
|
|
198
399
|
}
|
|
@@ -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,6 @@ 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 listAgentCatalog(): Promise<
|
|
11
|
+
export declare function listAgentCatalog(): Promise<AgentCatalogEntry[]>;
|
|
12
12
|
export declare function getDefaultAgentId(): string | undefined;
|
|
13
13
|
export declare function clearRuntimeRegistries(): void;
|
|
@@ -56,7 +56,7 @@ export async function listAgentCatalog() {
|
|
|
56
56
|
if (!runtime)
|
|
57
57
|
continue;
|
|
58
58
|
try {
|
|
59
|
-
out.push(await runtime.describe());
|
|
59
|
+
out.push({ ...(await runtime.describe()), media: runtime.media });
|
|
60
60
|
}
|
|
61
61
|
catch (err) {
|
|
62
62
|
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;
|