@coffer-org/server 7.2.0 → 7.3.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 +53 -0
- package/dist/auth-store.d.ts +2 -0
- package/dist/auth-store.js +1 -0
- package/dist/entity-schema.d.ts +2 -0
- package/dist/entity-schema.js +26 -0
- package/dist/identity-link.d.ts +15 -0
- package/dist/identity-link.js +76 -0
- package/dist/index.js +8 -1
- 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/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/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 +15 -9
- package/dist/orchestrator/index.js +13 -7
- package/dist/orchestrator/live-message.d.ts +7 -4
- package/dist/orchestrator/live-message.js +48 -31
- package/dist/orchestrator/pipeline.d.ts +25 -4
- package/dist/orchestrator/pipeline.js +214 -94
- package/dist/orchestrator/registry.d.ts +4 -2
- package/dist/orchestrator/registry.js +10 -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 +101 -44
- package/dist/plugin-hooks.d.ts +26 -0
- package/dist/plugin-http-mounts.d.ts +18 -0
- package/dist/plugin-http-mounts.js +94 -0
- package/dist/plugin-runtime.js +2 -2
- package/dist/records-api.js +15 -3
- package/dist/system-settings.js +0 -1
- 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,103 @@
|
|
|
1
|
+
import sharp from 'sharp';
|
|
2
|
+
export class ImageNormalizeError extends Error {
|
|
3
|
+
}
|
|
4
|
+
const MIME_BY_FORMAT = {
|
|
5
|
+
jpeg: 'image/jpeg',
|
|
6
|
+
jpg: 'image/jpeg',
|
|
7
|
+
png: 'image/png',
|
|
8
|
+
webp: 'image/webp',
|
|
9
|
+
gif: 'image/gif',
|
|
10
|
+
avif: 'image/avif',
|
|
11
|
+
tiff: 'image/tiff',
|
|
12
|
+
heif: 'image/heif',
|
|
13
|
+
};
|
|
14
|
+
const QUALITY_LADDER = [85, 72, 58, 44];
|
|
15
|
+
const MAX_ATTEMPTS = 6;
|
|
16
|
+
const QUALITY_CAN_CLOSE = 2;
|
|
17
|
+
function fit(width, height, maxEdge, maxPixels) {
|
|
18
|
+
let scale = Math.min(1, maxEdge / Math.max(width, height));
|
|
19
|
+
if (maxPixels)
|
|
20
|
+
scale = Math.min(scale, Math.sqrt(maxPixels / (width * height)));
|
|
21
|
+
return scaleTo(width, height, scale);
|
|
22
|
+
}
|
|
23
|
+
function scaleTo(width, height, scale) {
|
|
24
|
+
return {
|
|
25
|
+
width: Math.max(1, Math.floor(width * scale)),
|
|
26
|
+
height: Math.max(1, Math.floor(height * scale)),
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
function pickEncoding(target, hasAlpha) {
|
|
30
|
+
const accepts = (mime) => target.encode.includes(mime);
|
|
31
|
+
const order = hasAlpha ? ['image/webp', 'image/png', 'image/jpeg'] : ['image/jpeg', 'image/webp', 'image/png'];
|
|
32
|
+
for (const mime of order)
|
|
33
|
+
if (accepts(mime))
|
|
34
|
+
return mime;
|
|
35
|
+
throw new ImageNormalizeError(`the agent takes back none of the encodings this image could be produced in: ${target.encode.join(', ') || '(none declared)'}`);
|
|
36
|
+
}
|
|
37
|
+
function encode(pipeline, mime, quality) {
|
|
38
|
+
switch (mime) {
|
|
39
|
+
case 'image/jpeg':
|
|
40
|
+
return pipeline.jpeg({ quality, mozjpeg: true }).toBuffer();
|
|
41
|
+
case 'image/webp':
|
|
42
|
+
return pipeline.webp({ quality }).toBuffer();
|
|
43
|
+
case 'image/png':
|
|
44
|
+
return pipeline.png({ compressionLevel: 9, palette: true, quality }).toBuffer();
|
|
45
|
+
default:
|
|
46
|
+
throw new ImageNormalizeError(`Unsupported target encoding: ${mime}`);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
export async function normalizeImage(bytes, target) {
|
|
50
|
+
let meta;
|
|
51
|
+
try {
|
|
52
|
+
meta = await sharp(bytes).metadata();
|
|
53
|
+
}
|
|
54
|
+
catch (err) {
|
|
55
|
+
throw new ImageNormalizeError(`Not a decodable image: ${err instanceof Error ? err.message : String(err)}`);
|
|
56
|
+
}
|
|
57
|
+
const format = meta.format ? MIME_BY_FORMAT[meta.format] : undefined;
|
|
58
|
+
const swap = (meta.orientation ?? 0) >= 5;
|
|
59
|
+
const width = swap ? meta.height : meta.width;
|
|
60
|
+
const height = swap ? meta.width : meta.height;
|
|
61
|
+
if (!format || !width || !height)
|
|
62
|
+
throw new ImageNormalizeError('Image has no readable format or dimensions.');
|
|
63
|
+
const source = { mime: format, width, height, bytes: bytes.length };
|
|
64
|
+
const target1 = fit(width, height, target.maxEdge, target.maxPixels);
|
|
65
|
+
const fitsNow = target.encode.includes(format) &&
|
|
66
|
+
bytes.length <= target.maxBytes &&
|
|
67
|
+
target1.width === width &&
|
|
68
|
+
target1.height === height;
|
|
69
|
+
if (fitsNow)
|
|
70
|
+
return { bytes, mime: format, width, height, source, changed: false };
|
|
71
|
+
const mime = pickEncoding(target, meta.hasAlpha === true);
|
|
72
|
+
const flatten = meta.hasAlpha === true && mime === 'image/jpeg';
|
|
73
|
+
let dims = target1;
|
|
74
|
+
let qualityStep = 0;
|
|
75
|
+
let last;
|
|
76
|
+
for (let attempt = 0; attempt < MAX_ATTEMPTS; attempt++) {
|
|
77
|
+
let pipeline = sharp(bytes).rotate();
|
|
78
|
+
if (flatten)
|
|
79
|
+
pipeline = pipeline.flatten({ background: '#ffffff' });
|
|
80
|
+
pipeline = pipeline.resize({ ...dims, fit: 'inside', withoutEnlargement: true });
|
|
81
|
+
const quality = QUALITY_LADDER[Math.min(qualityStep, QUALITY_LADDER.length - 1)];
|
|
82
|
+
last = await encode(pipeline, mime, quality);
|
|
83
|
+
if (last.length <= target.maxBytes) {
|
|
84
|
+
const out = await sharp(last).metadata();
|
|
85
|
+
return {
|
|
86
|
+
bytes: last,
|
|
87
|
+
mime,
|
|
88
|
+
width: out.width ?? dims.width,
|
|
89
|
+
height: out.height ?? dims.height,
|
|
90
|
+
source,
|
|
91
|
+
changed: true,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
if (last.length <= target.maxBytes * QUALITY_CAN_CLOSE && qualityStep < QUALITY_LADDER.length - 1) {
|
|
95
|
+
qualityStep += 1;
|
|
96
|
+
}
|
|
97
|
+
else {
|
|
98
|
+
const shrink = Math.sqrt(target.maxBytes / last.length) * 0.9;
|
|
99
|
+
dims = scaleTo(dims.width, dims.height, Math.min(0.9, shrink));
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
throw new ImageNormalizeError(`Could not bring the image within ${target.maxBytes} bytes: ${last?.length ?? source.bytes} bytes after ${MAX_ATTEMPTS} attempts.`);
|
|
103
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { normalizeImage, ImageNormalizeError, type ImageTarget, type ImageFacts, type NormalizedImage, } from './image.ts';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { normalizeImage, ImageNormalizeError, } from "./image.js";
|
package/dist/migrations.js
CHANGED
|
@@ -394,7 +394,7 @@ export function makeTable(em, table, record) {
|
|
|
394
394
|
export async function renameSystemShelfKey(em) {
|
|
395
395
|
await makeTable(em, '_embeddings').renameColumn('type', 'shelf_key');
|
|
396
396
|
}
|
|
397
|
-
const LEGACY_ORCHESTRATOR_FIELDS = ['agent_id', '
|
|
397
|
+
const LEGACY_ORCHESTRATOR_FIELDS = ['agent_id', 'trigger_prefix', 'reply_window'];
|
|
398
398
|
export const SYSTEM_MIGRATIONS = [
|
|
399
399
|
{
|
|
400
400
|
name: 'embeddings-shelf-key',
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import type { AgentToolProvider, AttachmentRef } from './types.ts';
|
|
2
|
-
export declare function makeAttachmentCapabilities(attachments: AttachmentRef[]): Promise<AgentToolProvider>;
|
|
1
|
+
import type { AgentMediaLimits, AgentToolProvider, AttachmentRef } from './types.ts';
|
|
2
|
+
export declare function makeAttachmentCapabilities(attachments: AttachmentRef[], limits: AgentMediaLimits): Promise<AgentToolProvider>;
|
|
@@ -57,8 +57,8 @@ function fileItems(value) {
|
|
|
57
57
|
function findTool(defs, name) {
|
|
58
58
|
return defs.find((def) => def.server === 'coffer' && def.bareName === name);
|
|
59
59
|
}
|
|
60
|
-
export async function makeAttachmentCapabilities(attachments) {
|
|
61
|
-
const defs = await collectMcpTools();
|
|
60
|
+
export async function makeAttachmentCapabilities(attachments, limits) {
|
|
61
|
+
const defs = await collectMcpTools({ role: 'member' });
|
|
62
62
|
const getRecord = findTool(defs, 'get_record');
|
|
63
63
|
const updateRecord = findTool(defs, 'update_record');
|
|
64
64
|
const tools = [];
|
|
@@ -115,7 +115,7 @@ export async function makeAttachmentCapabilities(attachments) {
|
|
|
115
115
|
return inspectUpload(item['name'], {
|
|
116
116
|
...(typeof item['mime'] === 'string' ? { mime: item['mime'] } : {}),
|
|
117
117
|
...(typeof item['size'] === 'number' ? { size: item['size'] } : {}),
|
|
118
|
-
});
|
|
118
|
+
}, limits);
|
|
119
119
|
},
|
|
120
120
|
});
|
|
121
121
|
}
|
|
@@ -1,16 +1 @@
|
|
|
1
|
-
|
|
2
|
-
export declare const stateDir: () => string;
|
|
3
|
-
export declare function carryLegacyState(): void;
|
|
4
|
-
export declare function allowFileFor(connectorId: string): string;
|
|
5
|
-
export interface AllowState {
|
|
6
|
-
ids: Record<string, number>;
|
|
7
|
-
}
|
|
8
|
-
export declare function emptyAllowed(): AllowState;
|
|
9
|
-
export declare function loadAllowed(file: string): AllowState;
|
|
10
|
-
export declare function saveAllowed(file: string, state: AllowState): void;
|
|
11
|
-
export declare function isAllowed(state: AllowState, id: string | number | null | undefined): boolean;
|
|
12
|
-
export declare function addAllowed(state: AllowState, id: string | number, now: number): AllowState;
|
|
13
|
-
export declare function isSenderAllowed(connectorId: string, senderId: string, deps?: {
|
|
14
|
-
policy?: GatePolicy;
|
|
15
|
-
}): Promise<boolean>;
|
|
16
|
-
export declare function makeThrottle(maxAttempts?: number, windowMs?: number): (id: string | number, now?: number) => boolean;
|
|
1
|
+
export declare function makeThrottle(maxNotices?: number, windowMs?: number): (id: string | number, now?: number) => boolean;
|
|
@@ -1,63 +1,13 @@
|
|
|
1
|
-
|
|
2
|
-
import path from 'node:path';
|
|
3
|
-
import { loadGatePolicy } from "./config.js";
|
|
4
|
-
import { carryInto, dataDir } from "../data-dir.js";
|
|
5
|
-
export const stateDir = () => process.env['ORCHESTRATOR_STATE_DIR'] ?? path.join(dataDir(), 'state');
|
|
6
|
-
function legacyStateDirs() {
|
|
7
|
-
return [
|
|
8
|
-
path.join(process.cwd(), 'packages', 'plugin-orchestrator', 'runtime', 'state'),
|
|
9
|
-
path.join(process.cwd(), 'node_modules', '@coffer-org', 'plugin-orchestrator', 'runtime', 'state'),
|
|
10
|
-
];
|
|
11
|
-
}
|
|
12
|
-
export function carryLegacyState() {
|
|
13
|
-
carryInto(stateDir(), ...legacyStateDirs());
|
|
14
|
-
}
|
|
15
|
-
export function allowFileFor(connectorId) {
|
|
16
|
-
return path.join(stateDir(), `${connectorId}.allowed.json`);
|
|
17
|
-
}
|
|
18
|
-
export function emptyAllowed() {
|
|
19
|
-
return { ids: {} };
|
|
20
|
-
}
|
|
21
|
-
export function loadAllowed(file) {
|
|
22
|
-
try {
|
|
23
|
-
const obj = JSON.parse(fs.readFileSync(file, 'utf-8'));
|
|
24
|
-
if (obj && typeof obj === 'object' && 'ids' in obj && typeof obj.ids === 'object') {
|
|
25
|
-
return obj;
|
|
26
|
-
}
|
|
27
|
-
return emptyAllowed();
|
|
28
|
-
}
|
|
29
|
-
catch {
|
|
30
|
-
return emptyAllowed();
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
export function saveAllowed(file, state) {
|
|
34
|
-
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
35
|
-
const tmp = `${file}.${process.pid}.tmp`;
|
|
36
|
-
fs.writeFileSync(tmp, JSON.stringify(state), 'utf-8');
|
|
37
|
-
fs.renameSync(tmp, file);
|
|
38
|
-
}
|
|
39
|
-
export function isAllowed(state, id) {
|
|
40
|
-
return id != null && Object.prototype.hasOwnProperty.call(state.ids, String(id));
|
|
41
|
-
}
|
|
42
|
-
export function addAllowed(state, id, now) {
|
|
43
|
-
return { ids: { ...state.ids, [String(id)]: now } };
|
|
44
|
-
}
|
|
45
|
-
export async function isSenderAllowed(connectorId, senderId, deps) {
|
|
46
|
-
const policy = deps?.policy ?? (await loadGatePolicy());
|
|
47
|
-
if (!policy.accessPassword)
|
|
48
|
-
return true;
|
|
49
|
-
return isAllowed(loadAllowed(allowFileFor(connectorId)), senderId);
|
|
50
|
-
}
|
|
51
|
-
export function makeThrottle(maxAttempts = 5, windowMs = 60_000) {
|
|
1
|
+
export function makeThrottle(maxNotices = 5, windowMs = 60_000) {
|
|
52
2
|
const hits = new Map();
|
|
53
|
-
return function
|
|
3
|
+
return function allowNotice(id, now = Date.now()) {
|
|
54
4
|
const key = String(id);
|
|
55
5
|
const rec = hits.get(key);
|
|
56
6
|
if (!rec || now - rec.start >= windowMs) {
|
|
57
7
|
hits.set(key, { start: now, count: 1 });
|
|
58
8
|
return true;
|
|
59
9
|
}
|
|
60
|
-
if (rec.count >=
|
|
10
|
+
if (rec.count >= maxNotices)
|
|
61
11
|
return false;
|
|
62
12
|
rec.count += 1;
|
|
63
13
|
return true;
|
|
@@ -3,7 +3,6 @@ export function buildPolicy(dbSettings = {}) {
|
|
|
3
3
|
const db = dbSettings;
|
|
4
4
|
return {
|
|
5
5
|
...(typeof db.agent_id === 'string' && db.agent_id ? { agentId: db.agent_id } : {}),
|
|
6
|
-
accessPassword: db.access_password ?? '',
|
|
7
6
|
triggerPrefix: db.trigger_prefix ?? '',
|
|
8
7
|
replyWindow: Number(db.reply_window ?? 1800) || 1800,
|
|
9
8
|
};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { AuthRole } from '../plugin-hooks.ts';
|
|
2
|
+
export interface ContextFact {
|
|
3
|
+
name: string;
|
|
4
|
+
value: string;
|
|
5
|
+
attrs?: Record<string, string>;
|
|
6
|
+
}
|
|
7
|
+
export declare const CONNECTOR_FACT_NAME: RegExp;
|
|
8
|
+
export declare const CONTEXT_GAP_MS: number;
|
|
9
|
+
export declare const MAX_SPEAKER_NAME = 80;
|
|
10
|
+
export declare const MAX_FACT_VALUE = 200;
|
|
11
|
+
export declare const MAX_CONNECTOR_FACTS = 12;
|
|
12
|
+
export declare function oneLine(value: string, maxLen?: number): string;
|
|
13
|
+
export interface ContextSpeaker {
|
|
14
|
+
id: string;
|
|
15
|
+
name: string;
|
|
16
|
+
role: AuthRole;
|
|
17
|
+
}
|
|
18
|
+
export interface ContextInput {
|
|
19
|
+
connectorFacts: readonly ContextFact[];
|
|
20
|
+
speaker: ContextSpeaker;
|
|
21
|
+
now: Date;
|
|
22
|
+
timeZone: string;
|
|
23
|
+
previous: readonly ContextFact[] | null;
|
|
24
|
+
previousAt: Date | null;
|
|
25
|
+
}
|
|
26
|
+
export declare function humanizeGap(ms: number): string;
|
|
27
|
+
export declare function buildContextFacts(input: ContextInput): ContextFact[];
|
|
@@ -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
|
+
}
|
|
@@ -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,27 +1,33 @@
|
|
|
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
|
-
|
|
5
|
-
|
|
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>;
|
|
7
|
+
export { registerAgent, resolveAgent, registerConnector, isConnectorRegistered, clearRuntimeRegistries, listRegisteredAgents, listRegisteredConnectors, listLinkableConnectors, connectorLinkUrl, 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 type {
|
|
18
|
+
export { resolveSpeaker } from './pipeline.ts';
|
|
19
|
+
export { mayRead, mayWrite, mayBePrivate } from './conversation-access.ts';
|
|
20
|
+
export type { ConversationAccess } from './conversation-access.ts';
|
|
21
|
+
export { makeLiveSink, plainRender } from './live-message.ts';
|
|
22
|
+
export type { LiveChannelOps, LiveSinkOpts, RenderFn } from './live-message.ts';
|
|
19
23
|
export { chunk } from './format.ts';
|
|
20
24
|
export { DEFAULT_TASK_TIMEOUT_MS } from '../background-scheduler.ts';
|
|
21
|
-
export type { Connector,
|
|
25
|
+
export type { Connector, TurnEnvelope, ConnectorCapabilities, TurnBody, TurnRequest, SenderIdKind, GatePolicy, TurnEvent, TurnSink, AttachmentRef, AttachmentMaterializer, AgentToolDefinition, AgentToolProvider, ConvMessage, AgentTurn, AgentRuntime, AgentCapabilities, AgentMediaKind, AgentMediaLimits, AgentPreset, AgentDescriptor, AgentCatalogEntry, ConnectorRegistration, AgentToolContentBlock, AgentToolContentResult, } from './types.ts';
|
|
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;
|
|
30
|
+
export declare const linkCodePruneTask: BackgroundTask;
|
|
25
31
|
export declare function orchestratorDiagnostics(): Promise<{
|
|
26
32
|
generatedAt: string;
|
|
27
33
|
agents: string[];
|
|
@@ -6,19 +6,21 @@ 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
|
-
|
|
10
|
-
|
|
9
|
+
import { pruneLinkCodes } from "../identity-link.js";
|
|
10
|
+
import { handleIncoming as handleIncomingImpl } from "./pipeline.js";
|
|
11
|
+
export const handleIncoming = handleIncomingImpl;
|
|
12
|
+
export { registerAgent, resolveAgent, registerConnector, isConnectorRegistered, clearRuntimeRegistries, listRegisteredAgents, listRegisteredConnectors, listLinkableConnectors, connectorLinkUrl, listAgentCatalog, getDefaultAgentId, liveAgentId, } from "./registry.js";
|
|
11
13
|
export { attachmentMaterializer } from "./attachments.js";
|
|
12
14
|
export { makeAttachmentCapabilities } from "./agent-capabilities.js";
|
|
13
15
|
export { makeSystemCapabilities } from "./system-capabilities.js";
|
|
14
|
-
export { makeSuggestionCapabilities } from "./suggestion-capabilities.js";
|
|
15
16
|
export { systemTimeZone } from "./environment.js";
|
|
17
|
+
export { CONNECTOR_FACT_NAME } from "./context-facts.js";
|
|
16
18
|
export { inspectUpload, isAgentToolContentResult } from "./file-inspection.js";
|
|
17
19
|
export { buildPolicy, loadGatePolicy, loadAgentId } from "./config.js";
|
|
18
20
|
export { getConversationStarters, refreshSystemStarters } from "./starters.js";
|
|
19
|
-
export {
|
|
20
|
-
|
|
21
|
-
export {
|
|
21
|
+
export { resolveSpeaker } from "./pipeline.js";
|
|
22
|
+
export { mayRead, mayWrite, mayBePrivate } from "./conversation-access.js";
|
|
23
|
+
export { makeLiveSink, plainRender } from "./live-message.js";
|
|
22
24
|
export { chunk } from "./format.js";
|
|
23
25
|
export { DEFAULT_TASK_TIMEOUT_MS } from "../background-scheduler.js";
|
|
24
26
|
let db;
|
|
@@ -27,7 +29,6 @@ function runStartersRefresh() {
|
|
|
27
29
|
return refreshSystemStarters().catch((err) => log.warn(`system starters: ${String(err)}`));
|
|
28
30
|
}
|
|
29
31
|
export function startOrchestrator() {
|
|
30
|
-
carryLegacyState();
|
|
31
32
|
db = openLogDb();
|
|
32
33
|
setLogDb(db);
|
|
33
34
|
void runStartersRefresh();
|
|
@@ -46,6 +47,11 @@ export const orchestratorStartersTask = {
|
|
|
46
47
|
intervalMs: STARTERS_CHECK_INTERVAL_MS,
|
|
47
48
|
run: runStartersRefresh,
|
|
48
49
|
};
|
|
50
|
+
export const linkCodePruneTask = {
|
|
51
|
+
name: 'core:link-code-prune',
|
|
52
|
+
intervalMs: 86_400_000,
|
|
53
|
+
run: pruneLinkCodes,
|
|
54
|
+
};
|
|
49
55
|
export async function orchestratorDiagnostics() {
|
|
50
56
|
return {
|
|
51
57
|
generatedAt: new Date().toISOString(),
|
|
@@ -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;
|