@coffer-org/server 7.2.0 → 7.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/auth-api.d.ts +4 -0
- package/dist/auth-api.js +52 -0
- package/dist/auth-store.d.ts +2 -0
- package/dist/auth-store.js +1 -0
- package/dist/compute-unit.js +0 -3
- package/dist/conversation-store.d.ts +61 -0
- package/dist/conversation-store.js +223 -0
- package/dist/entity-schema.d.ts +5 -2
- package/dist/entity-schema.js +35 -18
- package/dist/identity-link.d.ts +15 -0
- package/dist/identity-link.js +76 -0
- package/dist/identity-providers.d.ts +17 -0
- package/dist/identity-providers.js +14 -0
- package/dist/index.js +8 -1
- package/dist/mcp-contract/schema.d.ts +0 -1
- package/dist/mcp-contract/schema.js +0 -2
- package/dist/mcp-http.js +7 -3
- package/dist/mcp-tools.d.ts +4 -3
- package/dist/mcp-tools.js +84 -84
- package/dist/media/image.d.ts +23 -0
- package/dist/media/image.js +103 -0
- package/dist/media/index.d.ts +1 -0
- package/dist/media/index.js +1 -0
- package/dist/migrations.js +1 -1
- package/dist/mutate.js +0 -15
- package/dist/orchestrator/agent-capabilities.d.ts +2 -2
- package/dist/orchestrator/agent-capabilities.js +3 -3
- package/dist/orchestrator/allow.d.ts +1 -16
- package/dist/orchestrator/allow.js +3 -53
- package/dist/orchestrator/config.js +0 -1
- package/dist/orchestrator/context-facts.d.ts +27 -0
- package/dist/orchestrator/context-facts.js +89 -0
- package/dist/orchestrator/conversation-access.d.ts +9 -0
- package/dist/orchestrator/conversation-access.js +12 -0
- package/dist/orchestrator/draft-message.d.ts +18 -0
- package/dist/orchestrator/draft-message.js +85 -0
- package/dist/orchestrator/environment.d.ts +1 -0
- package/dist/orchestrator/environment.js +10 -0
- package/dist/orchestrator/file-inspection.d.ts +2 -2
- package/dist/orchestrator/file-inspection.js +41 -19
- package/dist/orchestrator/index.d.ts +14 -9
- package/dist/orchestrator/index.js +6 -7
- package/dist/orchestrator/live-message.d.ts +7 -4
- package/dist/orchestrator/live-message.js +52 -32
- package/dist/orchestrator/pipeline.d.ts +22 -4
- package/dist/orchestrator/pipeline.js +316 -115
- package/dist/orchestrator/registry.d.ts +2 -2
- package/dist/orchestrator/registry.js +1 -1
- package/dist/orchestrator/system-areas.d.ts +12 -0
- package/dist/orchestrator/system-areas.js +63 -0
- package/dist/orchestrator/system-capabilities.js +1 -1
- package/dist/orchestrator/turn-context.d.ts +18 -0
- package/dist/orchestrator/turn-context.js +39 -0
- package/dist/orchestrator/types.d.ts +100 -49
- package/dist/plugin-hooks.d.ts +38 -1
- package/dist/plugin-hooks.js +4 -0
- package/dist/plugin-http-mounts.d.ts +18 -0
- package/dist/plugin-http-mounts.js +94 -0
- package/dist/plugin-runtime.d.ts +4 -0
- package/dist/plugin-runtime.js +9 -5
- package/dist/plugins-api.d.ts +8 -2
- package/dist/plugins-api.js +4 -2
- package/dist/records-api.js +15 -3
- package/dist/settings-write.d.ts +1 -2
- package/dist/settings-write.js +1 -2
- package/dist/system-settings.js +0 -1
- package/dist/temporal.js +8 -3
- package/dist/thread-state.d.ts +14 -0
- package/dist/thread-state.js +71 -11
- package/dist/thread-store.d.ts +5 -3
- package/dist/thread-store.js +12 -9
- package/dist/turn-gate.d.ts +8 -0
- package/dist/turn-gate.js +39 -0
- package/package.json +7 -2
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { currentMoment } from "./environment.js";
|
|
2
|
+
export const CONNECTOR_FACT_NAME = /^[a-z][a-z0-9-]*$/;
|
|
3
|
+
export const CONTEXT_GAP_MS = 60 * 60_000;
|
|
4
|
+
export const MAX_SPEAKER_NAME = 80;
|
|
5
|
+
export const MAX_FACT_VALUE = 200;
|
|
6
|
+
export const MAX_CONNECTOR_FACTS = 12;
|
|
7
|
+
const FORGERY_CHARS = /[\p{Cc}\p{Zl}\p{Zp}\p{Bidi_Control}]+/gu;
|
|
8
|
+
export function oneLine(value, maxLen) {
|
|
9
|
+
const flat = value.replace(FORGERY_CHARS, ' ').replace(/\s+/g, ' ').trim();
|
|
10
|
+
return maxLen !== undefined && flat.length > maxLen ? `${flat.slice(0, maxLen - 1).trimEnd()}…` : flat;
|
|
11
|
+
}
|
|
12
|
+
const ORCHESTRATOR_FACT_NAMES = new Set(['at', 'gap', 'speaker', 'cleared']);
|
|
13
|
+
function sameFact(a, b) {
|
|
14
|
+
if (a.value !== b.value)
|
|
15
|
+
return false;
|
|
16
|
+
const ka = Object.keys(a.attrs ?? {}).sort();
|
|
17
|
+
const kb = Object.keys(b.attrs ?? {}).sort();
|
|
18
|
+
if (ka.length !== kb.length || ka.some((k, i) => k !== kb[i]))
|
|
19
|
+
return false;
|
|
20
|
+
return ka.every((k) => a.attrs[k] === b.attrs[k]);
|
|
21
|
+
}
|
|
22
|
+
function dayAndZone(atValue) {
|
|
23
|
+
const [date, , ...zone] = atValue.split(' ');
|
|
24
|
+
return `${date} ${zone.join(' ')}`;
|
|
25
|
+
}
|
|
26
|
+
export function humanizeGap(ms) {
|
|
27
|
+
const minutes = Math.round(ms / 60_000);
|
|
28
|
+
if (minutes < 90)
|
|
29
|
+
return `about ${minutes} minutes`;
|
|
30
|
+
const hours = Math.round(ms / 3_600_000);
|
|
31
|
+
if (hours < 48)
|
|
32
|
+
return `about ${hours} hours`;
|
|
33
|
+
return `about ${Math.round(ms / 86_400_000)} days`;
|
|
34
|
+
}
|
|
35
|
+
export function buildContextFacts(input) {
|
|
36
|
+
const prev = input.previous;
|
|
37
|
+
const stated = (name) => prev?.find((f) => f.name === name);
|
|
38
|
+
const moment = currentMoment(input.now, input.timeZone);
|
|
39
|
+
const at = { name: 'at', value: `${moment} ${input.timeZone}` };
|
|
40
|
+
const speakerName = oneLine(input.speaker.name, MAX_SPEAKER_NAME) || 'unknown';
|
|
41
|
+
const speaker = {
|
|
42
|
+
name: 'speaker',
|
|
43
|
+
value: speakerName,
|
|
44
|
+
attrs: { id: oneLine(input.speaker.id, MAX_SPEAKER_NAME), role: input.speaker.role },
|
|
45
|
+
};
|
|
46
|
+
const facts = [];
|
|
47
|
+
const gapMs = input.previousAt ? input.now.getTime() - input.previousAt.getTime() : null;
|
|
48
|
+
const prevAt = stated('at');
|
|
49
|
+
const dayChanged = prevAt !== undefined && dayAndZone(prevAt.value) !== dayAndZone(at.value);
|
|
50
|
+
const stale = gapMs !== null && gapMs >= CONTEXT_GAP_MS;
|
|
51
|
+
if (!prevAt || dayChanged || stale) {
|
|
52
|
+
facts.push(at);
|
|
53
|
+
if (stale && gapMs !== null)
|
|
54
|
+
facts.push({ name: 'gap', value: humanizeGap(gapMs) });
|
|
55
|
+
}
|
|
56
|
+
const prevSpeaker = stated('speaker');
|
|
57
|
+
if (!prevSpeaker || !sameFact(prevSpeaker, speaker))
|
|
58
|
+
facts.push(speaker);
|
|
59
|
+
const validConnectorFacts = input.connectorFacts
|
|
60
|
+
.filter((f) => typeof f?.name === 'string' &&
|
|
61
|
+
CONNECTOR_FACT_NAME.test(f.name) &&
|
|
62
|
+
!ORCHESTRATOR_FACT_NAMES.has(f.name) &&
|
|
63
|
+
typeof f.value === 'string' &&
|
|
64
|
+
Object.entries(f.attrs ?? {}).every(([k, v]) => CONNECTOR_FACT_NAME.test(k) && typeof v === 'string'))
|
|
65
|
+
.slice(0, MAX_CONNECTOR_FACTS);
|
|
66
|
+
const currentConnectorNames = new Set(validConnectorFacts.map((f) => f.name));
|
|
67
|
+
for (const raw of validConnectorFacts) {
|
|
68
|
+
const fact = {
|
|
69
|
+
name: raw.name,
|
|
70
|
+
value: oneLine(raw.value, MAX_FACT_VALUE),
|
|
71
|
+
...(raw.attrs
|
|
72
|
+
? { attrs: Object.fromEntries(Object.entries(raw.attrs).map(([k, v]) => [k, oneLine(v, MAX_FACT_VALUE)])) }
|
|
73
|
+
: {}),
|
|
74
|
+
};
|
|
75
|
+
const before = stated(fact.name);
|
|
76
|
+
if (!before || !sameFact(before, fact))
|
|
77
|
+
facts.push(fact);
|
|
78
|
+
}
|
|
79
|
+
if (prev) {
|
|
80
|
+
for (const priorFact of prev) {
|
|
81
|
+
if (ORCHESTRATOR_FACT_NAMES.has(priorFact.name))
|
|
82
|
+
continue;
|
|
83
|
+
if (currentConnectorNames.has(priorFact.name))
|
|
84
|
+
continue;
|
|
85
|
+
facts.push({ name: 'cleared', value: oneLine(priorFact.name) });
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return facts;
|
|
89
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export interface ConversationAccess {
|
|
2
|
+
owner: string | null;
|
|
3
|
+
visibility: 'private' | null;
|
|
4
|
+
}
|
|
5
|
+
export declare function mayRead(access: ConversationAccess, viewerId: string): boolean;
|
|
6
|
+
export declare function mayWrite(access: ConversationAccess, viewerId: string): boolean;
|
|
7
|
+
export declare function mayBePrivate(capabilities: {
|
|
8
|
+
privateChats: boolean;
|
|
9
|
+
}): boolean;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
function isOwner(access, viewerId) {
|
|
2
|
+
return access.owner !== null && access.owner === viewerId;
|
|
3
|
+
}
|
|
4
|
+
export function mayRead(access, viewerId) {
|
|
5
|
+
return access.visibility !== 'private' || isOwner(access, viewerId);
|
|
6
|
+
}
|
|
7
|
+
export function mayWrite(access, viewerId) {
|
|
8
|
+
return access.visibility !== 'private' || isOwner(access, viewerId);
|
|
9
|
+
}
|
|
10
|
+
export function mayBePrivate(capabilities) {
|
|
11
|
+
return capabilities.privateChats;
|
|
12
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { TurnSink } from './types.ts';
|
|
2
|
+
import type { RenderFn } from './live-message.ts';
|
|
3
|
+
export interface DraftChannelOps {
|
|
4
|
+
draft(text: string): Promise<void>;
|
|
5
|
+
send(text: string): Promise<string | null>;
|
|
6
|
+
}
|
|
7
|
+
export interface DraftSinkOpts {
|
|
8
|
+
ops: DraftChannelOps;
|
|
9
|
+
throttleMs: number;
|
|
10
|
+
keepAliveMs: number;
|
|
11
|
+
render: RenderFn;
|
|
12
|
+
preview: (r: {
|
|
13
|
+
text: string | null;
|
|
14
|
+
reasoning: string | null;
|
|
15
|
+
}) => string;
|
|
16
|
+
maxLength: number;
|
|
17
|
+
}
|
|
18
|
+
export declare function makeDraftSink(o: DraftSinkOpts): TurnSink;
|
|
@@ -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
|
+
}
|
|
@@ -12,3 +12,13 @@ function part(now, timeZone, locale, options) {
|
|
|
12
12
|
export function todayDateString(now = new Date(), timeZone = systemTimeZone()) {
|
|
13
13
|
return part(now, timeZone, 'en-CA', { year: 'numeric', month: '2-digit', day: '2-digit' });
|
|
14
14
|
}
|
|
15
|
+
export function currentMoment(now = new Date(), timeZone = systemTimeZone()) {
|
|
16
|
+
return part(now, timeZone, 'en-CA', {
|
|
17
|
+
year: 'numeric',
|
|
18
|
+
month: '2-digit',
|
|
19
|
+
day: '2-digit',
|
|
20
|
+
hour: '2-digit',
|
|
21
|
+
minute: '2-digit',
|
|
22
|
+
hourCycle: 'h23',
|
|
23
|
+
}).replace(', ', ' ');
|
|
24
|
+
}
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import type { AgentToolContentResult, AttachmentRef } from './types.ts';
|
|
2
|
-
export declare function inspectUpload(name: string, ref
|
|
1
|
+
import type { AgentMediaLimits, AgentToolContentResult, AttachmentRef } from './types.ts';
|
|
2
|
+
export declare function inspectUpload(name: string, ref: Pick<AttachmentRef, 'mime' | 'size' | 'label'>, limits: AgentMediaLimits): Promise<AgentToolContentResult>;
|
|
3
3
|
export declare function isAgentToolContentResult(value: unknown): value is AgentToolContentResult;
|
|
@@ -2,15 +2,25 @@ import { readFile, stat } from 'node:fs/promises';
|
|
|
2
2
|
import { basename, join } from 'node:path';
|
|
3
3
|
import { mimeForName } from "../file-fields.js";
|
|
4
4
|
import { uploadsDir } from "../uploads.js";
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
const
|
|
5
|
+
import { normalizeImage, ImageNormalizeError } from "../media/index.js";
|
|
6
|
+
import { getLogger } from '@coffer-org/sdk/logger';
|
|
7
|
+
const log = getLogger('orchestrator');
|
|
8
8
|
const textResult = (text, isError = false) => ({
|
|
9
9
|
__cofferToolContent: true,
|
|
10
10
|
content: [{ type: 'text', text }],
|
|
11
11
|
...(isError ? { isError: true } : {}),
|
|
12
12
|
});
|
|
13
|
-
|
|
13
|
+
function accepts(mime, patterns) {
|
|
14
|
+
return patterns.some((p) => (p.endsWith('/*') ? mime.startsWith(p.slice(0, -1)) : p === mime));
|
|
15
|
+
}
|
|
16
|
+
function human(bytes) {
|
|
17
|
+
if (bytes < 1024)
|
|
18
|
+
return `${bytes} B`;
|
|
19
|
+
if (bytes < 1024 * 1024)
|
|
20
|
+
return `${Math.round(bytes / 1024)} KB`;
|
|
21
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
22
|
+
}
|
|
23
|
+
export async function inspectUpload(name, ref, limits) {
|
|
14
24
|
if (!name || basename(name) !== name)
|
|
15
25
|
return textResult('Invalid upload name.', true);
|
|
16
26
|
const file = join(uploadsDir(), name);
|
|
@@ -22,34 +32,46 @@ export async function inspectUpload(name, ref = {}) {
|
|
|
22
32
|
return textResult(`File is no longer present on the server: ${name}`, true);
|
|
23
33
|
}
|
|
24
34
|
const mime = ref.mime ?? mimeForName(name);
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
return textResult(`Image is ${size} bytes, above the ${MAX_VISION_BYTES}-byte inspection limit.`, true);
|
|
35
|
+
const label = ref.label ?? name;
|
|
36
|
+
if (mime && accepts(mime, limits.image.accepts)) {
|
|
28
37
|
const bytes = await readFile(file);
|
|
38
|
+
let image;
|
|
39
|
+
try {
|
|
40
|
+
image = await normalizeImage(bytes, limits.image);
|
|
41
|
+
}
|
|
42
|
+
catch (err) {
|
|
43
|
+
if (err instanceof ImageNormalizeError)
|
|
44
|
+
return textResult(`Image could not be prepared for the agent: ${err.message}`, true);
|
|
45
|
+
throw err;
|
|
46
|
+
}
|
|
47
|
+
const shape = image.changed
|
|
48
|
+
? `${image.source.width}×${image.source.height} ${human(image.source.bytes)} → ${image.width}×${image.height} ${human(image.bytes.length)}, ${image.mime}`
|
|
49
|
+
: `${image.width}×${image.height}, ${image.mime}, ${human(image.bytes.length)}`;
|
|
50
|
+
if (image.changed)
|
|
51
|
+
log.debug(`normalized ${name}: ${image.source.bytes} → ${image.bytes.length} bytes for the agent`);
|
|
29
52
|
return {
|
|
30
53
|
__cofferToolContent: true,
|
|
31
54
|
content: [
|
|
32
|
-
{ type: 'text', text: `Image: ${
|
|
33
|
-
{ type: 'image', mime, data: bytes.toString('base64') },
|
|
55
|
+
{ type: 'text', text: `Image: ${label} (${shape}).` },
|
|
56
|
+
{ type: 'image', mime: image.mime, data: image.bytes.toString('base64') },
|
|
34
57
|
],
|
|
35
58
|
};
|
|
36
59
|
}
|
|
37
|
-
if (mime
|
|
38
|
-
if (size >
|
|
39
|
-
return textResult(`
|
|
40
|
-
const bytes = await readFile(file);
|
|
60
|
+
if (mime && accepts(mime, limits.document.accepts)) {
|
|
61
|
+
if (size > limits.document.maxBytes)
|
|
62
|
+
return textResult(`Document is ${human(size)}, above the ${human(limits.document.maxBytes)} the agent accepts.`, true);
|
|
41
63
|
return {
|
|
42
64
|
__cofferToolContent: true,
|
|
43
65
|
content: [
|
|
44
|
-
{ type: 'text', text: `PDF: ${
|
|
45
|
-
{ type: 'document', mime: 'application/pdf', data:
|
|
66
|
+
{ type: 'text', text: `PDF: ${label} (${human(size)}).` },
|
|
67
|
+
{ type: 'document', mime: 'application/pdf', data: (await readFile(file)).toString('base64') },
|
|
46
68
|
],
|
|
47
69
|
};
|
|
48
70
|
}
|
|
49
|
-
if (mime
|
|
50
|
-
if (size >
|
|
51
|
-
return textResult(`Text file is
|
|
52
|
-
return textResult(`Text file: ${
|
|
71
|
+
if (mime && accepts(mime, limits.text.accepts)) {
|
|
72
|
+
if (size > limits.text.maxBytes)
|
|
73
|
+
return textResult(`Text file is ${human(size)}, above the ${human(limits.text.maxBytes)} the agent accepts.`, true);
|
|
74
|
+
return textResult(`Text file: ${label}\n\n${(await readFile(file)).toString('utf8')}`);
|
|
53
75
|
}
|
|
54
76
|
return textResult(`This file type cannot be inspected by the agent (${mime ?? 'unknown'}). ` +
|
|
55
77
|
'It can still be attached to a record.', true);
|
|
@@ -1,24 +1,29 @@
|
|
|
1
1
|
import type { BackgroundTask } from '../background-scheduler.ts';
|
|
2
2
|
import { listRecentMessageMetrics } from '../msg-log.ts';
|
|
3
3
|
import { listDiagnostics } from './diagnostics.ts';
|
|
4
|
-
|
|
4
|
+
import { type PipelineOptions } from './pipeline.ts';
|
|
5
|
+
import type { Connector, TurnRequest } from './types.ts';
|
|
6
|
+
export declare const handleIncoming: (connector: Connector, turn: TurnRequest, options?: PipelineOptions) => Promise<void>;
|
|
5
7
|
export { registerAgent, resolveAgent, registerConnector, isConnectorRegistered, clearRuntimeRegistries, listRegisteredAgents, listRegisteredConnectors, listAgentCatalog, getDefaultAgentId, liveAgentId, } from './registry.ts';
|
|
6
8
|
export { attachmentMaterializer } from './attachments.ts';
|
|
7
9
|
export { makeAttachmentCapabilities } from './agent-capabilities.ts';
|
|
8
10
|
export { makeSystemCapabilities } from './system-capabilities.ts';
|
|
9
|
-
export { makeSuggestionCapabilities } from './suggestion-capabilities.ts';
|
|
10
|
-
export type { SuggestionCapability } from './suggestion-capabilities.ts';
|
|
11
11
|
export { systemTimeZone } from './environment.ts';
|
|
12
|
+
export type { ContextFact } from './context-facts.ts';
|
|
13
|
+
export { CONNECTOR_FACT_NAME } from './context-facts.ts';
|
|
12
14
|
export { inspectUpload, isAgentToolContentResult } from './file-inspection.ts';
|
|
13
|
-
export type {
|
|
15
|
+
export type { PipelineOptions, RunAgentFn } from './pipeline.ts';
|
|
14
16
|
export { buildPolicy, loadGatePolicy, loadAgentId } from './config.ts';
|
|
15
17
|
export { getConversationStarters, refreshSystemStarters } from './starters.ts';
|
|
16
|
-
export {
|
|
17
|
-
export {
|
|
18
|
-
export
|
|
19
|
-
export {
|
|
18
|
+
export { mayRead, mayWrite, mayBePrivate } from './conversation-access.ts';
|
|
19
|
+
export type { ConversationAccess } from './conversation-access.ts';
|
|
20
|
+
export { makeLiveSink, plainRender } from './live-message.ts';
|
|
21
|
+
export type { LiveChannelOps, LiveSinkOpts, RenderFn } from './live-message.ts';
|
|
22
|
+
export { makeDraftSink } from './draft-message.ts';
|
|
23
|
+
export type { DraftChannelOps, DraftSinkOpts } from './draft-message.ts';
|
|
20
24
|
export { DEFAULT_TASK_TIMEOUT_MS } from '../background-scheduler.ts';
|
|
21
|
-
export type { Connector,
|
|
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
|
+
export type { AuthRole } from '../plugin-hooks.ts';
|
|
22
27
|
export declare function startOrchestrator(): void;
|
|
23
28
|
export declare function stopOrchestrator(): void;
|
|
24
29
|
export declare const orchestratorStartersTask: BackgroundTask;
|
|
@@ -6,20 +6,20 @@ 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
|
-
|
|
9
|
+
import { handleIncoming as handleIncomingImpl } from "./pipeline.js";
|
|
10
|
+
export const handleIncoming = handleIncomingImpl;
|
|
10
11
|
export { registerAgent, resolveAgent, registerConnector, isConnectorRegistered, clearRuntimeRegistries, listRegisteredAgents, listRegisteredConnectors, listAgentCatalog, getDefaultAgentId, liveAgentId, } from "./registry.js";
|
|
11
12
|
export { attachmentMaterializer } from "./attachments.js";
|
|
12
13
|
export { makeAttachmentCapabilities } from "./agent-capabilities.js";
|
|
13
14
|
export { makeSystemCapabilities } from "./system-capabilities.js";
|
|
14
|
-
export { makeSuggestionCapabilities } from "./suggestion-capabilities.js";
|
|
15
15
|
export { systemTimeZone } from "./environment.js";
|
|
16
|
+
export { CONNECTOR_FACT_NAME } from "./context-facts.js";
|
|
16
17
|
export { inspectUpload, isAgentToolContentResult } from "./file-inspection.js";
|
|
17
18
|
export { buildPolicy, loadGatePolicy, loadAgentId } from "./config.js";
|
|
18
19
|
export { getConversationStarters, refreshSystemStarters } from "./starters.js";
|
|
19
|
-
export {
|
|
20
|
-
|
|
21
|
-
export {
|
|
22
|
-
export { chunk } from "./format.js";
|
|
20
|
+
export { mayRead, mayWrite, mayBePrivate } from "./conversation-access.js";
|
|
21
|
+
export { makeLiveSink, plainRender } from "./live-message.js";
|
|
22
|
+
export { makeDraftSink } from "./draft-message.js";
|
|
23
23
|
export { DEFAULT_TASK_TIMEOUT_MS } from "../background-scheduler.js";
|
|
24
24
|
let db;
|
|
25
25
|
const STARTERS_CHECK_INTERVAL_MS = 10 * 60_000;
|
|
@@ -27,7 +27,6 @@ function runStartersRefresh() {
|
|
|
27
27
|
return refreshSystemStarters().catch((err) => log.warn(`system starters: ${String(err)}`));
|
|
28
28
|
}
|
|
29
29
|
export function startOrchestrator() {
|
|
30
|
-
carryLegacyState();
|
|
31
30
|
db = openLogDb();
|
|
32
31
|
setLogDb(db);
|
|
33
32
|
void runStartersRefresh();
|
|
@@ -1,14 +1,17 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { TurnSink } from './types.ts';
|
|
2
2
|
export interface LiveChannelOps {
|
|
3
3
|
send(text: string): Promise<string | null>;
|
|
4
4
|
edit(msgId: string, text: string): Promise<void>;
|
|
5
5
|
}
|
|
6
|
-
export type RenderFn = (r:
|
|
7
|
-
|
|
6
|
+
export type RenderFn = (r: {
|
|
7
|
+
text: string | null;
|
|
8
|
+
reasoning: string | null;
|
|
9
|
+
}) => string[];
|
|
10
|
+
export interface LiveSinkOpts {
|
|
8
11
|
ops: LiveChannelOps;
|
|
9
12
|
throttleMs: number;
|
|
10
13
|
render: RenderFn;
|
|
11
14
|
maxLength: number;
|
|
12
15
|
}
|
|
13
16
|
export declare function plainRender(max: number): RenderFn;
|
|
14
|
-
export declare function
|
|
17
|
+
export declare function makeLiveSink(o: LiveSinkOpts): TurnSink;
|
|
@@ -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
|
}
|
|
@@ -13,7 +18,7 @@ async function swallow(fn, fallback, label) {
|
|
|
13
18
|
return fallback;
|
|
14
19
|
}
|
|
15
20
|
}
|
|
16
|
-
export function
|
|
21
|
+
export function makeLiveSink(o) {
|
|
17
22
|
let msgId = null;
|
|
18
23
|
let pending = '';
|
|
19
24
|
let lastSent = '';
|
|
@@ -62,39 +67,54 @@ export function makeLiveChannel(o) {
|
|
|
62
67
|
}, o.throttleMs);
|
|
63
68
|
if (typeof timer.unref === 'function')
|
|
64
69
|
timer.unref();
|
|
70
|
+
async function deliver(r) {
|
|
71
|
+
closed = true;
|
|
72
|
+
clearInterval(timer);
|
|
73
|
+
await inFlight;
|
|
74
|
+
const parts = o.render(r);
|
|
75
|
+
const [head, ...rest] = parts;
|
|
76
|
+
if (msgId && head !== undefined) {
|
|
77
|
+
const id = msgId;
|
|
78
|
+
if (head !== lastSent) {
|
|
79
|
+
await swallow(() => o.ops.edit(id, head), undefined, 'edit(final)');
|
|
80
|
+
lastSent = head;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
else if (head !== undefined) {
|
|
84
|
+
await swallow(() => o.ops.send(head), null, 'send(final)');
|
|
85
|
+
}
|
|
86
|
+
for (const p of rest) {
|
|
87
|
+
await swallow(() => o.ops.send(p), null, 'send(overflow)');
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
let finished = Promise.resolve();
|
|
91
|
+
let terminalDelivered = false;
|
|
92
|
+
function deliverTerminal(kind, r) {
|
|
93
|
+
if (terminalDelivered) {
|
|
94
|
+
log.warn(`live-message: a second terminal event (${kind}) after the turn already finished — dropped`);
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
terminalDelivered = true;
|
|
98
|
+
finished = deliver(r);
|
|
99
|
+
}
|
|
65
100
|
return {
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
101
|
+
emit(e) {
|
|
102
|
+
if (e.kind === 'delta')
|
|
103
|
+
pending = e.text;
|
|
104
|
+
else if (e.kind === 'segment') {
|
|
105
|
+
gen++;
|
|
106
|
+
msgId = null;
|
|
107
|
+
pending = '';
|
|
108
|
+
lastSent = '';
|
|
109
|
+
}
|
|
110
|
+
else if (e.kind === 'answer')
|
|
111
|
+
deliverTerminal('answer', { text: e.text, reasoning: e.reasoning });
|
|
112
|
+
else if (e.kind === 'error')
|
|
113
|
+
deliverTerminal('error', { text: null, reasoning: null });
|
|
74
114
|
},
|
|
75
|
-
|
|
76
|
-
closed = true;
|
|
115
|
+
done: () => {
|
|
77
116
|
clearInterval(timer);
|
|
78
|
-
|
|
79
|
-
const parts = o.render(r);
|
|
80
|
-
let last = null;
|
|
81
|
-
const [head, ...rest] = parts;
|
|
82
|
-
if (msgId && head !== undefined) {
|
|
83
|
-
const id = msgId;
|
|
84
|
-
if (head !== lastSent) {
|
|
85
|
-
await swallow(() => o.ops.edit(id, head), undefined, 'edit(final)');
|
|
86
|
-
lastSent = head;
|
|
87
|
-
}
|
|
88
|
-
last = id;
|
|
89
|
-
}
|
|
90
|
-
else if (head !== undefined) {
|
|
91
|
-
last = await swallow(() => o.ops.send(head), null, 'send(final)');
|
|
92
|
-
}
|
|
93
|
-
for (const p of rest) {
|
|
94
|
-
const id = await swallow(() => o.ops.send(p), null, 'send(overflow)');
|
|
95
|
-
last = id ?? last;
|
|
96
|
-
}
|
|
97
|
-
return last;
|
|
117
|
+
return finished;
|
|
98
118
|
},
|
|
99
119
|
};
|
|
100
120
|
}
|
|
@@ -1,11 +1,29 @@
|
|
|
1
|
-
import type { Connector,
|
|
1
|
+
import type { AgentMediaLimits, Connector, ConvMessage, TurnRequest, GatePolicy, AgentRuntime } from './types.ts';
|
|
2
2
|
import type { LogDb } from './db.ts';
|
|
3
|
+
import { type AreaWorld, type SystemAreas } from './system-areas.ts';
|
|
4
|
+
import type { AuthUser } from '../auth-store.ts';
|
|
5
|
+
import { type ContextFact } from './context-facts.ts';
|
|
6
|
+
import { type StoredMsg } from '../conversation-store.ts';
|
|
3
7
|
export declare function setLogDb(db: LogDb | undefined): void;
|
|
4
8
|
export type RunAgentFn = AgentRuntime['run'];
|
|
5
|
-
export
|
|
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[];
|
|
13
|
+
export type UserLookup = (id: number) => Promise<AuthUser | null>;
|
|
14
|
+
export interface PipelineOptions {
|
|
15
|
+
policy?: GatePolicy;
|
|
16
|
+
}
|
|
17
|
+
export interface PipelineDeps extends PipelineOptions {
|
|
6
18
|
runAgent?: RunAgentFn;
|
|
19
|
+
afterword?: AgentRuntime['afterword'];
|
|
7
20
|
logDb?: LogDb | null;
|
|
8
|
-
policy?: GatePolicy;
|
|
9
21
|
agentBase?: () => Promise<string>;
|
|
22
|
+
media?: AgentMediaLimits;
|
|
23
|
+
lookupUser?: (id: number) => Promise<AuthUser | null>;
|
|
24
|
+
now?: () => Date;
|
|
25
|
+
timeZone?: string;
|
|
26
|
+
world?: AreaWorld;
|
|
27
|
+
domainAreas?: () => Promise<SystemAreas>;
|
|
10
28
|
}
|
|
11
|
-
export declare function handleIncoming(connector: Connector,
|
|
29
|
+
export declare function handleIncoming(connector: Connector, turn: TurnRequest, deps?: PipelineDeps): Promise<void>;
|