@coffer-org/server 6.0.0 → 7.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.
package/dist/index.js CHANGED
@@ -22,6 +22,7 @@ import { registerPluginUserApi, registerPluginAdminApi } from "./plugin-user-api
22
22
  import { registerAuthApi, resolveRequestUser, requireAdmin, PUBLIC_API_PATHS } from "./auth-api.js";
23
23
  import { registerMcpHttp } from "./mcp-http.js";
24
24
  import { registerOAuthApi } from "./oauth-api.js";
25
+ import { registerInstanceApi } from "./instance-api.js";
25
26
  import { baseUrl } from "./public-url.js";
26
27
  import { discoverPluginAssets, discoverRuntime } from "./plugin-discovery.js";
27
28
  import { checkLatestVersion, resolveUpdateTarget, resolveAllUpdateTargets, resolveBaseTargets, resolveRuntimeTarget, runNpmInstall, } from "./plugin-updates.js";
@@ -146,6 +147,7 @@ app.get('/health', async (_req, reply) => {
146
147
  return reply.code(503).send({ status: 'db_unavailable' });
147
148
  }
148
149
  });
150
+ registerInstanceApi(app);
149
151
  let librariesPayload = null;
150
152
  app.get('/api/libraries', async (req, reply) => {
151
153
  if (!librariesPayload) {
@@ -0,0 +1,5 @@
1
+ import type { FastifyInstance } from 'fastify';
2
+ export interface InstanceFacts {
3
+ origin: string;
4
+ }
5
+ export declare function registerInstanceApi(app: FastifyInstance): void;
@@ -0,0 +1,4 @@
1
+ import { baseUrl } from "./public-url.js";
2
+ export function registerInstanceApi(app) {
3
+ app.get('/api/instance', async (req) => ({ origin: await baseUrl(req) }));
4
+ }
@@ -13,7 +13,7 @@ export interface McpToolDef {
13
13
  inputSchema: z.ZodRawShape;
14
14
  scope: 'crud' | 'plugin' | 'rag' | 'settings' | 'upload';
15
15
  role: AuthRole;
16
- handler: (args: Record<string, unknown>) => Promise<ToolResult>;
16
+ handler: (args: Record<string, unknown>, signal?: AbortSignal) => Promise<ToolResult>;
17
17
  }
18
18
  export interface RagDeps {
19
19
  embeddingApiKey: string;
@@ -73,6 +73,12 @@ export declare function collectLibraryPurposes(reg?: {
73
73
  }, locales?: LocaleResolver): LibraryPurpose[];
74
74
  export declare function siteSection(siteUrl: string): Promise<string | null>;
75
75
  export declare function buildDomainSections(locales?: LocaleResolver): Promise<string[]>;
76
+ export interface StarterHint {
77
+ id: string;
78
+ text: string;
79
+ hash: string;
80
+ }
81
+ export declare const CORE_STARTER_HINT: string;
76
82
  export declare function collectStarterHints(hooks?: Record<string, PluginHooks>, emFactory?: () => EntityManager, reg?: {
77
83
  libraries: {
78
84
  meta: {
@@ -81,9 +87,6 @@ export declare function collectStarterHints(hooks?: Record<string, PluginHooks>,
81
87
  starterHint?: string;
82
88
  };
83
89
  }[];
84
- }, locales?: LocaleResolver): Promise<{
85
- combined: string;
86
- hash: string;
87
- }>;
90
+ }, locales?: LocaleResolver): Promise<StarterHint[]>;
88
91
  export declare function buildMcpInstructions(sections: string[]): string;
89
92
  export {};
package/dist/mcp-tools.js CHANGED
@@ -351,16 +351,23 @@ export async function buildDomainSections(locales) {
351
351
  const rules = (await collectPluginInstructions()).map(({ id, instructions }) => `## ${id}\n${instructions}`);
352
352
  return [dataModel, ...(overview ? [overview] : []), ...(singleSection ? [singleSection] : []), ...rules];
353
353
  }
354
+ export const CORE_STARTER_HINT = 'what this Coffer instance itself holds — which libraries are present and what kinds of ' +
355
+ 'questions the stored data can answer';
356
+ function hintHash(text) {
357
+ return createHash('sha1').update(text).digest('hex');
358
+ }
354
359
  export async function collectStarterHints(hooks = pluginHooks, emFactory = () => getEm().fork(), reg, locales) {
355
- const parts = [];
360
+ const out = [{ id: 'system', text: CORE_STARTER_HINT, hash: hintHash(CORE_STARTER_HINT) }];
356
361
  for (const [id, h] of Object.entries(hooks)) {
357
362
  const hint = h.agent?.starterHint;
358
363
  if (hint == null)
359
364
  continue;
360
365
  try {
361
- const text = typeof hint === 'function' ? await hint(pluginCtx(id, emFactory())) : hint;
362
- if (text)
363
- parts.push(`- ${id}: ${text}`);
366
+ const hintText = typeof hint === 'function' ? await hint(pluginCtx(id, emFactory())) : hint;
367
+ if (hintText) {
368
+ const text = `${id}: ${hintText}`;
369
+ out.push({ id: `plugin:${id}`, text, hash: hintHash(text) });
370
+ }
364
371
  }
365
372
  catch (e) {
366
373
  log.warn(`plugin ${id}: starterHint skipped — ${e.message}`);
@@ -380,11 +387,10 @@ export async function collectStarterHints(hooks = pluginHooks, emFactory = () =>
380
387
  if (!v.meta.starterHint)
381
388
  continue;
382
389
  const name = v.meta.label ? i18n.resolve(v.meta.label) : undefined;
383
- parts.push(`- ${v.meta.id}${name ? ` ("${name}")` : ''}: ${v.meta.starterHint}`);
390
+ const text = `${v.meta.id}${name ? ` ("${name}")` : ''}: ${v.meta.starterHint}`;
391
+ out.push({ id: `library:${v.meta.id}`, text, hash: hintHash(text) });
384
392
  }
385
- const combined = parts.join('\n');
386
- const hash = createHash('sha1').update(combined).digest('hex');
387
- return { combined, hash };
393
+ return out;
388
394
  }
389
395
  export function buildMcpInstructions(sections) {
390
396
  const base = [
@@ -12,8 +12,8 @@ export interface StartersDeps {
12
12
  loadAgentId: typeof loadAgentId;
13
13
  resolveAgent: (id?: string) => AgentRuntime;
14
14
  getDefaultAgentId: typeof getDefaultAgentId;
15
- today: () => string;
16
- now: () => string;
15
+ now: () => number;
16
+ shuffle: <T>(xs: T[]) => T[];
17
17
  }
18
- export declare function getConversationStarters(deps?: StartersDeps): Promise<string[]>;
19
18
  export declare function refreshSystemStarters(deps?: StartersDeps): Promise<void>;
19
+ export declare function getConversationStarters(deps?: StartersDeps): Promise<string[]>;
@@ -3,12 +3,24 @@ import { SYSTEM_SETTINGS_ID } from "../system-settings.js";
3
3
  import { getPluginState, setPluginState } from "../plugin-state.js";
4
4
  import { collectStarterHints } from "../mcp-tools.js";
5
5
  import { getLogger } from '@coffer-org/sdk/logger';
6
- import { todayDateString } from "./environment.js";
7
6
  import { loadAgentId } from "./config.js";
8
7
  import { resolveAgent, getDefaultAgentId } from "./registry.js";
9
8
  const log = getLogger('orchestrator');
10
9
  const PLUGIN = 'orchestrator';
11
10
  const STATE_KEY = 'system_starters';
11
+ const POOL_VERSION = 2;
12
+ const ENTRY_TTL_MS = 24 * 60 * 60_000;
13
+ const SAMPLE_MIN = 4;
14
+ const SAMPLE_MAX = 5;
15
+ const PER_SOURCE_SOFT_CAP = 2;
16
+ function shuffled(xs) {
17
+ const out = [...xs];
18
+ for (let i = out.length - 1; i > 0; i--) {
19
+ const j = Math.floor(Math.random() * (i + 1));
20
+ [out[i], out[j]] = [out[j], out[i]];
21
+ }
22
+ return out;
23
+ }
12
24
  const defaultDeps = {
13
25
  getPluginSettings,
14
26
  getPluginState,
@@ -17,65 +29,120 @@ const defaultDeps = {
17
29
  loadAgentId,
18
30
  resolveAgent,
19
31
  getDefaultAgentId,
20
- today: todayDateString,
21
- now: () => new Date().toISOString(),
32
+ now: () => Date.now(),
33
+ shuffle: shuffled,
22
34
  };
23
35
  async function currentLanguage(deps) {
24
36
  const system = await deps.getPluginSettings(SYSTEM_SETTINGS_ID);
25
37
  const v = system['language'];
26
38
  return typeof v === 'string' && v ? v : 'en';
27
39
  }
28
- async function readCache(deps) {
40
+ function isPool(v) {
41
+ if (typeof v !== 'object' || v === null)
42
+ return false;
43
+ const p = v;
44
+ return p.version === POOL_VERSION && typeof p.language === 'string' && typeof p.sources === 'object' && !!p.sources;
45
+ }
46
+ async function readPool(deps) {
29
47
  const raw = await deps.getPluginState(PLUGIN, STATE_KEY);
30
48
  if (!raw)
31
49
  return null;
32
50
  try {
33
- return JSON.parse(raw);
51
+ const parsed = JSON.parse(raw);
52
+ return isPool(parsed) ? parsed : null;
34
53
  }
35
54
  catch {
36
55
  return null;
37
56
  }
38
57
  }
39
- export async function getConversationStarters(deps = defaultDeps) {
40
- return (await readCache(deps))?.starters ?? [];
58
+ function fresh(entry, nowMs) {
59
+ return nowMs - Date.parse(entry.generatedAt) < ENTRY_TTL_MS;
60
+ }
61
+ function tryResolve(deps, agentId) {
62
+ try {
63
+ return deps.resolveAgent(agentId);
64
+ }
65
+ catch {
66
+ return undefined;
67
+ }
41
68
  }
42
69
  export async function refreshSystemStarters(deps = defaultDeps) {
43
- const date = deps.today();
44
70
  const [language, hints] = await Promise.all([currentLanguage(deps), deps.collectStarterHints()]);
45
71
  const agentId = (await deps.loadAgentId()) ?? deps.getDefaultAgentId();
46
72
  if (!agentId)
47
73
  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;
74
+ const stored = await readPool(deps);
75
+ const pool = stored && stored.language === language ? stored : { version: POOL_VERSION, language, sources: {} };
76
+ let dirty = pool !== stored;
77
+ const declared = new Set(hints.map((h) => h.id));
78
+ for (const id of Object.keys(pool.sources)) {
79
+ if (!declared.has(id)) {
80
+ delete pool.sources[id];
81
+ dirty = true;
82
+ }
55
83
  }
56
- let runtime;
57
- try {
58
- runtime = deps.resolveAgent(agentId);
84
+ const nowMs = deps.now();
85
+ const stale = hints.find((h) => {
86
+ const e = pool.sources[h.id];
87
+ if (!e)
88
+ return true;
89
+ if (e.hintHash !== h.hash || e.agentId !== agentId)
90
+ return true;
91
+ return !fresh(e, nowMs);
92
+ });
93
+ if (stale) {
94
+ const runtime = tryResolve(deps, agentId);
95
+ if (runtime?.starters) {
96
+ try {
97
+ const messages = await runtime.starters(stale.text);
98
+ pool.sources[stale.id] = {
99
+ messages,
100
+ generatedAt: new Date(nowMs).toISOString(),
101
+ hintHash: stale.hash,
102
+ agentId,
103
+ };
104
+ dirty = true;
105
+ }
106
+ catch (err) {
107
+ log.warn(`starters for ${stale.id} failed: ${err instanceof Error ? err.message : String(err)}`);
108
+ }
109
+ }
59
110
  }
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;
111
+ if (dirty)
112
+ await setPool(deps, pool);
113
+ }
114
+ async function setPool(deps, pool) {
115
+ await deps.setPluginState(PLUGIN, STATE_KEY, JSON.stringify(pool));
116
+ }
117
+ export async function getConversationStarters(deps = defaultDeps) {
118
+ const [pool, language] = await Promise.all([readPool(deps), currentLanguage(deps)]);
119
+ if (!pool)
120
+ return [];
121
+ if (pool.language !== language)
122
+ return [];
123
+ const nowMs = deps.now();
124
+ const seen = new Set();
125
+ const primary = [];
126
+ const spare = [];
127
+ for (const entry of deps.shuffle(Object.values(pool.sources))) {
128
+ if (!fresh(entry, nowMs))
129
+ continue;
130
+ let taken = 0;
131
+ for (const message of deps.shuffle(entry.messages)) {
132
+ if (seen.has(message))
133
+ continue;
134
+ seen.add(message);
135
+ if (taken < PER_SOURCE_SOFT_CAP) {
136
+ primary.push(message);
137
+ taken++;
138
+ }
139
+ else {
140
+ spare.push(message);
141
+ }
142
+ }
72
143
  }
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
- }));
144
+ const out = deps.shuffle(primary).slice(0, SAMPLE_MAX);
145
+ if (out.length < SAMPLE_MIN)
146
+ out.push(...deps.shuffle(spare).slice(0, SAMPLE_MAX - out.length));
147
+ return out;
81
148
  }
@@ -8,7 +8,7 @@ export interface AgentToolDefinition {
8
8
  name: string;
9
9
  description: string;
10
10
  inputSchema: Record<string, unknown>;
11
- handler: (args: Record<string, unknown>) => Promise<unknown>;
11
+ handler: (args: Record<string, unknown>, signal?: AbortSignal) => Promise<unknown>;
12
12
  forcedAfterAnswer?: boolean;
13
13
  }
14
14
  export interface AgentToolProvider {
@@ -83,7 +83,7 @@ export interface AgentRuntime {
83
83
  run(request: AgentRequest): Promise<AgentResult>;
84
84
  systemBase(): Promise<string>;
85
85
  describe(): Promise<AgentDescriptor>;
86
- starters?(): Promise<string[]>;
86
+ starters?(hint: string): Promise<string[]>;
87
87
  }
88
88
  export interface ConnectorRegistration {
89
89
  id: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@coffer-org/server",
3
- "version": "6.0.0",
3
+ "version": "7.0.0",
4
4
  "type": "module",
5
5
  "engines": {
6
6
  "node": ">=24"
@@ -36,7 +36,7 @@
36
36
  "postpack": "node ../../scripts/swap-exports.mjs src"
37
37
  },
38
38
  "dependencies": {
39
- "@coffer-org/sdk": "^6.0.0",
39
+ "@coffer-org/sdk": "^7.0.0",
40
40
  "@extractus/oembed-extractor": "^4.1.0",
41
41
  "@fastify/cors": "^11.2.0",
42
42
  "@fastify/multipart": "^10.0.0",