@coffer-org/server 7.3.0 → 7.5.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.
@@ -0,0 +1,85 @@
1
+ import { getLogger } from '@coffer-org/sdk/logger';
2
+ const log = getLogger('orchestrator');
3
+ async function swallow(fn, fallback, label) {
4
+ try {
5
+ return await fn();
6
+ }
7
+ catch (err) {
8
+ log.error(`draft-message ${label}: ${err instanceof Error ? err.message : String(err)}`);
9
+ return fallback;
10
+ }
11
+ }
12
+ export function makeDraftSink(o) {
13
+ let text = '';
14
+ let reasoning = '';
15
+ let lastDraft = '';
16
+ let lastPush = 0;
17
+ let busy = false;
18
+ let closed = false;
19
+ let inFlight = Promise.resolve();
20
+ const timer = setInterval(() => {
21
+ if (closed || busy)
22
+ return;
23
+ const t = o.preview({ text, reasoning: reasoning || null }).slice(0, o.maxLength);
24
+ if (!t)
25
+ return;
26
+ if (t === lastDraft && Date.now() - lastPush < o.keepAliveMs)
27
+ return;
28
+ busy = true;
29
+ inFlight = swallow(() => o.ops.draft(t), undefined, 'draft')
30
+ .then(() => {
31
+ lastDraft = t;
32
+ lastPush = Date.now();
33
+ })
34
+ .finally(() => {
35
+ busy = false;
36
+ });
37
+ }, o.throttleMs);
38
+ if (typeof timer.unref === 'function')
39
+ timer.unref();
40
+ function flushSegment() {
41
+ const body = text.slice(0, o.maxLength).trim();
42
+ text = '';
43
+ lastDraft = '';
44
+ lastPush = 0;
45
+ if (!body)
46
+ return;
47
+ inFlight = inFlight.then(() => swallow(() => o.ops.send(body), null, 'send(segment)'));
48
+ }
49
+ async function deliver(r) {
50
+ closed = true;
51
+ clearInterval(timer);
52
+ await inFlight;
53
+ for (const part of o.render(r)) {
54
+ await swallow(() => o.ops.send(part), null, 'send(final)');
55
+ }
56
+ }
57
+ let finished = Promise.resolve();
58
+ let terminalDelivered = false;
59
+ function deliverTerminal(kind, r) {
60
+ if (terminalDelivered) {
61
+ log.warn(`draft-message: a second terminal event (${kind}) after the turn already finished — dropped`);
62
+ return;
63
+ }
64
+ terminalDelivered = true;
65
+ finished = deliver(r);
66
+ }
67
+ return {
68
+ emit(e) {
69
+ if (e.kind === 'delta')
70
+ text = e.text;
71
+ else if (e.kind === 'reasoning')
72
+ reasoning = e.text;
73
+ else if (e.kind === 'segment')
74
+ flushSegment();
75
+ else if (e.kind === 'answer')
76
+ deliverTerminal('answer', { text: e.text, reasoning: e.reasoning });
77
+ else if (e.kind === 'error')
78
+ deliverTerminal('error', { text: null, reasoning: null });
79
+ },
80
+ done: () => {
81
+ clearInterval(timer);
82
+ return finished;
83
+ },
84
+ };
85
+ }
@@ -4,7 +4,7 @@ import { listDiagnostics } from './diagnostics.ts';
4
4
  import { type PipelineOptions } from './pipeline.ts';
5
5
  import type { Connector, TurnRequest } from './types.ts';
6
6
  export declare const handleIncoming: (connector: Connector, turn: TurnRequest, options?: PipelineOptions) => Promise<void>;
7
- export { registerAgent, resolveAgent, registerConnector, isConnectorRegistered, clearRuntimeRegistries, listRegisteredAgents, listRegisteredConnectors, listLinkableConnectors, connectorLinkUrl, listAgentCatalog, getDefaultAgentId, liveAgentId, } from './registry.ts';
7
+ export { registerAgent, resolveAgent, registerConnector, isConnectorRegistered, clearRuntimeRegistries, listRegisteredAgents, listRegisteredConnectors, listAgentCatalog, getDefaultAgentId, liveAgentId, } from './registry.ts';
8
8
  export { attachmentMaterializer } from './attachments.ts';
9
9
  export { makeAttachmentCapabilities } from './agent-capabilities.ts';
10
10
  export { makeSystemCapabilities } from './system-capabilities.ts';
@@ -15,19 +15,18 @@ export { inspectUpload, isAgentToolContentResult } from './file-inspection.ts';
15
15
  export type { PipelineOptions, RunAgentFn } from './pipeline.ts';
16
16
  export { buildPolicy, loadGatePolicy, loadAgentId } from './config.ts';
17
17
  export { getConversationStarters, refreshSystemStarters } from './starters.ts';
18
- export { resolveSpeaker } from './pipeline.ts';
19
18
  export { mayRead, mayWrite, mayBePrivate } from './conversation-access.ts';
20
19
  export type { ConversationAccess } from './conversation-access.ts';
21
20
  export { makeLiveSink, plainRender } from './live-message.ts';
22
21
  export type { LiveChannelOps, LiveSinkOpts, RenderFn } from './live-message.ts';
23
- export { chunk } from './format.ts';
22
+ export { makeDraftSink } from './draft-message.ts';
23
+ export type { DraftChannelOps, DraftSinkOpts } from './draft-message.ts';
24
24
  export { DEFAULT_TASK_TIMEOUT_MS } from '../background-scheduler.ts';
25
- export type { Connector, TurnEnvelope, ConnectorCapabilities, TurnBody, TurnRequest, SenderIdKind, GatePolicy, TurnEvent, TurnSink, AttachmentRef, AttachmentMaterializer, AgentToolDefinition, AgentToolProvider, ConvMessage, AgentTurn, AgentRuntime, AgentCapabilities, AgentMediaKind, AgentMediaLimits, AgentPreset, AgentDescriptor, AgentCatalogEntry, ConnectorRegistration, AgentToolContentBlock, AgentToolContentResult, } from './types.ts';
25
+ export type { Connector, TurnEnvelope, ConnectorCapabilities, TurnBody, TurnRequest, GatePolicy, TurnEvent, TurnSink, AttachmentRef, AttachmentMaterializer, AgentToolDefinition, AgentToolProvider, ConvMessage, AgentTurn, AgentRuntime, AgentCapabilities, AgentMediaKind, AgentMediaLimits, AgentPreset, AgentDescriptor, AgentCatalogEntry, ConnectorRegistration, AgentToolContentBlock, AgentToolContentResult, } from './types.ts';
26
26
  export type { AuthRole } from '../plugin-hooks.ts';
27
27
  export declare function startOrchestrator(): void;
28
28
  export declare function stopOrchestrator(): void;
29
29
  export declare const orchestratorStartersTask: BackgroundTask;
30
- export declare const linkCodePruneTask: BackgroundTask;
31
30
  export declare function orchestratorDiagnostics(): Promise<{
32
31
  generatedAt: string;
33
32
  agents: string[];
@@ -6,10 +6,9 @@ import { setLogDb } from "./pipeline.js";
6
6
  import { refreshSystemStarters } from "./starters.js";
7
7
  import { getLogger } from '@coffer-org/sdk/logger';
8
8
  const log = getLogger('orchestrator');
9
- import { pruneLinkCodes } from "../identity-link.js";
10
9
  import { handleIncoming as handleIncomingImpl } from "./pipeline.js";
11
10
  export const handleIncoming = handleIncomingImpl;
12
- export { registerAgent, resolveAgent, registerConnector, isConnectorRegistered, clearRuntimeRegistries, listRegisteredAgents, listRegisteredConnectors, listLinkableConnectors, connectorLinkUrl, listAgentCatalog, getDefaultAgentId, liveAgentId, } from "./registry.js";
11
+ export { registerAgent, resolveAgent, registerConnector, isConnectorRegistered, clearRuntimeRegistries, listRegisteredAgents, listRegisteredConnectors, listAgentCatalog, getDefaultAgentId, liveAgentId, } from "./registry.js";
13
12
  export { attachmentMaterializer } from "./attachments.js";
14
13
  export { makeAttachmentCapabilities } from "./agent-capabilities.js";
15
14
  export { makeSystemCapabilities } from "./system-capabilities.js";
@@ -18,10 +17,9 @@ export { CONNECTOR_FACT_NAME } from "./context-facts.js";
18
17
  export { inspectUpload, isAgentToolContentResult } from "./file-inspection.js";
19
18
  export { buildPolicy, loadGatePolicy, loadAgentId } from "./config.js";
20
19
  export { getConversationStarters, refreshSystemStarters } from "./starters.js";
21
- export { resolveSpeaker } from "./pipeline.js";
22
20
  export { mayRead, mayWrite, mayBePrivate } from "./conversation-access.js";
23
21
  export { makeLiveSink, plainRender } from "./live-message.js";
24
- export { chunk } from "./format.js";
22
+ export { makeDraftSink } from "./draft-message.js";
25
23
  export { DEFAULT_TASK_TIMEOUT_MS } from "../background-scheduler.js";
26
24
  let db;
27
25
  const STARTERS_CHECK_INTERVAL_MS = 10 * 60_000;
@@ -47,11 +45,6 @@ export const orchestratorStartersTask = {
47
45
  intervalMs: STARTERS_CHECK_INTERVAL_MS,
48
46
  run: runStartersRefresh,
49
47
  };
50
- export const linkCodePruneTask = {
51
- name: 'core:link-code-prune',
52
- intervalMs: 86_400_000,
53
- run: pruneLinkCodes,
54
- };
55
48
  export async function orchestratorDiagnostics() {
56
49
  return {
57
50
  generatedAt: new Date().toISOString(),
@@ -1,6 +1,11 @@
1
- import { chunk } from "./format.js";
2
1
  import { getLogger } from '@coffer-org/sdk/logger';
3
2
  const log = getLogger('orchestrator');
3
+ function chunk(text, max) {
4
+ const out = [];
5
+ for (let i = 0; i < text.length; i += max)
6
+ out.push(text.slice(i, i + max));
7
+ return out.length ? out : [''];
8
+ }
4
9
  export function plainRender(max) {
5
10
  return (r) => chunk((r.text ?? '').trim() || '⚠️ the agent returned no response', max);
6
11
  }
@@ -104,8 +109,6 @@ export function makeLiveSink(o) {
104
109
  }
105
110
  else if (e.kind === 'answer')
106
111
  deliverTerminal('answer', { text: e.text, reasoning: e.reasoning });
107
- else if (e.kind === 'notice')
108
- deliverTerminal('notice', { text: e.text, reasoning: null });
109
112
  else if (e.kind === 'error')
110
113
  deliverTerminal('error', { text: null, reasoning: null });
111
114
  },
@@ -1,18 +1,16 @@
1
- import type { AgentMediaLimits, Connector, TurnRequest, GatePolicy, AgentRuntime } from './types.ts';
1
+ import type { AgentMediaLimits, Connector, ConvMessage, TurnRequest, GatePolicy, AgentRuntime } from './types.ts';
2
2
  import type { LogDb } from './db.ts';
3
3
  import { type AreaWorld, type SystemAreas } from './system-areas.ts';
4
4
  import type { AuthUser } from '../auth-store.ts';
5
+ import { type ContextFact } from './context-facts.ts';
6
+ import { type StoredMsg } from '../conversation-store.ts';
5
7
  export declare function setLogDb(db: LogDb | undefined): void;
6
- export declare const NOTICE_THROTTLE: {
7
- readonly max: 5;
8
- readonly windowMs: 60000;
9
- };
10
8
  export type RunAgentFn = AgentRuntime['run'];
11
9
  export declare function liveWorld(): Promise<AreaWorld>;
10
+ export declare function foldContextFacts(messages: readonly (StoredMsg | ConvMessage)[]): ContextFact[];
11
+ export declare function lastContextAt(messages: readonly (StoredMsg | ConvMessage)[]): Date | null;
12
+ export declare function toConvMessages(stored: readonly StoredMsg[], facts: ContextFact[], userMsgId: string, queryText: string, contextSource?: readonly StoredMsg[]): ConvMessage[];
12
13
  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
14
  export interface PipelineOptions {
17
15
  policy?: GatePolicy;
18
16
  }
@@ -22,8 +20,7 @@ export interface PipelineDeps extends PipelineOptions {
22
20
  logDb?: LogDb | null;
23
21
  agentBase?: () => Promise<string>;
24
22
  media?: AgentMediaLimits;
25
- resolveUser?: (id: string) => Promise<AuthUser | null>;
26
- resolveLinkedUser?: (connectorId: string, externalId: string) => Promise<AuthUser | null>;
23
+ lookupUser?: (id: number) => Promise<AuthUser | null>;
27
24
  now?: () => Date;
28
25
  timeZone?: string;
29
26
  world?: AreaWorld;
@@ -1,4 +1,3 @@
1
- import { makeThrottle } from "./allow.js";
2
1
  import { loadAgentId, loadGatePolicy } from "./config.js";
3
2
  import { assembleSystem, mergeAreas } from "./system-areas.js";
4
3
  import { attachmentMaterializer } from "./attachments.js";
@@ -7,7 +6,6 @@ import { makeSystemCapabilities } from "./system-capabilities.js";
7
6
  import { resolveAgent } from "./registry.js";
8
7
  import { recordDiagnostic } from "./diagnostics.js";
9
8
  import { getLogger } from '@coffer-org/sdk/logger';
10
- import { findLinkedUser } from "../identity-link.js";
11
9
  import { buildContextFacts } from "./context-facts.js";
12
10
  import { systemTimeZone } from "./environment.js";
13
11
  import { discoverPlugins } from "../plugin-discovery.js";
@@ -16,21 +14,17 @@ import { LocalClient } from "../mcp-local.js";
16
14
  import { loadComposedLocales } from "../locale-registry.js";
17
15
  import { buildDomainAreas } from "../mcp-tools.js";
18
16
  import { getActiveRegistry } from "../registry-context.js";
17
+ import { DEFAULT_MAX_DEPTH, countUserMessages, newMessageId, putMessage, readConversation, readAll, } from "../conversation-store.js";
19
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
+ };
20
24
  let logDb;
21
25
  export function setLogDb(db) {
22
26
  logDb = db;
23
27
  }
24
- export const NOTICE_THROTTLE = { max: 5, windowMs: 60_000 };
25
- const throttleByConnector = new Map();
26
- function passThrottle(connectorId) {
27
- let t = throttleByConnector.get(connectorId);
28
- if (!t) {
29
- t = makeThrottle(NOTICE_THROTTLE.max, NOTICE_THROTTLE.windowMs);
30
- throttleByConnector.set(connectorId, t);
31
- }
32
- return t;
33
- }
34
28
  let cachedBase;
35
29
  let cachedBaseAgentId;
36
30
  function cachedAgentBase() {
@@ -71,40 +65,90 @@ function cachedDomainAreas() {
71
65
  cachedDomain = buildDomainAreas();
72
66
  return cachedDomain;
73
67
  }
74
- function foldContextFacts(messages) {
68
+ export function foldContextFacts(messages) {
75
69
  const acc = new Map();
76
70
  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);
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
+ }
82
91
  }
83
92
  }
84
93
  return [...acc.values()];
85
94
  }
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);
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;
99
113
  }
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);
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;
104
147
  }
105
- function openSink(connector, envelope) {
148
+ const storeLookup = async (id) => (await import("../auth-store.js")).findUserById(id);
149
+ function openSink(connector, envelope, msgId) {
106
150
  try {
107
- return connector.open(envelope);
151
+ return connector.open(envelope, msgId);
108
152
  }
109
153
  catch (err) {
110
154
  const message = err instanceof Error ? err.message : String(err);
@@ -129,82 +173,78 @@ export async function handleIncoming(connector, turn, deps) {
129
173
  const selectedAgentId = turn.agentId ?? policy.agentId ?? (deps?.runAgent ? undefined : await loadAgentId());
130
174
  const runtime = deps?.runAgent ? undefined : resolveAgent(selectedAgentId);
131
175
  const agent = deps?.runAgent ?? runtime.run.bind(runtime);
132
- const agentMedia = () => deps?.media ?? runtime.media;
176
+ const agentMedia = () => deps?.media ?? runtime?.media ?? DEFAULT_MEDIA;
133
177
  const db = deps && 'logDb' in deps ? deps.logDb : logDb;
134
178
  const agentBase = deps?.agentBase ?? (() => (selectedAgentId ? runtime.systemBase() : cachedAgentBase()));
135
- const { connectorId, chatId } = turn.envelope;
179
+ const { connectorId } = turn.envelope;
180
+ const conversationId = turn.envelope.conversationId;
136
181
  const { sender } = turn;
137
- const messages = turn.body.messages;
138
- const last = messages[messages.length - 1];
139
- if (!last || last.role !== 'user')
140
- return;
141
- const incomingMsgId = last.msgId;
142
- if (policy.triggerPrefix && !last.content.toLowerCase().startsWith(policy.triggerPrefix.toLowerCase()))
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()))
143
204
  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)');
151
- }
152
- }
205
+ const speakerRow = await (deps?.lookupUser ?? storeLookup)(sender.userId);
206
+ if (!speakerRow || speakerRow.disabled)
153
207
  return;
154
- }
155
208
  if (turn.signal?.aborted)
156
209
  return;
157
- const queryText = policy.triggerPrefix ? last.content.slice(policy.triggerPrefix.length).trim() : last.content.trim();
158
- if (!queryText) {
159
- if (turn.prepareAttachments && !policy.triggerPrefix) {
160
- await safeCall(() => turn.prepareAttachments(attachmentMaterializer), messages, 'prepareAttachments');
161
- }
210
+ const queryText = policy.triggerPrefix ? text.slice(policy.triggerPrefix.length).trim() : text.trim();
211
+ if (!queryText)
162
212
  return;
163
- }
164
- const preparedMessages = turn.prepareAttachments
165
- ? await safeCall(() => turn.prepareAttachments(attachmentMaterializer), messages, 'prepareAttachments')
166
- : messages;
167
213
  const speaker = {
168
- id: sender.id,
169
- name: speakerRow.displayName ?? sender.displayName ?? sender.id,
214
+ id: String(sender.userId),
215
+ name: speakerRow.displayName ?? sender.displayName ?? String(sender.userId),
170
216
  role: speakerRow.role,
171
217
  };
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();
218
+ const stored = await readConversation(conversationId, { limit: DEFAULT_MAX_DEPTH });
219
+ const all = await readAll(conversationId);
175
220
  const facts = buildContextFacts({
176
221
  connectorFacts: turn.turnContext ?? [],
177
222
  speaker,
178
223
  now,
179
224
  timeZone: deps?.timeZone ?? systemTimeZone(),
180
- previous: stated.length ? foldContextFacts(preparedMessages) : null,
181
- previousAt: lastStated ? new Date(lastStated.ts * 1000) : null,
225
+ previous: foldContextFacts(all),
226
+ previousAt: lastContextAt(all),
182
227
  });
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;
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
+ });
236
+ }
237
+ const agentMessages = toConvMessages(stored, facts, userMsgId, queryText, all);
238
+ const authored = turn.systemPrompt ?? {};
197
239
  const domain = await (deps?.domainAreas ?? cachedDomainAreas)();
198
240
  const areas = mergeAreas({ root: [await agentBase()] }, domain, authored);
199
241
  const system = assembleSystem(areas, deps?.world ?? (await liveWorld()));
200
- if (turn.signal?.aborted) {
201
- await persistContext();
242
+ if (turn.signal?.aborted)
202
243
  return;
203
- }
204
244
  db?.logTurn({
205
245
  connector: connectorId,
206
- chatId,
207
- userId: sender.id,
246
+ chatId: conversationId,
247
+ userId: String(sender.userId),
208
248
  role: 'user',
209
249
  text: queryText,
210
250
  tokensIn: null,
@@ -214,11 +254,13 @@ export async function handleIncoming(connector, turn, deps) {
214
254
  presetId: turn.presetId ?? null,
215
255
  });
216
256
  const startedAt = Date.now();
217
- const sink = openSink(connector, turn.envelope);
257
+ const botMsgId = newMessageId();
258
+ const sink = openSink(connector, turn.envelope, botMsgId);
218
259
  if (!sink)
219
260
  return;
220
261
  let usage;
221
262
  let answerText = null;
263
+ let reasoningText = null;
222
264
  let runFailed = false;
223
265
  let agentTurn;
224
266
  try {
@@ -226,9 +268,10 @@ export async function handleIncoming(connector, turn, deps) {
226
268
  const toolProvider = {
227
269
  tools: [...attachmentTools.tools, ...makeSystemCapabilities().tools],
228
270
  };
271
+ const userMessageCount = await countUserMessages(conversationId);
229
272
  agentTurn = {
230
273
  envelope: turn.envelope,
231
- body: { system, messages: agentMessages, userTurns: turn.body.userTurns },
274
+ body: { system, messages: agentMessages, userMessageCount },
232
275
  toolProvider,
233
276
  ...(turn.presetId ? { presetId: turn.presetId } : {}),
234
277
  senderRole: speaker.role,
@@ -239,8 +282,14 @@ export async function handleIncoming(connector, turn, deps) {
239
282
  if (event.kind === 'usage') {
240
283
  usage = { tokensIn: event.tokensIn, tokensOut: event.tokensOut, presetId: event.presetId };
241
284
  }
285
+ else if (event.kind === 'reasoning') {
286
+ reasoningText = event.text;
287
+ }
242
288
  else if (event.kind === 'answer') {
243
289
  answerText = event.text;
290
+ if (event.reasoning !== null) {
291
+ reasoningText = event.reasoning;
292
+ }
244
293
  }
245
294
  sink.emit(event);
246
295
  }
@@ -262,8 +311,31 @@ export async function handleIncoming(connector, turn, deps) {
262
311
  }
263
312
  }
264
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;
265
337
  if (!runFailed && answerText && agentTurn) {
266
- const events = turn.body.capabilities.events;
338
+ const events = capabilities.events;
267
339
  const want = { suggestions: events.includes('suggestions'), title: events.includes('title') };
268
340
  const afterwordFn = deps?.afterword ?? runtime?.afterword;
269
341
  if ((want.suggestions || want.title) && afterwordFn) {
@@ -276,6 +348,7 @@ export async function handleIncoming(connector, turn, deps) {
276
348
  recordDiagnostic('error', 'agent.afterword', message);
277
349
  }
278
350
  if (extra?.suggestions?.length) {
351
+ extraSuggestions = extra.suggestions;
279
352
  try {
280
353
  sink.emit({ kind: 'suggestions', items: extra.suggestions });
281
354
  }
@@ -301,11 +374,20 @@ export async function handleIncoming(connector, turn, deps) {
301
374
  }
302
375
  }
303
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
+ });
385
+ }
304
386
  if (!runFailed && usage && answerText)
305
387
  db?.logTurn({
306
388
  connector: connectorId,
307
- chatId,
308
- userId: sender.id,
389
+ chatId: conversationId,
390
+ userId: String(sender.userId),
309
391
  role: 'assistant',
310
392
  text: answerText,
311
393
  tokensIn: usage.tokensIn,
@@ -314,5 +396,4 @@ export async function handleIncoming(connector, turn, deps) {
314
396
  agentId: selectedAgentId ?? null,
315
397
  presetId: usage.presetId,
316
398
  });
317
- await persistContext();
318
399
  }
@@ -8,8 +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 listLinkableConnectors(): string[];
12
- export declare function connectorLinkUrl(connectorId: string, code: string): string | undefined;
13
11
  export declare function listAgentCatalog(): Promise<AgentCatalogEntry[]>;
14
12
  export declare function getDefaultAgentId(): string | undefined;
15
13
  export declare function clearRuntimeRegistries(): void;
@@ -48,15 +48,6 @@ 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
- }
60
51
  export async function listAgentCatalog() {
61
52
  const ids = [...agents.keys()].sort();
62
53
  const out = [];