@coffer-org/server 7.2.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/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-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/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 +15 -9
- package/dist/orchestrator/index.js +13 -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 +101 -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
|
@@ -13,7 +13,7 @@ async function swallow(fn, fallback, label) {
|
|
|
13
13
|
return fallback;
|
|
14
14
|
}
|
|
15
15
|
}
|
|
16
|
-
export function
|
|
16
|
+
export function makeLiveSink(o) {
|
|
17
17
|
let msgId = null;
|
|
18
18
|
let pending = '';
|
|
19
19
|
let lastSent = '';
|
|
@@ -62,39 +62,56 @@ export function makeLiveChannel(o) {
|
|
|
62
62
|
}, o.throttleMs);
|
|
63
63
|
if (typeof timer.unref === 'function')
|
|
64
64
|
timer.unref();
|
|
65
|
+
async function deliver(r) {
|
|
66
|
+
closed = true;
|
|
67
|
+
clearInterval(timer);
|
|
68
|
+
await inFlight;
|
|
69
|
+
const parts = o.render(r);
|
|
70
|
+
const [head, ...rest] = parts;
|
|
71
|
+
if (msgId && head !== undefined) {
|
|
72
|
+
const id = msgId;
|
|
73
|
+
if (head !== lastSent) {
|
|
74
|
+
await swallow(() => o.ops.edit(id, head), undefined, 'edit(final)');
|
|
75
|
+
lastSent = head;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
else if (head !== undefined) {
|
|
79
|
+
await swallow(() => o.ops.send(head), null, 'send(final)');
|
|
80
|
+
}
|
|
81
|
+
for (const p of rest) {
|
|
82
|
+
await swallow(() => o.ops.send(p), null, 'send(overflow)');
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
let finished = Promise.resolve();
|
|
86
|
+
let terminalDelivered = false;
|
|
87
|
+
function deliverTerminal(kind, r) {
|
|
88
|
+
if (terminalDelivered) {
|
|
89
|
+
log.warn(`live-message: a second terminal event (${kind}) after the turn already finished — dropped`);
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
terminalDelivered = true;
|
|
93
|
+
finished = deliver(r);
|
|
94
|
+
}
|
|
65
95
|
return {
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
96
|
+
emit(e) {
|
|
97
|
+
if (e.kind === 'delta')
|
|
98
|
+
pending = e.text;
|
|
99
|
+
else if (e.kind === 'segment') {
|
|
100
|
+
gen++;
|
|
101
|
+
msgId = null;
|
|
102
|
+
pending = '';
|
|
103
|
+
lastSent = '';
|
|
104
|
+
}
|
|
105
|
+
else if (e.kind === 'answer')
|
|
106
|
+
deliverTerminal('answer', { text: e.text, reasoning: e.reasoning });
|
|
107
|
+
else if (e.kind === 'notice')
|
|
108
|
+
deliverTerminal('notice', { text: e.text, reasoning: null });
|
|
109
|
+
else if (e.kind === 'error')
|
|
110
|
+
deliverTerminal('error', { text: null, reasoning: null });
|
|
74
111
|
},
|
|
75
|
-
|
|
76
|
-
closed = true;
|
|
112
|
+
done: () => {
|
|
77
113
|
clearInterval(timer);
|
|
78
|
-
|
|
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;
|
|
114
|
+
return finished;
|
|
98
115
|
},
|
|
99
116
|
};
|
|
100
117
|
}
|
|
@@ -1,11 +1,32 @@
|
|
|
1
|
-
import type { Connector,
|
|
1
|
+
import type { AgentMediaLimits, Connector, TurnRequest, GatePolicy, AgentRuntime } from './types.ts';
|
|
2
2
|
import type { LogDb } from './db.ts';
|
|
3
|
+
import { type AreaWorld, type SystemAreas } from './system-areas.ts';
|
|
4
|
+
import type { AuthUser } from '../auth-store.ts';
|
|
3
5
|
export declare function setLogDb(db: LogDb | undefined): void;
|
|
6
|
+
export declare const NOTICE_THROTTLE: {
|
|
7
|
+
readonly max: 5;
|
|
8
|
+
readonly windowMs: 60000;
|
|
9
|
+
};
|
|
4
10
|
export type RunAgentFn = AgentRuntime['run'];
|
|
5
|
-
export
|
|
11
|
+
export declare function liveWorld(): Promise<AreaWorld>;
|
|
12
|
+
export type UserLookup = (id: number) => Promise<AuthUser | null>;
|
|
13
|
+
export declare function defaultResolveUser(id: string, lookup?: UserLookup): Promise<AuthUser | null>;
|
|
14
|
+
export declare function defaultResolveLinkedUser(connectorId: string, externalId: string, lookup?: UserLookup): Promise<AuthUser | null>;
|
|
15
|
+
export declare function resolveSpeaker(connectorId: string, sender: TurnRequest['sender'], deps?: Pick<PipelineDeps, 'resolveUser' | 'resolveLinkedUser'>): Promise<AuthUser | null>;
|
|
16
|
+
export interface PipelineOptions {
|
|
17
|
+
policy?: GatePolicy;
|
|
18
|
+
}
|
|
19
|
+
export interface PipelineDeps extends PipelineOptions {
|
|
6
20
|
runAgent?: RunAgentFn;
|
|
21
|
+
afterword?: AgentRuntime['afterword'];
|
|
7
22
|
logDb?: LogDb | null;
|
|
8
|
-
policy?: GatePolicy;
|
|
9
23
|
agentBase?: () => Promise<string>;
|
|
24
|
+
media?: AgentMediaLimits;
|
|
25
|
+
resolveUser?: (id: string) => Promise<AuthUser | null>;
|
|
26
|
+
resolveLinkedUser?: (connectorId: string, externalId: string) => Promise<AuthUser | null>;
|
|
27
|
+
now?: () => Date;
|
|
28
|
+
timeZone?: string;
|
|
29
|
+
world?: AreaWorld;
|
|
30
|
+
domainAreas?: () => Promise<SystemAreas>;
|
|
10
31
|
}
|
|
11
|
-
export declare function handleIncoming(connector: Connector,
|
|
32
|
+
export declare function handleIncoming(connector: Connector, turn: TurnRequest, deps?: PipelineDeps): Promise<void>;
|
|
@@ -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(),
|