@coffer-org/server 7.0.0 → 7.2.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/background-scheduler.d.ts +1 -0
- package/dist/background-scheduler.js +2 -1
- package/dist/embed-openai.d.ts +2 -2
- package/dist/embed-openai.js +4 -3
- package/dist/embeddings.d.ts +1 -0
- package/dist/embeddings.js +8 -0
- package/dist/mcp-contract/schema.d.ts +34 -1
- package/dist/mcp-contract/schema.js +32 -9
- package/dist/mcp-contract/tools.js +3 -1
- package/dist/mcp-tools.d.ts +3 -2
- package/dist/mcp-tools.js +15 -11
- package/dist/orchestrator/index.d.ts +1 -0
- package/dist/orchestrator/index.js +1 -0
- package/dist/orchestrator/starter-sources.d.ts +2 -0
- package/dist/orchestrator/starter-sources.js +22 -0
- package/dist/orchestrator/starters.d.ts +4 -0
- package/dist/orchestrator/starters.js +68 -5
- package/dist/orchestrator/types.d.ts +5 -0
- package/dist/plugin-hooks.d.ts +2 -1
- package/dist/plugin-hooks.js +2 -2
- package/package.json +2 -2
|
@@ -17,6 +17,7 @@ export interface BackgroundScheduler {
|
|
|
17
17
|
start(): void;
|
|
18
18
|
stop(): void;
|
|
19
19
|
}
|
|
20
|
+
export declare const DEFAULT_TASK_TIMEOUT_MS = 600000;
|
|
20
21
|
export declare function makeScheduler(opts?: SchedulerOpts): BackgroundScheduler;
|
|
21
22
|
export declare function startScheduler(tasks: BackgroundTask[], opts?: SchedulerOpts): void;
|
|
22
23
|
export declare function stopScheduler(): void;
|
|
@@ -6,10 +6,11 @@ const defaultSetTimer = (cb, ms) => {
|
|
|
6
6
|
t.unref();
|
|
7
7
|
return t;
|
|
8
8
|
};
|
|
9
|
+
export const DEFAULT_TASK_TIMEOUT_MS = 600_000;
|
|
9
10
|
export function makeScheduler(opts = {}) {
|
|
10
11
|
const startupDelayMs = opts.startupDelayMs ?? (Number(process.env['BG_STARTUP_DELAY_MS']) || 600_000);
|
|
11
12
|
const gapMs = opts.gapMs ?? (Number(process.env['BG_GAP_MS']) || 30_000);
|
|
12
|
-
const defaultTimeoutMs = opts.taskTimeoutMs ?? (Number(process.env['BG_TASK_TIMEOUT_MS']) ||
|
|
13
|
+
const defaultTimeoutMs = opts.taskTimeoutMs ?? (Number(process.env['BG_TASK_TIMEOUT_MS']) || DEFAULT_TASK_TIMEOUT_MS);
|
|
13
14
|
const setTimer = opts.setTimer ?? defaultSetTimer;
|
|
14
15
|
const clearTimer = opts.clearTimer ?? ((h) => clearTimeout(h));
|
|
15
16
|
const tasks = [];
|
package/dist/embed-openai.d.ts
CHANGED
|
@@ -14,11 +14,11 @@ export declare class OpenAIEmbeddingError extends Error {
|
|
|
14
14
|
});
|
|
15
15
|
}
|
|
16
16
|
export declare function embeddingFailureMessage(error: unknown): string;
|
|
17
|
-
export declare function embedBatch(texts: string[], apiKey: string, fetchImpl?: typeof fetch): Promise<{
|
|
17
|
+
export declare function embedBatch(texts: string[], apiKey: string, fetchImpl?: typeof fetch, signal?: AbortSignal): Promise<{
|
|
18
18
|
vectors: number[][];
|
|
19
19
|
tokens: number;
|
|
20
20
|
}>;
|
|
21
|
-
export declare function embedOne(text: string, apiKey: string, fetchImpl?: typeof fetch): Promise<{
|
|
21
|
+
export declare function embedOne(text: string, apiKey: string, fetchImpl?: typeof fetch, signal?: AbortSignal): Promise<{
|
|
22
22
|
vector: number[];
|
|
23
23
|
tokens: number;
|
|
24
24
|
}>;
|
package/dist/embed-openai.js
CHANGED
|
@@ -54,7 +54,7 @@ export function embeddingFailureMessage(error) {
|
|
|
54
54
|
return `OpenAI embeddings are unavailable${error.status == null ? '' : ` (HTTP ${error.status})`}. RAG will retry; records are still saved.`;
|
|
55
55
|
}
|
|
56
56
|
}
|
|
57
|
-
export async function embedBatch(texts, apiKey, fetchImpl = fetch) {
|
|
57
|
+
export async function embedBatch(texts, apiKey, fetchImpl = fetch, signal) {
|
|
58
58
|
if (texts.length === 0)
|
|
59
59
|
return { vectors: [], tokens: 0 };
|
|
60
60
|
if (!apiKey)
|
|
@@ -65,6 +65,7 @@ export async function embedBatch(texts, apiKey, fetchImpl = fetch) {
|
|
|
65
65
|
method: 'POST',
|
|
66
66
|
headers: { 'content-type': 'application/json', authorization: `Bearer ${apiKey}` },
|
|
67
67
|
body: JSON.stringify({ model: EMBED_MODEL, input: texts, dimensions: EMBED_DIM }),
|
|
68
|
+
...(signal ? { signal } : {}),
|
|
68
69
|
});
|
|
69
70
|
}
|
|
70
71
|
catch (cause) {
|
|
@@ -79,8 +80,8 @@ export async function embedBatch(texts, apiKey, fetchImpl = fetch) {
|
|
|
79
80
|
const vectors = [...json.data].sort((a, b) => a.index - b.index).map((d) => d.embedding);
|
|
80
81
|
return { vectors, tokens: json.usage?.total_tokens ?? 0 };
|
|
81
82
|
}
|
|
82
|
-
export async function embedOne(text, apiKey, fetchImpl = fetch) {
|
|
83
|
-
const { vectors, tokens } = await embedBatch([text], apiKey, fetchImpl);
|
|
83
|
+
export async function embedOne(text, apiKey, fetchImpl = fetch, signal) {
|
|
84
|
+
const { vectors, tokens } = await embedBatch([text], apiKey, fetchImpl, signal);
|
|
84
85
|
const vector = vectors[0];
|
|
85
86
|
if (!vector)
|
|
86
87
|
throw new Error('embeddings: empty response');
|
package/dist/embeddings.d.ts
CHANGED
|
@@ -39,3 +39,4 @@ export declare function searchEmbeddings(queryVec: number[], k: number, opts?: {
|
|
|
39
39
|
shelfKeys?: Set<string>;
|
|
40
40
|
}): Promise<EmbeddingHit[]>;
|
|
41
41
|
export declare function listEventsSince(lastId: number, limit: number): Promise<EventRow[]>;
|
|
42
|
+
export declare function latestEventId(): Promise<number>;
|
package/dist/embeddings.js
CHANGED
|
@@ -116,3 +116,11 @@ export async function listEventsSince(lastId, limit) {
|
|
|
116
116
|
}));
|
|
117
117
|
return rows;
|
|
118
118
|
}
|
|
119
|
+
export async function latestEventId() {
|
|
120
|
+
const em = getEm().fork();
|
|
121
|
+
const rows = (await em.find('_Event', {}, {
|
|
122
|
+
orderBy: { id: 'DESC' },
|
|
123
|
+
limit: 1,
|
|
124
|
+
}));
|
|
125
|
+
return rows[0]?.id ?? 0;
|
|
126
|
+
}
|
|
@@ -1,4 +1,30 @@
|
|
|
1
1
|
import type { LocaleResolver } from '../locale-registry.ts';
|
|
2
|
+
import { type AgentMeta } from '@coffer-org/sdk/library';
|
|
3
|
+
import type { Condition } from '@coffer-org/sdk/condition';
|
|
4
|
+
interface ShelfClient {
|
|
5
|
+
shelf: string;
|
|
6
|
+
library: string;
|
|
7
|
+
label: string;
|
|
8
|
+
agent?: string;
|
|
9
|
+
standalone?: boolean;
|
|
10
|
+
fields: unknown[];
|
|
11
|
+
}
|
|
12
|
+
interface ExtendClient {
|
|
13
|
+
id: string;
|
|
14
|
+
attachTo: {
|
|
15
|
+
library: string;
|
|
16
|
+
shelf?: string;
|
|
17
|
+
}[];
|
|
18
|
+
showWhen?: Condition;
|
|
19
|
+
agent?: AgentMeta | string;
|
|
20
|
+
}
|
|
21
|
+
interface LibraryClient {
|
|
22
|
+
id: string;
|
|
23
|
+
label: string;
|
|
24
|
+
agent?: AgentMeta | string;
|
|
25
|
+
extends?: ExtendClient[];
|
|
26
|
+
shelves: ShelfClient[];
|
|
27
|
+
}
|
|
2
28
|
export interface ShelfIndexEntry {
|
|
3
29
|
library: string;
|
|
4
30
|
libraryLabel: string;
|
|
@@ -25,13 +51,20 @@ export declare const FILE_WRITE_HINT: string;
|
|
|
25
51
|
export declare const SOURCE_WRITE_HINT: string;
|
|
26
52
|
export declare const DERIVED_WRITE_HINT: string;
|
|
27
53
|
export declare const serverOwnedPartsHint: (roles: string[]) => string;
|
|
54
|
+
export interface OperatingRule {
|
|
55
|
+
source: string;
|
|
56
|
+
when?: string;
|
|
57
|
+
text: string;
|
|
58
|
+
}
|
|
28
59
|
export interface ShelfDescription {
|
|
29
60
|
library: string;
|
|
30
61
|
shelf: string;
|
|
31
62
|
label: string;
|
|
32
63
|
agent?: string;
|
|
64
|
+
operatingRules?: OperatingRule[];
|
|
33
65
|
fields: FieldInfo[];
|
|
34
66
|
}
|
|
67
|
+
export declare function collectOperatingRules(lib: LibraryClient, shelf: string): OperatingRule[];
|
|
35
68
|
interface SchemaClient {
|
|
36
69
|
getSchema(): Promise<unknown>;
|
|
37
70
|
}
|
|
@@ -46,6 +79,6 @@ export declare class SchemaCache {
|
|
|
46
79
|
private load;
|
|
47
80
|
index(): Promise<ShelfIndexEntry[]>;
|
|
48
81
|
describeShelf(library: string, shelf: string): Promise<ShelfDescription>;
|
|
49
|
-
private
|
|
82
|
+
private locate;
|
|
50
83
|
}
|
|
51
84
|
export {};
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { normalizeAgentMeta } from '@coffer-org/sdk/library';
|
|
2
|
+
import { extendMatches } from '@coffer-org/sdk/extend';
|
|
3
|
+
import { describeCondition } from '@coffer-org/sdk/condition';
|
|
1
4
|
export const FILE_WRITE_HINT = 'Uploaded file only: {"name":"<filename returned by POST /api/upload>"} ' +
|
|
2
5
|
'(or an array of those when the field is multiple). ' +
|
|
3
6
|
'A remote URL is rejected — the server stores files, not links. ' +
|
|
@@ -13,6 +16,22 @@ export const DERIVED_WRITE_HINT = 'Read-only — the server recomputes this fiel
|
|
|
13
16
|
export const serverOwnedPartsHint = (roles) => `Partly read-only — the server owns ${roles.map((r) => `\`${r}\``).join(', ')} in this value and ` +
|
|
14
17
|
`computes ${roles.length > 1 ? 'them' : 'it'} on every write. Send the other roles only; anything ` +
|
|
15
18
|
`sent for these is discarded, and a read returns the server's value.`;
|
|
19
|
+
export function collectOperatingRules(lib, shelf) {
|
|
20
|
+
const out = [];
|
|
21
|
+
const libAgent = normalizeAgentMeta(lib.agent);
|
|
22
|
+
if (libAgent?.instructions)
|
|
23
|
+
out.push({ source: lib.id, text: libAgent.instructions });
|
|
24
|
+
for (const e of lib.extends ?? []) {
|
|
25
|
+
if (!extendMatches(e, lib.id, shelf))
|
|
26
|
+
continue;
|
|
27
|
+
const meta = normalizeAgentMeta(e.agent);
|
|
28
|
+
if (!meta?.instructions)
|
|
29
|
+
continue;
|
|
30
|
+
const when = describeCondition(e.showWhen, (f) => f).join(' and ');
|
|
31
|
+
out.push({ source: e.id, ...(when ? { when } : {}), text: meta.instructions });
|
|
32
|
+
}
|
|
33
|
+
return out;
|
|
34
|
+
}
|
|
16
35
|
function serverOwnedRoles(t) {
|
|
17
36
|
const parts = t.parts ?? [];
|
|
18
37
|
return parts.filter((p) => p.mode === 'computedStored').map((p) => p.role);
|
|
@@ -89,20 +108,24 @@ export class SchemaCache {
|
|
|
89
108
|
})));
|
|
90
109
|
}
|
|
91
110
|
async describeShelf(library, shelf) {
|
|
92
|
-
let
|
|
93
|
-
if (!
|
|
94
|
-
|
|
95
|
-
if (!
|
|
111
|
+
let at = this.locate(await this.load(), library, shelf);
|
|
112
|
+
if (!at)
|
|
113
|
+
at = this.locate(await this.load(true), library, shelf);
|
|
114
|
+
if (!at)
|
|
96
115
|
throw new Error(`unknown_shelf ${library}/${shelf}`);
|
|
116
|
+
const rules = collectOperatingRules(at.lib, shelf);
|
|
97
117
|
return {
|
|
98
118
|
library,
|
|
99
119
|
shelf,
|
|
100
|
-
label: this.display(m.label, m.shelf),
|
|
101
|
-
...(m.agent ? { agent: m.agent } : {}),
|
|
102
|
-
|
|
120
|
+
label: this.display(at.m.label, at.m.shelf),
|
|
121
|
+
...(at.m.agent ? { agent: at.m.agent } : {}),
|
|
122
|
+
...(rules.length ? { operatingRules: rules } : {}),
|
|
123
|
+
fields: flattenFields(at.m.fields),
|
|
103
124
|
};
|
|
104
125
|
}
|
|
105
|
-
|
|
106
|
-
|
|
126
|
+
locate(libraries, library, shelf) {
|
|
127
|
+
const lib = libraries.find((v) => v.id === library);
|
|
128
|
+
const m = lib?.shelves.find((s) => s.shelf === shelf);
|
|
129
|
+
return lib && m ? { lib, m } : undefined;
|
|
107
130
|
}
|
|
108
131
|
}
|
|
@@ -53,7 +53,9 @@ export function buildTools(client, cache, locales) {
|
|
|
53
53
|
},
|
|
54
54
|
{
|
|
55
55
|
name: 'describe_shelf',
|
|
56
|
-
description: 'Detailed shelf schema: the shelf note (agent),
|
|
56
|
+
description: 'Detailed shelf schema: the shelf note (agent), the operating rules that govern writing here ' +
|
|
57
|
+
'(operatingRules — from the library and from any extra field-set attached to this shelf, each ' +
|
|
58
|
+
'with the condition it applies under), then fields with kind/prim/required, relation/options, and ' +
|
|
57
59
|
'a per-field note (agent) saying what that field means. A collection is one entry with multiple:true and its ' +
|
|
58
60
|
'row shape in fields[]. A field with derived:true is computed by the server and rejects writes — omit it when ' +
|
|
59
61
|
'writing. Call BEFORE create_record/update_record.',
|
package/dist/mcp-tools.d.ts
CHANGED
|
@@ -4,6 +4,7 @@ import { type ToolResult } from './mcp-contract/tools.ts';
|
|
|
4
4
|
import { type LocaleResolver } from './locale-registry.ts';
|
|
5
5
|
import { type AuthRole, type PluginHooks } from './plugin-hooks.ts';
|
|
6
6
|
import { type Condition } from '@coffer-org/sdk/condition';
|
|
7
|
+
import { type AgentMeta } from '@coffer-org/sdk/library';
|
|
7
8
|
import { type RagHit } from './rag-search.ts';
|
|
8
9
|
export interface McpToolDef {
|
|
9
10
|
server: string;
|
|
@@ -58,12 +59,12 @@ export declare function collectLibraryPurposes(reg?: {
|
|
|
58
59
|
meta: {
|
|
59
60
|
id: string;
|
|
60
61
|
label?: string;
|
|
61
|
-
agent?: string;
|
|
62
|
+
agent?: AgentMeta | string;
|
|
62
63
|
};
|
|
63
64
|
}[];
|
|
64
65
|
extends_: {
|
|
65
66
|
id: string;
|
|
66
|
-
agent?: string;
|
|
67
|
+
agent?: AgentMeta | string;
|
|
67
68
|
showWhen?: Condition;
|
|
68
69
|
attachTo: {
|
|
69
70
|
library: string;
|
package/dist/mcp-tools.js
CHANGED
|
@@ -10,6 +10,7 @@ import { pluginHooks, pluginCtx } from "./plugin-hooks.js";
|
|
|
10
10
|
import { getActiveRegistry } from "./registry-context.js";
|
|
11
11
|
import { countTargetsFor, recordCounts } from "./counts.js";
|
|
12
12
|
import { describeCondition } from '@coffer-org/sdk/condition';
|
|
13
|
+
import { normalizeAgentMeta } from '@coffer-org/sdk/library';
|
|
13
14
|
import { getEm } from "./db.js";
|
|
14
15
|
import { getPluginSettings } from "./plugin-runtime.js";
|
|
15
16
|
import { configuredPublicUrl } from "./public-url.js";
|
|
@@ -130,9 +131,9 @@ export async function collectMcpTools(opts = {}) {
|
|
|
130
131
|
inputSchema: t.inputSchema,
|
|
131
132
|
scope: 'plugin',
|
|
132
133
|
role: t.role ?? 'member',
|
|
133
|
-
handler: async (args) => {
|
|
134
|
+
handler: async (args, signal) => {
|
|
134
135
|
try {
|
|
135
|
-
const data = await t.handler(args, pluginCtx(id, getEm().fork()));
|
|
136
|
+
const data = await t.handler(args, pluginCtx(id, getEm().fork(), signal));
|
|
136
137
|
return ok(data);
|
|
137
138
|
}
|
|
138
139
|
catch (e) {
|
|
@@ -162,7 +163,7 @@ export async function collectMcpTools(opts = {}) {
|
|
|
162
163
|
},
|
|
163
164
|
scope: 'rag',
|
|
164
165
|
role: 'member',
|
|
165
|
-
handler: async (args) => {
|
|
166
|
+
handler: async (args, signal) => {
|
|
166
167
|
const library = args.library;
|
|
167
168
|
const shelf = args.shelf;
|
|
168
169
|
if (shelf && !library) {
|
|
@@ -180,7 +181,7 @@ export async function collectMcpTools(opts = {}) {
|
|
|
180
181
|
}
|
|
181
182
|
let vector = null;
|
|
182
183
|
try {
|
|
183
|
-
vector = (await embedOne(args.query, embeddingApiKey)).vector;
|
|
184
|
+
vector = (await embedOne(args.query, embeddingApiKey, undefined, signal)).vector;
|
|
184
185
|
}
|
|
185
186
|
catch (e) {
|
|
186
187
|
log.warn(`embedding failed, text-only search — ${embeddingFailureMessage(e)}`);
|
|
@@ -296,16 +297,19 @@ export function collectLibraryPurposes(reg, locales) {
|
|
|
296
297
|
if (!registry)
|
|
297
298
|
return [];
|
|
298
299
|
return registry.libraries
|
|
299
|
-
.filter((v) => v.meta.agent)
|
|
300
|
+
.filter((v) => normalizeAgentMeta(v.meta.agent)?.description)
|
|
300
301
|
.map((v) => ({
|
|
301
302
|
id: v.meta.id,
|
|
302
303
|
name: (v.meta.label ? locales?.resolve(v.meta.label) : undefined) ?? v.meta.id,
|
|
303
|
-
agent: v.meta.agent,
|
|
304
|
-
extends: registry.extends_.flatMap((e) =>
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
304
|
+
agent: normalizeAgentMeta(v.meta.agent).description,
|
|
305
|
+
extends: registry.extends_.flatMap((e) => {
|
|
306
|
+
const meta = normalizeAgentMeta(e.agent);
|
|
307
|
+
return meta?.description
|
|
308
|
+
? e.attachTo
|
|
309
|
+
.filter((a) => a.library === v.meta.id)
|
|
310
|
+
.map((a) => ({ id: e.id, shelf: a.shelf, agent: meta.description, showWhen: e.showWhen }))
|
|
311
|
+
: [];
|
|
312
|
+
}),
|
|
309
313
|
}));
|
|
310
314
|
}
|
|
311
315
|
export async function siteSection(siteUrl) {
|
|
@@ -17,6 +17,7 @@ export { isSenderAllowed } from './allow.ts';
|
|
|
17
17
|
export { makeLiveChannel, plainRender } from './live-message.ts';
|
|
18
18
|
export type { LiveChannelOps, LiveChannelOpts, RenderFn } from './live-message.ts';
|
|
19
19
|
export { chunk } from './format.ts';
|
|
20
|
+
export { DEFAULT_TASK_TIMEOUT_MS } from '../background-scheduler.ts';
|
|
20
21
|
export type { Connector, IncomingConversation, GatePolicy, ReplyContext, ReplyPayload, ReplyChannel, AttachmentRef, AttachmentMaterializer, AgentToolDefinition, AgentToolProvider, ConvMessage, AgentRequest, AgentResult, AgentRuntime, AgentCapabilities, AgentPreset, AgentDescriptor, ConnectorRegistration, AgentToolContentBlock, AgentToolContentResult, } from './types.ts';
|
|
21
22
|
export declare function startOrchestrator(): void;
|
|
22
23
|
export declare function stopOrchestrator(): void;
|
|
@@ -20,6 +20,7 @@ export { isSenderAllowed } from "./allow.js";
|
|
|
20
20
|
import { carryLegacyState } from "./allow.js";
|
|
21
21
|
export { makeLiveChannel, plainRender } from "./live-message.js";
|
|
22
22
|
export { chunk } from "./format.js";
|
|
23
|
+
export { DEFAULT_TASK_TIMEOUT_MS } from "../background-scheduler.js";
|
|
23
24
|
let db;
|
|
24
25
|
const STARTERS_CHECK_INTERVAL_MS = 10 * 60_000;
|
|
25
26
|
function runStartersRefresh() {
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { getActiveRegistry } from "../registry-context.js";
|
|
2
|
+
export function shelfSources(slug, getShelfOwner) {
|
|
3
|
+
const parts = slug.split('/');
|
|
4
|
+
if (parts.length !== 2)
|
|
5
|
+
return [];
|
|
6
|
+
const [library, shelf] = parts;
|
|
7
|
+
if (!library || !shelf)
|
|
8
|
+
return [];
|
|
9
|
+
const out = [`library:${library}`];
|
|
10
|
+
const owner = getShelfOwner(library, shelf);
|
|
11
|
+
if (owner)
|
|
12
|
+
out.push(`plugin:${owner}`);
|
|
13
|
+
return out;
|
|
14
|
+
}
|
|
15
|
+
export function defaultShelfSources(slug) {
|
|
16
|
+
try {
|
|
17
|
+
return shelfSources(slug, getActiveRegistry().getShelfOwner);
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
return [];
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -3,6 +3,7 @@ import { getPluginState, setPluginState } from '../plugin-state.ts';
|
|
|
3
3
|
import { collectStarterHints } from '../mcp-tools.ts';
|
|
4
4
|
import { loadAgentId } from './config.ts';
|
|
5
5
|
import { getDefaultAgentId } from './registry.ts';
|
|
6
|
+
import { listEventsSince, latestEventId } from '../embeddings.ts';
|
|
6
7
|
import type { AgentRuntime } from './types.ts';
|
|
7
8
|
export interface StartersDeps {
|
|
8
9
|
getPluginSettings: typeof getPluginSettings;
|
|
@@ -14,6 +15,9 @@ export interface StartersDeps {
|
|
|
14
15
|
getDefaultAgentId: typeof getDefaultAgentId;
|
|
15
16
|
now: () => number;
|
|
16
17
|
shuffle: <T>(xs: T[]) => T[];
|
|
18
|
+
listEventsSince: typeof listEventsSince;
|
|
19
|
+
latestEventId: typeof latestEventId;
|
|
20
|
+
shelfSources: (slug: string) => string[];
|
|
17
21
|
}
|
|
18
22
|
export declare function refreshSystemStarters(deps?: StartersDeps): Promise<void>;
|
|
19
23
|
export declare function getConversationStarters(deps?: StartersDeps): Promise<string[]>;
|
|
@@ -5,14 +5,20 @@ import { collectStarterHints } from "../mcp-tools.js";
|
|
|
5
5
|
import { getLogger } from '@coffer-org/sdk/logger';
|
|
6
6
|
import { loadAgentId } from "./config.js";
|
|
7
7
|
import { resolveAgent, getDefaultAgentId } from "./registry.js";
|
|
8
|
+
import { listEventsSince, latestEventId } from "../embeddings.js";
|
|
9
|
+
import { defaultShelfSources } from "./starter-sources.js";
|
|
8
10
|
const log = getLogger('orchestrator');
|
|
9
11
|
const PLUGIN = 'orchestrator';
|
|
10
12
|
const STATE_KEY = 'system_starters';
|
|
11
13
|
const POOL_VERSION = 2;
|
|
12
14
|
const ENTRY_TTL_MS = 24 * 60 * 60_000;
|
|
15
|
+
const EARLY_DEBOUNCE_MS = 60 * 60_000;
|
|
16
|
+
const EARLY_COOLDOWN_MS = 6 * 60 * 60_000;
|
|
17
|
+
const EARLY_BUDGET_PER_DAY = 10;
|
|
13
18
|
const SAMPLE_MIN = 4;
|
|
14
19
|
const SAMPLE_MAX = 5;
|
|
15
20
|
const PER_SOURCE_SOFT_CAP = 2;
|
|
21
|
+
const EVENT_BATCH = 5000;
|
|
16
22
|
function shuffled(xs) {
|
|
17
23
|
const out = [...xs];
|
|
18
24
|
for (let i = out.length - 1; i > 0; i--) {
|
|
@@ -31,6 +37,9 @@ const defaultDeps = {
|
|
|
31
37
|
getDefaultAgentId,
|
|
32
38
|
now: () => Date.now(),
|
|
33
39
|
shuffle: shuffled,
|
|
40
|
+
listEventsSince,
|
|
41
|
+
latestEventId,
|
|
42
|
+
shelfSources: defaultShelfSources,
|
|
34
43
|
};
|
|
35
44
|
async function currentLanguage(deps) {
|
|
36
45
|
const system = await deps.getPluginSettings(SYSTEM_SETTINGS_ID);
|
|
@@ -58,6 +67,33 @@ async function readPool(deps) {
|
|
|
58
67
|
function fresh(entry, nowMs) {
|
|
59
68
|
return nowMs - Date.parse(entry.generatedAt) < ENTRY_TTL_MS;
|
|
60
69
|
}
|
|
70
|
+
function dayKey(nowMs) {
|
|
71
|
+
return new Date(nowMs).toISOString().slice(0, 10);
|
|
72
|
+
}
|
|
73
|
+
function earlyBudgetLeft(pool, nowMs) {
|
|
74
|
+
const spent = pool.earlyRuns;
|
|
75
|
+
if (!spent || spent.date !== dayKey(nowMs))
|
|
76
|
+
return true;
|
|
77
|
+
return spent.count < EARLY_BUDGET_PER_DAY;
|
|
78
|
+
}
|
|
79
|
+
function countEarlyRun(pool, nowMs) {
|
|
80
|
+
const key = dayKey(nowMs);
|
|
81
|
+
pool.earlyRuns =
|
|
82
|
+
pool.earlyRuns?.date === key ? { date: key, count: pool.earlyRuns.count + 1 } : { date: key, count: 1 };
|
|
83
|
+
}
|
|
84
|
+
function pickEarly(pool, hints, nowMs) {
|
|
85
|
+
const at = (h) => Date.parse(pool.sources[h.id].generatedAt);
|
|
86
|
+
return hints
|
|
87
|
+
.filter((h) => {
|
|
88
|
+
const e = pool.sources[h.id];
|
|
89
|
+
if (!e?.dirtySince)
|
|
90
|
+
return false;
|
|
91
|
+
if (nowMs - Date.parse(e.dirtySince) < EARLY_DEBOUNCE_MS)
|
|
92
|
+
return false;
|
|
93
|
+
return nowMs - Date.parse(e.generatedAt) >= EARLY_COOLDOWN_MS;
|
|
94
|
+
})
|
|
95
|
+
.sort((a, b) => at(a) - at(b))[0];
|
|
96
|
+
}
|
|
61
97
|
function tryResolve(deps, agentId) {
|
|
62
98
|
try {
|
|
63
99
|
return deps.resolveAgent(agentId);
|
|
@@ -66,6 +102,27 @@ function tryResolve(deps, agentId) {
|
|
|
66
102
|
return undefined;
|
|
67
103
|
}
|
|
68
104
|
}
|
|
105
|
+
async function markChangedSources(deps, pool, declared, nowMs) {
|
|
106
|
+
if (pool.eventCursor == null) {
|
|
107
|
+
pool.eventCursor = await deps.latestEventId();
|
|
108
|
+
return true;
|
|
109
|
+
}
|
|
110
|
+
const rows = await deps.listEventsSince(pool.eventCursor, EVENT_BATCH);
|
|
111
|
+
if (!rows.length)
|
|
112
|
+
return false;
|
|
113
|
+
for (const row of rows) {
|
|
114
|
+
for (const id of deps.shelfSources(String(row.type ?? ''))) {
|
|
115
|
+
if (!declared.has(id))
|
|
116
|
+
continue;
|
|
117
|
+
const entry = pool.sources[id];
|
|
118
|
+
if (!entry || entry.dirtySince)
|
|
119
|
+
continue;
|
|
120
|
+
entry.dirtySince = new Date(nowMs).toISOString();
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
pool.eventCursor = rows[rows.length - 1].id;
|
|
124
|
+
return true;
|
|
125
|
+
}
|
|
69
126
|
export async function refreshSystemStarters(deps = defaultDeps) {
|
|
70
127
|
const [language, hints] = await Promise.all([currentLanguage(deps), deps.collectStarterHints()]);
|
|
71
128
|
const agentId = (await deps.loadAgentId()) ?? deps.getDefaultAgentId();
|
|
@@ -82,6 +139,8 @@ export async function refreshSystemStarters(deps = defaultDeps) {
|
|
|
82
139
|
}
|
|
83
140
|
}
|
|
84
141
|
const nowMs = deps.now();
|
|
142
|
+
if (await markChangedSources(deps, pool, declared, nowMs))
|
|
143
|
+
dirty = true;
|
|
85
144
|
const stale = hints.find((h) => {
|
|
86
145
|
const e = pool.sources[h.id];
|
|
87
146
|
if (!e)
|
|
@@ -90,21 +149,25 @@ export async function refreshSystemStarters(deps = defaultDeps) {
|
|
|
90
149
|
return true;
|
|
91
150
|
return !fresh(e, nowMs);
|
|
92
151
|
});
|
|
93
|
-
|
|
152
|
+
const early = stale || !earlyBudgetLeft(pool, nowMs) ? undefined : pickEarly(pool, hints, nowMs);
|
|
153
|
+
const target = stale ?? early;
|
|
154
|
+
if (target) {
|
|
94
155
|
const runtime = tryResolve(deps, agentId);
|
|
95
156
|
if (runtime?.starters) {
|
|
96
157
|
try {
|
|
97
|
-
const messages = await runtime.starters(
|
|
98
|
-
pool.sources[
|
|
158
|
+
const messages = await runtime.starters(target.text);
|
|
159
|
+
pool.sources[target.id] = {
|
|
99
160
|
messages,
|
|
100
161
|
generatedAt: new Date(nowMs).toISOString(),
|
|
101
|
-
hintHash:
|
|
162
|
+
hintHash: target.hash,
|
|
102
163
|
agentId,
|
|
103
164
|
};
|
|
165
|
+
if (target === early)
|
|
166
|
+
countEarlyRun(pool, nowMs);
|
|
104
167
|
dirty = true;
|
|
105
168
|
}
|
|
106
169
|
catch (err) {
|
|
107
|
-
log.warn(`starters for ${
|
|
170
|
+
log.warn(`starters for ${target.id} failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
108
171
|
}
|
|
109
172
|
}
|
|
110
173
|
}
|
|
@@ -60,11 +60,16 @@ export interface AgentDescriptor {
|
|
|
60
60
|
title: string;
|
|
61
61
|
presets: AgentPreset[];
|
|
62
62
|
}
|
|
63
|
+
export interface AgentBudget {
|
|
64
|
+
responseTimeout?: number;
|
|
65
|
+
toolRounds?: number;
|
|
66
|
+
}
|
|
63
67
|
export interface AgentRequest {
|
|
64
68
|
system: string;
|
|
65
69
|
messages: ConvMessage[];
|
|
66
70
|
toolProvider?: AgentToolProvider;
|
|
67
71
|
presetId?: string;
|
|
72
|
+
budget?: AgentBudget;
|
|
68
73
|
onDelta?: (accumulated: string) => void;
|
|
69
74
|
onReasoning?: (accumulated: string) => void;
|
|
70
75
|
onSegment?: () => void;
|
package/dist/plugin-hooks.d.ts
CHANGED
|
@@ -25,8 +25,9 @@ export interface Migration {
|
|
|
25
25
|
export interface PluginCtx {
|
|
26
26
|
em: EntityManager;
|
|
27
27
|
log: Logger;
|
|
28
|
+
signal?: AbortSignal;
|
|
28
29
|
}
|
|
29
|
-
export declare function pluginCtx(id: string, em: EntityManager): PluginCtx;
|
|
30
|
+
export declare function pluginCtx(id: string, em: EntityManager, signal?: AbortSignal): PluginCtx;
|
|
30
31
|
export interface Seed {
|
|
31
32
|
name: string;
|
|
32
33
|
run(ctx: PluginCtx): Promise<void> | void;
|
package/dist/plugin-hooks.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { getLogger } from '@coffer-org/sdk/logger';
|
|
2
|
-
export function pluginCtx(id, em) {
|
|
3
|
-
return { em, log: getLogger(id) };
|
|
2
|
+
export function pluginCtx(id, em, signal) {
|
|
3
|
+
return { em, log: getLogger(id), ...(signal ? { signal } : {}) };
|
|
4
4
|
}
|
|
5
5
|
export class HttpError extends Error {
|
|
6
6
|
status;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@coffer-org/server",
|
|
3
|
-
"version": "7.
|
|
3
|
+
"version": "7.2.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": "^7.
|
|
39
|
+
"@coffer-org/sdk": "^7.2.0",
|
|
40
40
|
"@extractus/oembed-extractor": "^4.1.0",
|
|
41
41
|
"@fastify/cors": "^11.2.0",
|
|
42
42
|
"@fastify/multipart": "^10.0.0",
|