@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.
@@ -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']) || 600_000);
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 = [];
@@ -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
  }>;
@@ -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');
@@ -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>;
@@ -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 find;
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 m = this.find(await this.load(), library, shelf);
93
- if (!m)
94
- m = this.find(await this.load(true), library, shelf);
95
- if (!m)
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
- fields: flattenFields(m.fields),
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
- find(libraries, library, shelf) {
106
- return libraries.find((v) => v.id === library)?.shelves.find((m) => m.shelf === shelf);
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), then fields with kind/prim/required, relation/options, and ' +
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.',
@@ -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) => e.agent
305
- ? e.attachTo
306
- .filter((a) => a.library === v.meta.id)
307
- .map((a) => ({ id: e.id, shelf: a.shelf, agent: e.agent, showWhen: e.showWhen }))
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,2 @@
1
+ export declare function shelfSources(slug: string, getShelfOwner: (library: string, shelf: string) => string | undefined): string[];
2
+ export declare function defaultShelfSources(slug: string): string[];
@@ -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
- if (stale) {
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(stale.text);
98
- pool.sources[stale.id] = {
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: stale.hash,
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 ${stale.id} failed: ${err instanceof Error ? err.message : String(err)}`);
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;
@@ -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;
@@ -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.0.0",
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.0.0",
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",