@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.
Files changed (74) hide show
  1. package/dist/auth-api.d.ts +4 -0
  2. package/dist/auth-api.js +52 -0
  3. package/dist/auth-store.d.ts +2 -0
  4. package/dist/auth-store.js +1 -0
  5. package/dist/compute-unit.js +0 -3
  6. package/dist/conversation-store.d.ts +61 -0
  7. package/dist/conversation-store.js +223 -0
  8. package/dist/entity-schema.d.ts +5 -2
  9. package/dist/entity-schema.js +35 -18
  10. package/dist/identity-link.d.ts +15 -0
  11. package/dist/identity-link.js +76 -0
  12. package/dist/identity-providers.d.ts +17 -0
  13. package/dist/identity-providers.js +14 -0
  14. package/dist/index.js +8 -1
  15. package/dist/mcp-contract/schema.d.ts +0 -1
  16. package/dist/mcp-contract/schema.js +0 -2
  17. package/dist/mcp-http.js +7 -3
  18. package/dist/mcp-tools.d.ts +4 -3
  19. package/dist/mcp-tools.js +84 -84
  20. package/dist/media/image.d.ts +23 -0
  21. package/dist/media/image.js +103 -0
  22. package/dist/media/index.d.ts +1 -0
  23. package/dist/media/index.js +1 -0
  24. package/dist/migrations.js +1 -1
  25. package/dist/mutate.js +0 -15
  26. package/dist/orchestrator/agent-capabilities.d.ts +2 -2
  27. package/dist/orchestrator/agent-capabilities.js +3 -3
  28. package/dist/orchestrator/allow.d.ts +1 -16
  29. package/dist/orchestrator/allow.js +3 -53
  30. package/dist/orchestrator/config.js +0 -1
  31. package/dist/orchestrator/context-facts.d.ts +27 -0
  32. package/dist/orchestrator/context-facts.js +89 -0
  33. package/dist/orchestrator/conversation-access.d.ts +9 -0
  34. package/dist/orchestrator/conversation-access.js +12 -0
  35. package/dist/orchestrator/draft-message.d.ts +18 -0
  36. package/dist/orchestrator/draft-message.js +85 -0
  37. package/dist/orchestrator/environment.d.ts +1 -0
  38. package/dist/orchestrator/environment.js +10 -0
  39. package/dist/orchestrator/file-inspection.d.ts +2 -2
  40. package/dist/orchestrator/file-inspection.js +41 -19
  41. package/dist/orchestrator/index.d.ts +14 -9
  42. package/dist/orchestrator/index.js +6 -7
  43. package/dist/orchestrator/live-message.d.ts +7 -4
  44. package/dist/orchestrator/live-message.js +52 -32
  45. package/dist/orchestrator/pipeline.d.ts +22 -4
  46. package/dist/orchestrator/pipeline.js +316 -115
  47. package/dist/orchestrator/registry.d.ts +2 -2
  48. package/dist/orchestrator/registry.js +1 -1
  49. package/dist/orchestrator/system-areas.d.ts +12 -0
  50. package/dist/orchestrator/system-areas.js +63 -0
  51. package/dist/orchestrator/system-capabilities.js +1 -1
  52. package/dist/orchestrator/turn-context.d.ts +18 -0
  53. package/dist/orchestrator/turn-context.js +39 -0
  54. package/dist/orchestrator/types.d.ts +100 -49
  55. package/dist/plugin-hooks.d.ts +38 -1
  56. package/dist/plugin-hooks.js +4 -0
  57. package/dist/plugin-http-mounts.d.ts +18 -0
  58. package/dist/plugin-http-mounts.js +94 -0
  59. package/dist/plugin-runtime.d.ts +4 -0
  60. package/dist/plugin-runtime.js +9 -5
  61. package/dist/plugins-api.d.ts +8 -2
  62. package/dist/plugins-api.js +4 -2
  63. package/dist/records-api.js +15 -3
  64. package/dist/settings-write.d.ts +1 -2
  65. package/dist/settings-write.js +1 -2
  66. package/dist/system-settings.js +0 -1
  67. package/dist/temporal.js +8 -3
  68. package/dist/thread-state.d.ts +14 -0
  69. package/dist/thread-state.js +71 -11
  70. package/dist/thread-store.d.ts +5 -3
  71. package/dist/thread-store.js +12 -9
  72. package/dist/turn-gate.d.ts +8 -0
  73. package/dist/turn-gate.js +39 -0
  74. 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 { buildSystem } from "./system-assembly.js";
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
- function openChannel(connector, chatId, ctx) {
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.reply(chatId, ctx);
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 reply: ${message}`);
42
- recordDiagnostic('error', 'connector.reply', message);
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, conversation, deps) {
171
+ export async function handleIncoming(connector, turn, deps) {
58
172
  const policy = deps?.policy ?? (await loadGatePolicy());
59
- const selectedAgentId = conversation.agentId ?? policy.agentId ?? (deps?.runAgent ? undefined : await loadAgentId());
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, chatId, sender, messages } = conversation;
65
- const last = messages[messages.length - 1];
66
- if (!last || last.role !== 'user')
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 incomingMsgId = last.msgId;
69
- const allowFile = allowFileFor(connectorId);
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
- const queryText = policy.triggerPrefix ? last.content.slice(policy.triggerPrefix.length).trim() : last.content.trim();
95
- if (!queryText) {
96
- if (conversation.prepareAttachments && !policy.triggerPrefix) {
97
- await safeCall(() => conversation.prepareAttachments(attachmentMaterializer), messages, 'prepareAttachments');
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 preparedMessages = conversation.prepareAttachments
102
- ? await safeCall(() => conversation.prepareAttachments(attachmentMaterializer), messages, 'prepareAttachments')
103
- : messages;
104
- const agentMessages = preparedMessages.map((m, i) => i === preparedMessages.length - 1
105
- ? {
106
- ...m,
107
- ...(policy.triggerPrefix ? { content: queryText } : {}),
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.id,
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: conversation.presetId ?? null,
254
+ presetId: turn.presetId ?? null,
124
255
  });
125
256
  const startedAt = Date.now();
126
- const ch = openChannel(connector, chatId, { parentMsgId: incomingMsgId });
127
- if (!ch)
257
+ const botMsgId = newMessageId();
258
+ const sink = openSink(connector, turn.envelope, botMsgId);
259
+ if (!sink)
128
260
  return;
129
- let result;
130
- const suggestionCaps = conversation.supportsSuggestions ? makeSuggestionCapabilities() : undefined;
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, ...(suggestionCaps?.tools ?? [])],
269
+ tools: [...attachmentTools.tools, ...makeSystemCapabilities().tools],
135
270
  };
136
- result = await agent({
137
- system,
138
- messages: agentMessages,
271
+ const userMessageCount = await countUserMessages(conversationId);
272
+ agentTurn = {
273
+ envelope: turn.envelope,
274
+ body: { system, messages: agentMessages, userMessageCount },
139
275
  toolProvider,
140
- ...(conversation.presetId ? { presetId: conversation.presetId } : {}),
141
- onDelta: (acc) => {
142
- try {
143
- ch.update(acc);
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
- catch (err) {
146
- log.error(`connector error in update: ${err instanceof Error ? err.message : String(err)}`);
285
+ else if (event.kind === 'reasoning') {
286
+ reasoningText = event.text;
147
287
  }
148
- },
149
- onReasoning: (acc) => {
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
- ch.updateReasoning?.(acc);
353
+ sink.emit({ kind: 'suggestions', items: extra.suggestions });
152
354
  }
153
355
  catch (err) {
154
- log.error(`connector error in updateReasoning: ${err instanceof Error ? err.message : String(err)}`);
356
+ log.error(`connector error in suggestions: ${err instanceof Error ? err.message : String(err)}`);
155
357
  }
156
- },
157
- onSegment: () => {
358
+ }
359
+ if (extra?.title) {
158
360
  try {
159
- ch.segment();
361
+ sink.emit({ kind: 'title', text: extra.title });
160
362
  }
161
363
  catch (err) {
162
- log.error(`connector error in segment: ${err instanceof Error ? err.message : String(err)}`);
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
- catch (err) {
168
- const message = err instanceof Error ? err.message : String(err);
169
- log.error(`agent error: ${message}`);
170
- recordDiagnostic('error', 'agent.run', message);
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 (result && suggestionCaps)
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.id,
389
+ chatId: conversationId,
390
+ userId: String(sender.userId),
184
391
  role: 'assistant',
185
- text: result.text,
186
- tokensIn: result.tokensIn,
187
- tokensOut: result.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: result.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 { AgentDescriptor, AgentRuntime, ConnectorRegistration } from './types.ts';
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<AgentDescriptor[]>;
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
- 'The current date and time are already given in the system prompt; do not call this tool to find them.',
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;