@coffer-org/server 3.3.0 → 4.0.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 (112) hide show
  1. package/dist/auth-api.js +4 -5
  2. package/dist/cache-clock.d.ts +8 -0
  3. package/dist/cache-clock.js +103 -0
  4. package/dist/cache-refresh.d.ts +7 -0
  5. package/dist/cache-refresh.js +322 -0
  6. package/dist/collection-io.d.ts +13 -0
  7. package/dist/collection-io.js +42 -28
  8. package/dist/compute-unit.d.ts +66 -0
  9. package/dist/compute-unit.js +369 -0
  10. package/dist/condition-where.js +1 -3
  11. package/dist/data-dir.d.ts +3 -0
  12. package/dist/data-dir.js +39 -0
  13. package/dist/db.js +2 -0
  14. package/dist/entity-schema.d.ts +1 -0
  15. package/dist/entity-schema.js +25 -2
  16. package/dist/extend-io.d.ts +3 -2
  17. package/dist/extend-io.js +4 -4
  18. package/dist/extend-table.d.ts +3 -2
  19. package/dist/extend-table.js +48 -11
  20. package/dist/file-fields.js +50 -23
  21. package/dist/global-search.js +1 -1
  22. package/dist/http-errors.d.ts +7 -0
  23. package/dist/http-errors.js +24 -0
  24. package/dist/index.js +31 -35
  25. package/dist/mcp-contract/client.d.ts +24 -0
  26. package/dist/mcp-contract/client.js +12 -0
  27. package/dist/mcp-contract/schema.d.ts +46 -0
  28. package/dist/mcp-contract/schema.js +90 -0
  29. package/dist/mcp-contract/tools.d.ts +19 -0
  30. package/dist/mcp-contract/tools.js +160 -0
  31. package/dist/mcp-local.d.ts +1 -1
  32. package/dist/mcp-local.js +4 -2
  33. package/dist/mcp-tools.d.ts +12 -1
  34. package/dist/mcp-tools.js +41 -8
  35. package/dist/migrations.d.ts +30 -2
  36. package/dist/migrations.js +368 -19
  37. package/dist/mutate.js +86 -57
  38. package/dist/orchestrator/agent-capabilities.d.ts +2 -0
  39. package/dist/orchestrator/agent-capabilities.js +201 -0
  40. package/dist/orchestrator/allow.d.ts +16 -0
  41. package/dist/orchestrator/allow.js +65 -0
  42. package/dist/orchestrator/attachments.d.ts +2 -0
  43. package/dist/orchestrator/attachments.js +11 -0
  44. package/dist/orchestrator/config.d.ts +4 -0
  45. package/dist/orchestrator/config.js +19 -0
  46. package/dist/orchestrator/db.d.ts +17 -0
  47. package/dist/orchestrator/db.js +12 -0
  48. package/dist/orchestrator/diagnostics.d.ts +9 -0
  49. package/dist/orchestrator/diagnostics.js +13 -0
  50. package/dist/orchestrator/environment.d.ts +3 -0
  51. package/dist/orchestrator/environment.js +30 -0
  52. package/dist/orchestrator/file-inspection.d.ts +3 -0
  53. package/dist/orchestrator/file-inspection.js +61 -0
  54. package/dist/orchestrator/format.d.ts +1 -0
  55. package/dist/orchestrator/format.js +6 -0
  56. package/dist/orchestrator/index.d.ts +30 -0
  57. package/dist/orchestrator/index.js +56 -0
  58. package/dist/orchestrator/live-message.d.ts +14 -0
  59. package/dist/orchestrator/live-message.js +100 -0
  60. package/dist/orchestrator/pipeline.d.ts +11 -0
  61. package/dist/orchestrator/pipeline.js +198 -0
  62. package/dist/orchestrator/registry.d.ts +13 -0
  63. package/dist/orchestrator/registry.js +74 -0
  64. package/dist/orchestrator/starters.d.ts +19 -0
  65. package/dist/orchestrator/starters.js +81 -0
  66. package/dist/orchestrator/suggestion-capabilities.d.ts +5 -0
  67. package/dist/orchestrator/suggestion-capabilities.js +33 -0
  68. package/dist/orchestrator/system-assembly.d.ts +7 -0
  69. package/dist/orchestrator/system-assembly.js +13 -0
  70. package/dist/orchestrator/system-capabilities.d.ts +2 -0
  71. package/dist/orchestrator/system-capabilities.js +63 -0
  72. package/dist/orchestrator/types.d.ts +144 -0
  73. package/dist/orchestrator/types.js +1 -0
  74. package/dist/part-errors.d.ts +3 -0
  75. package/dist/part-errors.js +6 -0
  76. package/dist/part-injection.d.ts +11 -3
  77. package/dist/part-injection.js +72 -92
  78. package/dist/part-ref.d.ts +13 -0
  79. package/dist/part-ref.js +109 -0
  80. package/dist/plugin-hooks.d.ts +1 -0
  81. package/dist/plugin-i18n.js +2 -1
  82. package/dist/plugin-runtime.d.ts +2 -2
  83. package/dist/plugin-runtime.js +21 -11
  84. package/dist/plugin-updates.d.ts +2 -2
  85. package/dist/plugin-updates.js +6 -12
  86. package/dist/public-url.js +2 -1
  87. package/dist/recompute-derived.d.ts +2 -1
  88. package/dist/recompute-derived.js +123 -26
  89. package/dist/records-api.d.ts +5 -1
  90. package/dist/records-api.js +47 -15
  91. package/dist/registry-context.d.ts +1 -1
  92. package/dist/registry-context.js +3 -0
  93. package/dist/rename-guard.d.ts +1 -0
  94. package/dist/rename-guard.js +9 -0
  95. package/dist/schema-sync.js +38 -1
  96. package/dist/search-index.js +1 -1
  97. package/dist/search-indexer.d.ts +1 -1
  98. package/dist/search-indexer.js +3 -3
  99. package/dist/settings-groups.d.ts +14 -0
  100. package/dist/settings-groups.js +25 -0
  101. package/dist/settings-write.d.ts +4 -4
  102. package/dist/settings-write.js +23 -26
  103. package/dist/system-settings.d.ts +3 -0
  104. package/dist/system-settings.js +27 -0
  105. package/dist/thread-store.d.ts +3 -2
  106. package/dist/thread-store.js +6 -5
  107. package/dist/trigger-queue.d.ts +33 -0
  108. package/dist/trigger-queue.js +432 -0
  109. package/dist/uploads.js +6 -3
  110. package/dist/value-compare.d.ts +1 -0
  111. package/dist/value-compare.js +7 -0
  112. package/package.json +17 -3
@@ -0,0 +1,61 @@
1
+ import { readFile, stat } from 'node:fs/promises';
2
+ import { basename, join } from 'node:path';
3
+ import { mimeForName } from "../file-fields.js";
4
+ import { uploadsDir } from "../uploads.js";
5
+ const MAX_VISION_BYTES = 5 * 1024 * 1024;
6
+ const MAX_DOCUMENT_BYTES = 10 * 1024 * 1024;
7
+ const MAX_TEXT_BYTES = 512 * 1024;
8
+ const textResult = (text, isError = false) => ({
9
+ __cofferToolContent: true,
10
+ content: [{ type: 'text', text }],
11
+ ...(isError ? { isError: true } : {}),
12
+ });
13
+ export async function inspectUpload(name, ref = {}) {
14
+ if (!name || basename(name) !== name)
15
+ return textResult('Invalid upload name.', true);
16
+ const file = join(uploadsDir(), name);
17
+ let size;
18
+ try {
19
+ size = (await stat(file)).size;
20
+ }
21
+ catch {
22
+ return textResult(`File is no longer present on the server: ${name}`, true);
23
+ }
24
+ const mime = ref.mime ?? mimeForName(name);
25
+ if (mime === 'image/jpeg' || mime === 'image/png' || mime === 'image/gif' || mime === 'image/webp') {
26
+ if (size > MAX_VISION_BYTES)
27
+ return textResult(`Image is ${size} bytes, above the ${MAX_VISION_BYTES}-byte inspection limit.`, true);
28
+ const bytes = await readFile(file);
29
+ return {
30
+ __cofferToolContent: true,
31
+ content: [
32
+ { type: 'text', text: `Image: ${ref.label ?? name} (${mime}, ${size} bytes).` },
33
+ { type: 'image', mime, data: bytes.toString('base64') },
34
+ ],
35
+ };
36
+ }
37
+ if (mime === 'application/pdf') {
38
+ if (size > MAX_DOCUMENT_BYTES)
39
+ return textResult(`PDF is ${size} bytes, above the ${MAX_DOCUMENT_BYTES}-byte inspection limit.`, true);
40
+ const bytes = await readFile(file);
41
+ return {
42
+ __cofferToolContent: true,
43
+ content: [
44
+ { type: 'text', text: `PDF: ${ref.label ?? name} (${size} bytes).` },
45
+ { type: 'document', mime: 'application/pdf', data: bytes.toString('base64') },
46
+ ],
47
+ };
48
+ }
49
+ if (mime?.startsWith('text/') || mime === 'application/json' || mime === 'text/csv') {
50
+ if (size > MAX_TEXT_BYTES)
51
+ return textResult(`Text file is too large to inspect: ${size} bytes.`, true);
52
+ return textResult(`Text file: ${ref.label ?? name}\n\n${(await readFile(file)).toString('utf8')}`);
53
+ }
54
+ return textResult(`This file type cannot be inspected by the agent (${mime ?? 'unknown'}). ` +
55
+ 'It can still be attached to a record.', true);
56
+ }
57
+ export function isAgentToolContentResult(value) {
58
+ return (typeof value === 'object' &&
59
+ value !== null &&
60
+ value.__cofferToolContent === true);
61
+ }
@@ -0,0 +1 @@
1
+ export declare function chunk(text: string, max: number): string[];
@@ -0,0 +1,6 @@
1
+ export function chunk(text, max) {
2
+ const out = [];
3
+ for (let i = 0; i < text.length; i += max)
4
+ out.push(text.slice(i, i + max));
5
+ return out.length ? out : [''];
6
+ }
@@ -0,0 +1,30 @@
1
+ import type { BackgroundTask } from '../background-scheduler.ts';
2
+ import { listRecentMessageMetrics } from '../msg-log.ts';
3
+ import { listDiagnostics } from './diagnostics.ts';
4
+ export { handleIncoming } from './pipeline.ts';
5
+ export { registerAgent, resolveAgent, registerConnector, isConnectorRegistered, clearRuntimeRegistries, listRegisteredAgents, listRegisteredConnectors, listAgentCatalog, getDefaultAgentId, liveAgentId, } from './registry.ts';
6
+ export { attachmentMaterializer } from './attachments.ts';
7
+ export { makeAttachmentCapabilities } from './agent-capabilities.ts';
8
+ export { makeSystemCapabilities } from './system-capabilities.ts';
9
+ export { makeSuggestionCapabilities } from './suggestion-capabilities.ts';
10
+ export type { SuggestionCapability } from './suggestion-capabilities.ts';
11
+ export { currentEnvironment, systemTimeZone } from './environment.ts';
12
+ export { inspectUpload, isAgentToolContentResult } from './file-inspection.ts';
13
+ export type { PipelineDeps, RunAgentFn } from './pipeline.ts';
14
+ export { buildPolicy, loadGatePolicy, loadAgentId } from './config.ts';
15
+ export { getConversationStarters, refreshSystemStarters } from './starters.ts';
16
+ export { isSenderAllowed } from './allow.ts';
17
+ export { makeLiveChannel, plainRender } from './live-message.ts';
18
+ export type { LiveChannelOps, LiveChannelOpts, RenderFn } from './live-message.ts';
19
+ export { chunk } from './format.ts';
20
+ export type { Connector, IncomingConversation, GatePolicy, ReplyContext, ReplyPayload, ReplyChannel, AttachmentRef, AttachmentMaterializer, AgentToolDefinition, AgentToolProvider, ConvMessage, SystemLayer, AgentRequest, AgentResult, AgentRuntime, AgentCapabilities, AgentPreset, AgentDescriptor, ConnectorRegistration, AgentToolContentBlock, AgentToolContentResult, } from './types.ts';
21
+ export declare function startOrchestrator(): void;
22
+ export declare function stopOrchestrator(): void;
23
+ export declare const orchestratorStartersTask: BackgroundTask;
24
+ export declare function orchestratorDiagnostics(): Promise<{
25
+ generatedAt: string;
26
+ agents: string[];
27
+ connectors: string[];
28
+ recentMessages: Awaited<ReturnType<typeof listRecentMessageMetrics>>;
29
+ recentErrors: ReturnType<typeof listDiagnostics>;
30
+ }>;
@@ -0,0 +1,56 @@
1
+ import { openLogDb } from "./db.js";
2
+ import { listRecentMessageMetrics } from "../msg-log.js";
3
+ import { listRegisteredAgents, listRegisteredConnectors } from "./registry.js";
4
+ import { listDiagnostics } from "./diagnostics.js";
5
+ import { setLogDb } from "./pipeline.js";
6
+ import { refreshSystemStarters } from "./starters.js";
7
+ import { getLogger } from '@coffer-org/sdk/logger';
8
+ const log = getLogger('orchestrator');
9
+ export { handleIncoming } from "./pipeline.js";
10
+ export { registerAgent, resolveAgent, registerConnector, isConnectorRegistered, clearRuntimeRegistries, listRegisteredAgents, listRegisteredConnectors, listAgentCatalog, getDefaultAgentId, liveAgentId, } from "./registry.js";
11
+ export { attachmentMaterializer } from "./attachments.js";
12
+ export { makeAttachmentCapabilities } from "./agent-capabilities.js";
13
+ export { makeSystemCapabilities } from "./system-capabilities.js";
14
+ export { makeSuggestionCapabilities } from "./suggestion-capabilities.js";
15
+ export { currentEnvironment, systemTimeZone } from "./environment.js";
16
+ export { inspectUpload, isAgentToolContentResult } from "./file-inspection.js";
17
+ export { buildPolicy, loadGatePolicy, loadAgentId } from "./config.js";
18
+ export { getConversationStarters, refreshSystemStarters } from "./starters.js";
19
+ export { isSenderAllowed } from "./allow.js";
20
+ import { carryLegacyState } from "./allow.js";
21
+ export { makeLiveChannel, plainRender } from "./live-message.js";
22
+ export { chunk } from "./format.js";
23
+ let db;
24
+ const STARTERS_CHECK_INTERVAL_MS = 10 * 60_000;
25
+ function runStartersRefresh() {
26
+ return refreshSystemStarters().catch((err) => log.warn(`system starters: ${String(err)}`));
27
+ }
28
+ export function startOrchestrator() {
29
+ carryLegacyState();
30
+ db = openLogDb();
31
+ setLogDb(db);
32
+ void runStartersRefresh();
33
+ }
34
+ export function stopOrchestrator() {
35
+ try {
36
+ db?.close();
37
+ }
38
+ catch {
39
+ }
40
+ db = undefined;
41
+ setLogDb(undefined);
42
+ }
43
+ export const orchestratorStartersTask = {
44
+ name: 'core:orchestrator-starters',
45
+ intervalMs: STARTERS_CHECK_INTERVAL_MS,
46
+ run: runStartersRefresh,
47
+ };
48
+ export async function orchestratorDiagnostics() {
49
+ return {
50
+ generatedAt: new Date().toISOString(),
51
+ agents: listRegisteredAgents(),
52
+ connectors: listRegisteredConnectors(),
53
+ recentMessages: await listRecentMessageMetrics(25),
54
+ recentErrors: listDiagnostics(25),
55
+ };
56
+ }
@@ -0,0 +1,14 @@
1
+ import type { ReplyChannel, ReplyPayload } from './types.ts';
2
+ export interface LiveChannelOps {
3
+ send(text: string): Promise<string | null>;
4
+ edit(msgId: string, text: string): Promise<void>;
5
+ }
6
+ export type RenderFn = (r: ReplyPayload) => string[];
7
+ export interface LiveChannelOpts {
8
+ ops: LiveChannelOps;
9
+ throttleMs: number;
10
+ render: RenderFn;
11
+ maxLength: number;
12
+ }
13
+ export declare function plainRender(max: number): RenderFn;
14
+ export declare function makeLiveChannel(o: LiveChannelOpts): ReplyChannel;
@@ -0,0 +1,100 @@
1
+ import { chunk } from "./format.js";
2
+ import { getLogger } from '@coffer-org/sdk/logger';
3
+ const log = getLogger('orchestrator');
4
+ export function plainRender(max) {
5
+ return (r) => chunk((r.text ?? '').trim() || '⚠️ the agent returned no response', max);
6
+ }
7
+ async function swallow(fn, fallback, label) {
8
+ try {
9
+ return await fn();
10
+ }
11
+ catch (err) {
12
+ log.error(`live-message ${label}: ${err instanceof Error ? err.message : String(err)}`);
13
+ return fallback;
14
+ }
15
+ }
16
+ export function makeLiveChannel(o) {
17
+ let msgId = null;
18
+ let pending = '';
19
+ let lastSent = '';
20
+ let busy = false;
21
+ let closed = false;
22
+ let gen = 0;
23
+ let inFlight = Promise.resolve();
24
+ const timer = setInterval(() => {
25
+ if (closed || busy)
26
+ return;
27
+ const t = pending.slice(0, o.maxLength);
28
+ if (!t || t === lastSent)
29
+ return;
30
+ busy = true;
31
+ const myGen = gen;
32
+ if (!msgId) {
33
+ inFlight = o.ops
34
+ .send(t)
35
+ .then((id) => {
36
+ if (gen === myGen) {
37
+ msgId = id;
38
+ lastSent = t;
39
+ }
40
+ })
41
+ .catch((err) => {
42
+ log.error(`live-message send: ${err instanceof Error ? err.message : String(err)}`);
43
+ })
44
+ .finally(() => {
45
+ busy = false;
46
+ });
47
+ return;
48
+ }
49
+ const id = msgId;
50
+ inFlight = o.ops
51
+ .edit(id, t)
52
+ .then(() => {
53
+ if (gen === myGen)
54
+ lastSent = t;
55
+ })
56
+ .catch((err) => {
57
+ log.error(`live-message edit: ${err instanceof Error ? err.message : String(err)}`);
58
+ })
59
+ .finally(() => {
60
+ busy = false;
61
+ });
62
+ }, o.throttleMs);
63
+ if (typeof timer.unref === 'function')
64
+ timer.unref();
65
+ return {
66
+ update(text) {
67
+ pending = text;
68
+ },
69
+ segment() {
70
+ gen++;
71
+ msgId = null;
72
+ pending = '';
73
+ lastSent = '';
74
+ },
75
+ async finish(r) {
76
+ closed = true;
77
+ clearInterval(timer);
78
+ await inFlight;
79
+ const parts = o.render(r);
80
+ let last = null;
81
+ const [head, ...rest] = parts;
82
+ if (msgId && head !== undefined) {
83
+ const id = msgId;
84
+ if (head !== lastSent) {
85
+ await swallow(() => o.ops.edit(id, head), undefined, 'edit(final)');
86
+ lastSent = head;
87
+ }
88
+ last = id;
89
+ }
90
+ else if (head !== undefined) {
91
+ last = await swallow(() => o.ops.send(head), null, 'send(final)');
92
+ }
93
+ for (const p of rest) {
94
+ const id = await swallow(() => o.ops.send(p), null, 'send(overflow)');
95
+ last = id ?? last;
96
+ }
97
+ return last;
98
+ },
99
+ };
100
+ }
@@ -0,0 +1,11 @@
1
+ import type { Connector, IncomingConversation, GatePolicy, AgentRuntime } from './types.ts';
2
+ import type { LogDb } from './db.ts';
3
+ export declare function setLogDb(db: LogDb | undefined): void;
4
+ export type RunAgentFn = AgentRuntime['run'];
5
+ export interface PipelineDeps {
6
+ runAgent?: RunAgentFn;
7
+ logDb?: LogDb | null;
8
+ policy?: GatePolicy;
9
+ agentBase?: () => Promise<string>;
10
+ }
11
+ export declare function handleIncoming(connector: Connector, conversation: IncomingConversation, deps?: PipelineDeps): Promise<void>;
@@ -0,0 +1,198 @@
1
+ import { loadAllowed, saveAllowed, isAllowed, addAllowed, makeThrottle, allowFileFor } from "./allow.js";
2
+ import { loadAgentId, loadGatePolicy } from "./config.js";
3
+ import { buildSystem } from "./system-assembly.js";
4
+ import { attachmentMaterializer } from "./attachments.js";
5
+ import { makeAttachmentCapabilities } from "./agent-capabilities.js";
6
+ import { makeSystemCapabilities } from "./system-capabilities.js";
7
+ import { makeSuggestionCapabilities } from "./suggestion-capabilities.js";
8
+ import { resolveAgent } from "./registry.js";
9
+ import { recordDiagnostic } from "./diagnostics.js";
10
+ import { getLogger } from '@coffer-org/sdk/logger';
11
+ const log = getLogger('orchestrator');
12
+ let logDb;
13
+ export function setLogDb(db) {
14
+ logDb = db;
15
+ }
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
+ let cachedBase;
26
+ let cachedBaseAgentId;
27
+ function cachedAgentBase() {
28
+ const runtime = resolveAgent();
29
+ if (cachedBaseAgentId !== runtime.id) {
30
+ cachedBaseAgentId = runtime.id;
31
+ cachedBase = runtime.systemBase();
32
+ }
33
+ return cachedBase;
34
+ }
35
+ function openChannel(connector, chatId, ctx) {
36
+ try {
37
+ return connector.reply(chatId, ctx);
38
+ }
39
+ catch (err) {
40
+ const message = err instanceof Error ? err.message : String(err);
41
+ log.error(`connector error in reply: ${message}`);
42
+ recordDiagnostic('error', 'connector.reply', message);
43
+ return null;
44
+ }
45
+ }
46
+ async function safeCall(fn, fallback, label) {
47
+ try {
48
+ return await fn();
49
+ }
50
+ catch (err) {
51
+ const message = err instanceof Error ? err.message : String(err);
52
+ log.error(`connector error in ${label}: ${message}`);
53
+ recordDiagnostic('error', `connector.${label}`, message);
54
+ return fallback;
55
+ }
56
+ }
57
+ export async function handleIncoming(connector, conversation, deps) {
58
+ const policy = deps?.policy ?? (await loadGatePolicy());
59
+ const selectedAgentId = conversation.agentId ?? policy.agentId ?? (deps?.runAgent ? undefined : await loadAgentId());
60
+ const runtime = deps?.runAgent ? undefined : resolveAgent(selectedAgentId);
61
+ const agent = deps?.runAgent ?? runtime.run.bind(runtime);
62
+ const db = deps && 'logDb' in deps ? deps.logDb : logDb;
63
+ 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')
67
+ 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()))
93
+ 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
+ }
99
+ return;
100
+ }
101
+ const preparedMessages = conversation.prepareAttachments
102
+ ? await safeCall(() => conversation.prepareAttachments(attachmentMaterializer), messages, 'prepareAttachments')
103
+ : messages;
104
+ const agentMessages = policy.triggerPrefix
105
+ ? preparedMessages.map((m, i) => (i === preparedMessages.length - 1 ? { ...m, content: queryText } : m))
106
+ : preparedMessages;
107
+ const base = await agentBase();
108
+ const system = buildSystem({
109
+ base,
110
+ channelSystem: conversation.channelSystem,
111
+ volatileSystem: conversation.volatileSystem,
112
+ });
113
+ db?.logTurn({
114
+ connector: connectorId,
115
+ chatId,
116
+ userId: sender.id,
117
+ role: 'user',
118
+ text: queryText,
119
+ tokensIn: null,
120
+ tokensOut: null,
121
+ ms: null,
122
+ agentId: selectedAgentId ?? null,
123
+ presetId: conversation.presetId ?? null,
124
+ });
125
+ const startedAt = Date.now();
126
+ const ch = openChannel(connector, chatId, { parentMsgId: incomingMsgId });
127
+ if (!ch)
128
+ return;
129
+ let result;
130
+ const suggestionCaps = conversation.supportsSuggestions ? makeSuggestionCapabilities() : undefined;
131
+ try {
132
+ const attachmentTools = await makeAttachmentCapabilities(agentMessages.flatMap((m) => m.attachments ?? []));
133
+ const toolProvider = {
134
+ tools: [...attachmentTools.tools, ...makeSystemCapabilities().tools, ...(suggestionCaps?.tools ?? [])],
135
+ };
136
+ result = await agent({
137
+ system,
138
+ messages: agentMessages,
139
+ toolProvider,
140
+ ...(conversation.presetId ? { presetId: conversation.presetId } : {}),
141
+ onDelta: (acc) => {
142
+ try {
143
+ ch.update(acc);
144
+ }
145
+ catch (err) {
146
+ log.error(`connector error in update: ${err instanceof Error ? err.message : String(err)}`);
147
+ }
148
+ },
149
+ onReasoning: (acc) => {
150
+ try {
151
+ ch.updateReasoning?.(acc);
152
+ }
153
+ catch (err) {
154
+ log.error(`connector error in updateReasoning: ${err instanceof Error ? err.message : String(err)}`);
155
+ }
156
+ },
157
+ onSegment: () => {
158
+ try {
159
+ ch.segment();
160
+ }
161
+ catch (err) {
162
+ log.error(`connector error in segment: ${err instanceof Error ? err.message : String(err)}`);
163
+ }
164
+ },
165
+ });
166
+ }
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);
171
+ }
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)
180
+ db?.logTurn({
181
+ connector: connectorId,
182
+ chatId,
183
+ userId: sender.id,
184
+ role: 'assistant',
185
+ text: result.text,
186
+ tokensIn: result.tokensIn,
187
+ tokensOut: result.tokensOut,
188
+ ms: Date.now() - startedAt,
189
+ agentId: selectedAgentId ?? null,
190
+ presetId: result.presetId,
191
+ });
192
+ await safeCall(() => connector.recordAssistant({
193
+ parentMsgId: incomingMsgId,
194
+ botMsgId,
195
+ text: result?.text ?? '',
196
+ reasoning: result?.reasoning ?? null,
197
+ }), undefined, 'recordAssistant');
198
+ }
@@ -0,0 +1,13 @@
1
+ import type { AgentDescriptor, AgentRuntime, ConnectorRegistration } from './types.ts';
2
+ export declare function registerAgent(runtime: AgentRuntime, opts?: {
3
+ default?: boolean;
4
+ }): () => void;
5
+ export declare function resolveAgent(id?: string): AgentRuntime;
6
+ export declare function liveAgentId(agentId: string | null | undefined): string | undefined;
7
+ export declare function registerConnector(registration: ConnectorRegistration): () => void;
8
+ export declare function isConnectorRegistered(id: string): boolean;
9
+ export declare function listRegisteredAgents(): string[];
10
+ export declare function listRegisteredConnectors(): string[];
11
+ export declare function listAgentCatalog(): Promise<AgentDescriptor[]>;
12
+ export declare function getDefaultAgentId(): string | undefined;
13
+ export declare function clearRuntimeRegistries(): void;
@@ -0,0 +1,74 @@
1
+ import { getLogger } from '@coffer-org/sdk/logger';
2
+ const log = getLogger('orchestrator');
3
+ const agents = new Map();
4
+ const connectors = new Map();
5
+ let defaultAgentId;
6
+ export function registerAgent(runtime, opts = {}) {
7
+ const current = agents.get(runtime.id);
8
+ if (current && current !== runtime)
9
+ throw new Error(`agent already registered: ${runtime.id}`);
10
+ agents.set(runtime.id, runtime);
11
+ if (opts.default || !defaultAgentId)
12
+ defaultAgentId = runtime.id;
13
+ return () => unregisterAgent(runtime.id, runtime);
14
+ }
15
+ function unregisterAgent(id, expected) {
16
+ if (agents.get(id) !== expected)
17
+ return;
18
+ agents.delete(id);
19
+ if (defaultAgentId === id)
20
+ defaultAgentId = agents.keys().next().value;
21
+ }
22
+ export function resolveAgent(id) {
23
+ const key = id ?? defaultAgentId;
24
+ const runtime = key ? agents.get(key) : undefined;
25
+ if (!runtime)
26
+ throw new Error(id ? `agent is not registered: ${id}` : 'no agent is registered');
27
+ return runtime;
28
+ }
29
+ export function liveAgentId(agentId) {
30
+ return agentId && agents.has(agentId) ? agentId : undefined;
31
+ }
32
+ export function registerConnector(registration) {
33
+ const current = connectors.get(registration.id);
34
+ if (current && current !== registration)
35
+ throw new Error(`connector already registered: ${registration.id}`);
36
+ connectors.set(registration.id, registration);
37
+ return () => {
38
+ if (connectors.get(registration.id) === registration)
39
+ connectors.delete(registration.id);
40
+ };
41
+ }
42
+ export function isConnectorRegistered(id) {
43
+ return connectors.has(id);
44
+ }
45
+ export function listRegisteredAgents() {
46
+ return [...agents.keys()].sort();
47
+ }
48
+ export function listRegisteredConnectors() {
49
+ return [...connectors.keys()].sort();
50
+ }
51
+ export async function listAgentCatalog() {
52
+ const ids = [...agents.keys()].sort();
53
+ const out = [];
54
+ for (const id of ids) {
55
+ const runtime = agents.get(id);
56
+ if (!runtime)
57
+ continue;
58
+ try {
59
+ out.push(await runtime.describe());
60
+ }
61
+ catch (err) {
62
+ log.error(`agent describe failed for ${id}: ${err instanceof Error ? err.message : String(err)}`);
63
+ }
64
+ }
65
+ return out;
66
+ }
67
+ export function getDefaultAgentId() {
68
+ return defaultAgentId;
69
+ }
70
+ export function clearRuntimeRegistries() {
71
+ agents.clear();
72
+ connectors.clear();
73
+ defaultAgentId = undefined;
74
+ }
@@ -0,0 +1,19 @@
1
+ import { getPluginSettings } from '../plugin-runtime.ts';
2
+ import { getPluginState, setPluginState } from '../plugin-state.ts';
3
+ import { collectStarterHints } from '../mcp-tools.ts';
4
+ import { loadAgentId } from './config.ts';
5
+ import { getDefaultAgentId } from './registry.ts';
6
+ import type { AgentRuntime } from './types.ts';
7
+ export interface StartersDeps {
8
+ getPluginSettings: typeof getPluginSettings;
9
+ getPluginState: typeof getPluginState;
10
+ setPluginState: typeof setPluginState;
11
+ collectStarterHints: typeof collectStarterHints;
12
+ loadAgentId: typeof loadAgentId;
13
+ resolveAgent: (id?: string) => AgentRuntime;
14
+ getDefaultAgentId: typeof getDefaultAgentId;
15
+ today: () => string;
16
+ now: () => string;
17
+ }
18
+ export declare function getConversationStarters(deps?: StartersDeps): Promise<string[]>;
19
+ export declare function refreshSystemStarters(deps?: StartersDeps): Promise<void>;
@@ -0,0 +1,81 @@
1
+ import { getPluginSettings } from "../plugin-runtime.js";
2
+ import { SYSTEM_SETTINGS_ID } from "../system-settings.js";
3
+ import { getPluginState, setPluginState } from "../plugin-state.js";
4
+ import { collectStarterHints } from "../mcp-tools.js";
5
+ import { getLogger } from '@coffer-org/sdk/logger';
6
+ import { todayDateString } from "./environment.js";
7
+ import { loadAgentId } from "./config.js";
8
+ import { resolveAgent, getDefaultAgentId } from "./registry.js";
9
+ const log = getLogger('orchestrator');
10
+ const PLUGIN = 'orchestrator';
11
+ const STATE_KEY = 'system_starters';
12
+ const defaultDeps = {
13
+ getPluginSettings,
14
+ getPluginState,
15
+ setPluginState,
16
+ collectStarterHints,
17
+ loadAgentId,
18
+ resolveAgent,
19
+ getDefaultAgentId,
20
+ today: todayDateString,
21
+ now: () => new Date().toISOString(),
22
+ };
23
+ async function currentLanguage(deps) {
24
+ const system = await deps.getPluginSettings(SYSTEM_SETTINGS_ID);
25
+ const v = system['language'];
26
+ return typeof v === 'string' && v ? v : 'en';
27
+ }
28
+ async function readCache(deps) {
29
+ const raw = await deps.getPluginState(PLUGIN, STATE_KEY);
30
+ if (!raw)
31
+ return null;
32
+ try {
33
+ return JSON.parse(raw);
34
+ }
35
+ catch {
36
+ return null;
37
+ }
38
+ }
39
+ export async function getConversationStarters(deps = defaultDeps) {
40
+ return (await readCache(deps))?.starters ?? [];
41
+ }
42
+ export async function refreshSystemStarters(deps = defaultDeps) {
43
+ const date = deps.today();
44
+ const [language, hints] = await Promise.all([currentLanguage(deps), deps.collectStarterHints()]);
45
+ const agentId = (await deps.loadAgentId()) ?? deps.getDefaultAgentId();
46
+ if (!agentId)
47
+ return;
48
+ const cached = await readCache(deps);
49
+ if (cached &&
50
+ cached.date === date &&
51
+ cached.language === language &&
52
+ cached.hintsHash === hints.hash &&
53
+ cached.agentId === agentId) {
54
+ return;
55
+ }
56
+ let runtime;
57
+ try {
58
+ runtime = deps.resolveAgent(agentId);
59
+ }
60
+ catch {
61
+ return;
62
+ }
63
+ if (!runtime.starters)
64
+ return;
65
+ let starters;
66
+ try {
67
+ starters = await runtime.starters();
68
+ }
69
+ catch (err) {
70
+ log.warn(`system starters generation failed: ${err instanceof Error ? err.message : String(err)}`);
71
+ return;
72
+ }
73
+ await deps.setPluginState(PLUGIN, STATE_KEY, JSON.stringify({
74
+ date,
75
+ language,
76
+ hintsHash: hints.hash,
77
+ agentId,
78
+ starters,
79
+ generatedAt: deps.now(),
80
+ }));
81
+ }