@coffer-org/server 7.1.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/background-scheduler.d.ts +1 -0
- package/dist/background-scheduler.js +2 -1
- 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-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-http.js +7 -3
- package/dist/mcp-tools.d.ts +7 -5
- package/dist/mcp-tools.js +95 -91
- 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 +16 -9
- package/dist/orchestrator/index.js +14 -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 +106 -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
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
import type { ImageTarget } from '../media/index.ts';
|
|
2
|
+
import type { AuthRole } from '../plugin-hooks.ts';
|
|
3
|
+
import type { ContextFact } from './context-facts.ts';
|
|
4
|
+
import type { SystemAreas } from './system-areas.ts';
|
|
5
|
+
export type { ImageTarget };
|
|
6
|
+
export type { ContextFact };
|
|
7
|
+
export type { SystemAreas };
|
|
1
8
|
export interface AttachmentRef {
|
|
2
9
|
name: string;
|
|
3
10
|
mime?: string;
|
|
@@ -9,7 +16,6 @@ export interface AgentToolDefinition {
|
|
|
9
16
|
description: string;
|
|
10
17
|
inputSchema: Record<string, unknown>;
|
|
11
18
|
handler: (args: Record<string, unknown>, signal?: AbortSignal) => Promise<unknown>;
|
|
12
|
-
forcedAfterAnswer?: boolean;
|
|
13
19
|
}
|
|
14
20
|
export interface AgentToolProvider {
|
|
15
21
|
tools: AgentToolDefinition[];
|
|
@@ -34,18 +40,25 @@ export interface AgentToolContentResult {
|
|
|
34
40
|
export interface ConvMessage {
|
|
35
41
|
role: 'user' | 'assistant';
|
|
36
42
|
content: string;
|
|
37
|
-
context?:
|
|
43
|
+
context?: ContextFact[];
|
|
38
44
|
attachments?: AttachmentRef[];
|
|
39
45
|
sender?: string | null;
|
|
40
46
|
msgId: string;
|
|
41
47
|
ts: number;
|
|
42
48
|
}
|
|
43
49
|
export interface AgentCapabilities {
|
|
44
|
-
vision?: boolean;
|
|
45
|
-
documents?: boolean;
|
|
46
50
|
tools?: boolean;
|
|
47
51
|
reasoning?: boolean;
|
|
48
52
|
}
|
|
53
|
+
export interface AgentMediaKind {
|
|
54
|
+
maxBytes: number;
|
|
55
|
+
accepts: readonly string[];
|
|
56
|
+
}
|
|
57
|
+
export interface AgentMediaLimits {
|
|
58
|
+
image: AgentMediaKind & ImageTarget;
|
|
59
|
+
document: AgentMediaKind;
|
|
60
|
+
text: AgentMediaKind;
|
|
61
|
+
}
|
|
49
62
|
export interface AgentPreset {
|
|
50
63
|
id: string;
|
|
51
64
|
title: string;
|
|
@@ -60,33 +73,46 @@ export interface AgentDescriptor {
|
|
|
60
73
|
title: string;
|
|
61
74
|
presets: AgentPreset[];
|
|
62
75
|
}
|
|
63
|
-
export interface
|
|
64
|
-
|
|
65
|
-
|
|
76
|
+
export interface AgentCatalogEntry extends AgentDescriptor {
|
|
77
|
+
media: AgentMediaLimits;
|
|
78
|
+
}
|
|
79
|
+
export interface AgentBudget {
|
|
80
|
+
responseTimeout?: number;
|
|
81
|
+
toolRounds?: number;
|
|
82
|
+
}
|
|
83
|
+
export interface AgentTurn {
|
|
84
|
+
envelope: TurnEnvelope;
|
|
85
|
+
body: {
|
|
86
|
+
system: string[];
|
|
87
|
+
messages: ConvMessage[];
|
|
88
|
+
userTurns: number;
|
|
89
|
+
};
|
|
66
90
|
toolProvider?: AgentToolProvider;
|
|
67
91
|
presetId?: string;
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
}
|
|
72
|
-
export interface AgentResult {
|
|
73
|
-
text: string | null;
|
|
74
|
-
reasoning: string | null;
|
|
75
|
-
tokensIn: number | null;
|
|
76
|
-
tokensOut: number | null;
|
|
77
|
-
stopReason: string | null;
|
|
78
|
-
presetId: string | null;
|
|
79
|
-
suggestions?: string[] | null;
|
|
92
|
+
budget?: AgentBudget;
|
|
93
|
+
senderRole?: AuthRole;
|
|
94
|
+
signal?: AbortSignal;
|
|
80
95
|
}
|
|
81
96
|
export interface AgentRuntime {
|
|
82
97
|
id: string;
|
|
83
|
-
|
|
98
|
+
media: AgentMediaLimits;
|
|
99
|
+
run(turn: AgentTurn, emit: (e: TurnEvent) => void): Promise<void>;
|
|
84
100
|
systemBase(): Promise<string>;
|
|
85
101
|
describe(): Promise<AgentDescriptor>;
|
|
86
102
|
starters?(hint: string): Promise<string[]>;
|
|
103
|
+
afterword?(turn: AgentTurn, answer: string, want: {
|
|
104
|
+
suggestions: boolean;
|
|
105
|
+
title: boolean;
|
|
106
|
+
}): Promise<{
|
|
107
|
+
suggestions?: string[];
|
|
108
|
+
title?: string;
|
|
109
|
+
tokensIn?: number;
|
|
110
|
+
tokensOut?: number;
|
|
111
|
+
}>;
|
|
87
112
|
}
|
|
88
113
|
export interface ConnectorRegistration {
|
|
89
114
|
id: string;
|
|
115
|
+
linkUrl?(code: string): string | undefined;
|
|
90
116
|
}
|
|
91
117
|
export interface AttachmentMaterializer {
|
|
92
118
|
store(bytes: Uint8Array, opts?: {
|
|
@@ -94,48 +120,84 @@ export interface AttachmentMaterializer {
|
|
|
94
120
|
mime?: string;
|
|
95
121
|
}): Promise<AttachmentRef>;
|
|
96
122
|
}
|
|
97
|
-
export
|
|
123
|
+
export type SenderIdKind = 'coffer-user' | 'transport';
|
|
124
|
+
export interface TurnEnvelope {
|
|
98
125
|
connectorId: string;
|
|
126
|
+
chatId: string;
|
|
127
|
+
turnId: string;
|
|
128
|
+
}
|
|
129
|
+
export interface ConnectorCapabilities {
|
|
130
|
+
events: readonly ('delta' | 'reasoning' | 'segment' | 'suggestions' | 'title')[];
|
|
131
|
+
privateChats: boolean;
|
|
132
|
+
}
|
|
133
|
+
export interface TurnBody {
|
|
134
|
+
systemPrompt: SystemAreas;
|
|
135
|
+
messages: ConvMessage[];
|
|
136
|
+
capabilities: ConnectorCapabilities;
|
|
137
|
+
userTurns: number;
|
|
138
|
+
}
|
|
139
|
+
export interface TurnRequest {
|
|
140
|
+
envelope: TurnEnvelope;
|
|
141
|
+
body: TurnBody;
|
|
99
142
|
agentId?: string;
|
|
100
143
|
presetId?: string;
|
|
101
|
-
chatId: string;
|
|
102
|
-
channelSystem?: string;
|
|
103
|
-
turnContext?: string;
|
|
104
144
|
sender: {
|
|
105
145
|
id: string;
|
|
106
146
|
displayName?: string;
|
|
147
|
+
idKind?: SenderIdKind;
|
|
107
148
|
};
|
|
108
|
-
|
|
149
|
+
turnContext?: ContextFact[];
|
|
109
150
|
prepareAttachments?: (materializer: AttachmentMaterializer) => Promise<ConvMessage[]>;
|
|
110
|
-
|
|
151
|
+
signal?: AbortSignal;
|
|
111
152
|
}
|
|
112
|
-
export
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
153
|
+
export type TurnEvent = {
|
|
154
|
+
kind: 'delta';
|
|
155
|
+
text: string;
|
|
156
|
+
} | {
|
|
157
|
+
kind: 'reasoning';
|
|
158
|
+
text: string;
|
|
159
|
+
} | {
|
|
160
|
+
kind: 'segment';
|
|
161
|
+
} | {
|
|
162
|
+
kind: 'answer';
|
|
116
163
|
text: string | null;
|
|
117
164
|
reasoning: string | null;
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
165
|
+
} | {
|
|
166
|
+
kind: 'usage';
|
|
167
|
+
tokensIn: number | null;
|
|
168
|
+
tokensOut: number | null;
|
|
169
|
+
presetId: string | null;
|
|
170
|
+
stopReason: string | null;
|
|
171
|
+
} | {
|
|
172
|
+
kind: 'error';
|
|
173
|
+
message: string;
|
|
174
|
+
} | {
|
|
175
|
+
kind: 'suggestions';
|
|
176
|
+
items: string[];
|
|
177
|
+
} | {
|
|
178
|
+
kind: 'title';
|
|
179
|
+
text: string;
|
|
180
|
+
} | {
|
|
181
|
+
kind: 'notice';
|
|
182
|
+
text: string;
|
|
183
|
+
};
|
|
184
|
+
export interface TurnSink {
|
|
185
|
+
emit(event: TurnEvent): void;
|
|
186
|
+
done(): Promise<void>;
|
|
125
187
|
}
|
|
126
188
|
export interface Connector {
|
|
127
189
|
id: string;
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
190
|
+
enrolmentNotice: string;
|
|
191
|
+
open(envelope: TurnEnvelope): TurnSink;
|
|
192
|
+
recordContext(m: {
|
|
193
|
+
chatId: string;
|
|
194
|
+
userMsgId: string;
|
|
195
|
+
facts: ContextFact[];
|
|
196
|
+
ts: number;
|
|
134
197
|
}): Promise<void>;
|
|
135
198
|
}
|
|
136
199
|
export interface GatePolicy {
|
|
137
200
|
agentId?: string;
|
|
138
|
-
accessPassword: string;
|
|
139
201
|
triggerPrefix: string;
|
|
140
202
|
replyWindow: number;
|
|
141
203
|
}
|
package/dist/plugin-hooks.d.ts
CHANGED
|
@@ -70,5 +70,31 @@ export interface PluginHooks {
|
|
|
70
70
|
actions?: Record<string, PluginAction>;
|
|
71
71
|
userActions?: Record<string, PluginUserAction>;
|
|
72
72
|
streamActions?: Record<string, PluginStreamAction>;
|
|
73
|
+
http?: PluginHttpMount[];
|
|
73
74
|
}
|
|
74
75
|
export declare const pluginHooks: Record<string, PluginHooks>;
|
|
76
|
+
export interface PluginHttpRequest {
|
|
77
|
+
method: string;
|
|
78
|
+
path: string;
|
|
79
|
+
url: string;
|
|
80
|
+
headers: Record<string, string>;
|
|
81
|
+
body: string;
|
|
82
|
+
user: {
|
|
83
|
+
id: number;
|
|
84
|
+
login: string;
|
|
85
|
+
role: AuthRole;
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
export interface PluginHttpResponse {
|
|
89
|
+
status: number;
|
|
90
|
+
headers?: Record<string, string>;
|
|
91
|
+
body?: string;
|
|
92
|
+
}
|
|
93
|
+
export interface PluginHttpMount {
|
|
94
|
+
prefix: string;
|
|
95
|
+
methods: string[];
|
|
96
|
+
auth: 'member' | 'admin' | 'token';
|
|
97
|
+
body?: 'text' | 'none';
|
|
98
|
+
wellKnown?: string;
|
|
99
|
+
handle(req: PluginHttpRequest): Promise<PluginHttpResponse>;
|
|
100
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { FastifyInstance, FastifyRequest } from 'fastify';
|
|
2
|
+
import type { AuthRole, PluginHooks, PluginHttpMount } from './plugin-hooks.ts';
|
|
3
|
+
export interface MountEntry {
|
|
4
|
+
pluginId: string;
|
|
5
|
+
mount: PluginHttpMount;
|
|
6
|
+
}
|
|
7
|
+
export declare function collectHttpMounts(hooks: Record<string, PluginHooks>): MountEntry[];
|
|
8
|
+
export declare function customMethodsOf(entries: MountEntry[]): string[];
|
|
9
|
+
export declare function registerTextBodyParsers(app: FastifyInstance): void;
|
|
10
|
+
export interface MountDeps {
|
|
11
|
+
disabledSet: () => Promise<Set<string>>;
|
|
12
|
+
resolveUser: (req: FastifyRequest) => Promise<{
|
|
13
|
+
id: number;
|
|
14
|
+
login: string;
|
|
15
|
+
role: AuthRole;
|
|
16
|
+
} | null>;
|
|
17
|
+
}
|
|
18
|
+
export declare function registerPluginHttpMounts(app: FastifyInstance, entries: MountEntry[], deps: MountDeps): void;
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
const RESERVED = ['/api', '/mcp', '/health', '/uploads', '/.well-known', '/assets'];
|
|
2
|
+
const DEFAULT_METHODS = new Set(['GET', 'HEAD', 'POST', 'PUT', 'DELETE', 'OPTIONS', 'PATCH']);
|
|
3
|
+
export function collectHttpMounts(hooks) {
|
|
4
|
+
const out = [];
|
|
5
|
+
const owner = new Map();
|
|
6
|
+
for (const [pluginId, h] of Object.entries(hooks)) {
|
|
7
|
+
for (const mount of h.http ?? []) {
|
|
8
|
+
const prefix = mount.prefix.replace(/\/+$/, '');
|
|
9
|
+
if (!prefix.startsWith('/') || prefix === '') {
|
|
10
|
+
throw new Error(`[plugin-http] ${pluginId}: prefix must be absolute, got '${mount.prefix}'`);
|
|
11
|
+
}
|
|
12
|
+
if (RESERVED.some((r) => prefix === r || prefix.startsWith(r + '/'))) {
|
|
13
|
+
throw new Error(`[plugin-http] ${pluginId}: prefix '${prefix}' is reserved for the core`);
|
|
14
|
+
}
|
|
15
|
+
const other = owner.get(prefix);
|
|
16
|
+
if (other)
|
|
17
|
+
throw new Error(`[plugin-http] prefix '${prefix}' is declared by both ${other} and ${pluginId}`);
|
|
18
|
+
owner.set(prefix, pluginId);
|
|
19
|
+
out.push({ pluginId, mount: { ...mount, prefix, methods: mount.methods.map((m) => m.toUpperCase()) } });
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
return out;
|
|
23
|
+
}
|
|
24
|
+
export function customMethodsOf(entries) {
|
|
25
|
+
const seen = new Set();
|
|
26
|
+
for (const e of entries)
|
|
27
|
+
for (const m of e.mount.methods)
|
|
28
|
+
if (!DEFAULT_METHODS.has(m))
|
|
29
|
+
seen.add(m);
|
|
30
|
+
return [...seen];
|
|
31
|
+
}
|
|
32
|
+
export function registerTextBodyParsers(app) {
|
|
33
|
+
const asText = (_req, body, done) => done(null, body);
|
|
34
|
+
for (const type of ['text/calendar', 'text/xml', 'application/xml', 'text/plain', 'application/octet-stream']) {
|
|
35
|
+
if (!app.hasContentTypeParser(type))
|
|
36
|
+
app.addContentTypeParser(type, { parseAs: 'string' }, asText);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
function flatHeaders(raw) {
|
|
40
|
+
const out = {};
|
|
41
|
+
for (const [k, v] of Object.entries(raw)) {
|
|
42
|
+
if (v === undefined)
|
|
43
|
+
continue;
|
|
44
|
+
out[k.toLowerCase()] = Array.isArray(v) ? v.join(', ') : String(v);
|
|
45
|
+
}
|
|
46
|
+
return out;
|
|
47
|
+
}
|
|
48
|
+
export function registerPluginHttpMounts(app, entries, deps) {
|
|
49
|
+
for (const method of customMethodsOf(entries))
|
|
50
|
+
app.addHttpMethod(method, { hasBody: true });
|
|
51
|
+
for (const { pluginId, mount } of entries) {
|
|
52
|
+
const allow = mount.methods.join(', ');
|
|
53
|
+
const handler = async (req, reply) => {
|
|
54
|
+
if ((await deps.disabledSet()).has(pluginId))
|
|
55
|
+
return reply.code(404).send({ error: 'not_found' });
|
|
56
|
+
if (!mount.methods.includes(req.method.toUpperCase())) {
|
|
57
|
+
return reply.code(405).header('allow', allow).send();
|
|
58
|
+
}
|
|
59
|
+
const user = req.user ?? (await deps.resolveUser(req));
|
|
60
|
+
if (!user) {
|
|
61
|
+
if (mount.auth === 'token')
|
|
62
|
+
reply.header('www-authenticate', 'Basic realm="Coffer"');
|
|
63
|
+
return reply.code(401).send({ error: 'unauthorized' });
|
|
64
|
+
}
|
|
65
|
+
if (mount.auth === 'admin' && user.role !== 'admin')
|
|
66
|
+
return reply.code(403).send({ error: 'forbidden' });
|
|
67
|
+
const full = req.url.split('?')[0];
|
|
68
|
+
const below = full.slice(mount.prefix.length);
|
|
69
|
+
const body = mount.body === 'none' ? '' : typeof req.body === 'string' ? req.body : req.body ? JSON.stringify(req.body) : '';
|
|
70
|
+
const res = await mount.handle({
|
|
71
|
+
method: req.method.toUpperCase(),
|
|
72
|
+
path: below === '' ? '/' : below,
|
|
73
|
+
url: req.url,
|
|
74
|
+
headers: flatHeaders(req.headers),
|
|
75
|
+
body,
|
|
76
|
+
user: { id: user.id, login: user.login, role: user.role },
|
|
77
|
+
});
|
|
78
|
+
reply.code(res.status);
|
|
79
|
+
for (const [k, v] of Object.entries(res.headers ?? {}))
|
|
80
|
+
reply.header(k, v);
|
|
81
|
+
return reply.send(res.body ?? '');
|
|
82
|
+
};
|
|
83
|
+
const verbs = [...new Set([...mount.methods, ...DEFAULT_METHODS])];
|
|
84
|
+
app.route({ method: verbs, url: mount.prefix, handler });
|
|
85
|
+
app.route({ method: verbs, url: `${mount.prefix}/*`, handler });
|
|
86
|
+
if (mount.wellKnown) {
|
|
87
|
+
app.route({
|
|
88
|
+
method: verbs,
|
|
89
|
+
url: `/.well-known/${mount.wellKnown}`,
|
|
90
|
+
handler: async (_req, reply) => reply.code(301).header('location', `${mount.prefix}/`).send(),
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
package/dist/plugin-runtime.js
CHANGED
|
@@ -13,7 +13,7 @@ import { migrateEmbeddingVectorsToBlob } from "./embeddings.js";
|
|
|
13
13
|
import { ensureSearchTable } from "./search-index.js";
|
|
14
14
|
import { startScheduler, stopScheduler } from "./background-scheduler.js";
|
|
15
15
|
import { startSearchIndexer, indexSearchOnce } from "./search-indexer.js";
|
|
16
|
-
import { startOrchestrator, stopOrchestrator, orchestratorStartersTask } from "./orchestrator/index.js";
|
|
16
|
+
import { startOrchestrator, stopOrchestrator, orchestratorStartersTask, linkCodePruneTask, } from "./orchestrator/index.js";
|
|
17
17
|
import { SYSTEM_SETTINGS_ID, SYSTEM_SETTINGS } from "./system-settings.js";
|
|
18
18
|
const log = getLogger('plugins');
|
|
19
19
|
let stopSearchIndexer;
|
|
@@ -110,7 +110,7 @@ export async function initPlugins() {
|
|
|
110
110
|
Object.assign(pluginHooks, await loadServerHooks());
|
|
111
111
|
await runSeeds({ em: getEm().fork(), plugins: reg.order, hooks: pluginHooks });
|
|
112
112
|
startOrchestrator();
|
|
113
|
-
const bgTasks = [orchestratorStartersTask];
|
|
113
|
+
const bgTasks = [orchestratorStartersTask, linkCodePruneTask];
|
|
114
114
|
for (const p of reg.order) {
|
|
115
115
|
const h = pluginHooks[p.id];
|
|
116
116
|
try {
|
package/dist/records-api.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { isJsonArrayStored } from '@coffer-org/sdk/fields';
|
|
2
|
+
import { raw } from '@mikro-orm/core';
|
|
1
3
|
import { fieldMap, textSearchKeys, titleKey, recordTitle, listKeys, storageColumnsFor, magnitudeSub, resolveColumnKey, } from '@coffer-org/sdk/shelf';
|
|
2
4
|
import { tokenize, matchScoreFolded, foldText } from '@coffer-org/sdk/search';
|
|
3
5
|
import { getActiveRegistry, getShelf, getExtendsFor } from "./registry-context.js";
|
|
@@ -97,7 +99,7 @@ function resolveFilterKey(m, fm, k) {
|
|
|
97
99
|
const direct = fm[k];
|
|
98
100
|
if (direct) {
|
|
99
101
|
if (!direct.columns)
|
|
100
|
-
return { column: k, type: direct.column };
|
|
102
|
+
return { column: k, type: direct.column, json: isJsonArrayStored(direct) };
|
|
101
103
|
const sub = magnitudeSub(direct);
|
|
102
104
|
if (sub === undefined)
|
|
103
105
|
throw new FilterError(`${m.library}/${m.shelf}: '${k}' is a composite with no stored part — it owns no column to filter on`);
|
|
@@ -110,14 +112,24 @@ function resolveFilterKey(m, fm, k) {
|
|
|
110
112
|
throw new FilterError(`${m.library}/${m.shelf}: '${k}' names no part of composite '${owner.key}' (parts: ${Object.keys(owner.field.columns).join(', ')})`);
|
|
111
113
|
return { column: k, type: owner.field.columns[owner.sub] };
|
|
112
114
|
}
|
|
115
|
+
function jsonMemberClause(column, value) {
|
|
116
|
+
const col = `"${column.replace(/"/g, '""')}"`;
|
|
117
|
+
return {
|
|
118
|
+
[raw(`(json_valid(${col}) AND EXISTS (SELECT 1 FROM json_each(${col}) WHERE CAST(value AS TEXT) = ?))`, [value])]: 1,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
113
121
|
function buildWhere(m, filterParams) {
|
|
114
|
-
|
|
122
|
+
let where = {};
|
|
115
123
|
const fm = fieldMap(m.fields);
|
|
116
124
|
for (const [k, v] of Object.entries(filterParams)) {
|
|
117
125
|
if (k === 'id' || v === undefined)
|
|
118
126
|
continue;
|
|
119
127
|
const target = resolveFilterKey(m, fm, k);
|
|
120
|
-
if (target)
|
|
128
|
+
if (!target)
|
|
129
|
+
continue;
|
|
130
|
+
if (target.json)
|
|
131
|
+
where = { ...where, ...jsonMemberClause(target.column, String(v)) };
|
|
132
|
+
else
|
|
121
133
|
where[target.column] = coerceFilter(String(v), target.type);
|
|
122
134
|
}
|
|
123
135
|
if (filterParams['id']) {
|
package/dist/system-settings.js
CHANGED
|
@@ -20,7 +20,6 @@ export const SYSTEM_SETTINGS = defineSettings({
|
|
|
20
20
|
],
|
|
21
21
|
}),
|
|
22
22
|
agent_id: field.string({ label: 'core.settings.agent_id', strict: true, noSearch: true }),
|
|
23
|
-
access_password: field.password({ label: 'core.settings.access_password' }),
|
|
24
23
|
trigger_prefix: field.string({ label: 'core.settings.trigger_prefix' }),
|
|
25
24
|
reply_window: field.int({ label: 'core.settings.reply_window', default: 1800 }),
|
|
26
25
|
},
|
package/dist/thread-state.d.ts
CHANGED
|
@@ -1,8 +1,22 @@
|
|
|
1
1
|
export interface ThreadSelection {
|
|
2
2
|
agentId: string | null;
|
|
3
3
|
presetId: string | null;
|
|
4
|
+
title: string | null;
|
|
5
|
+
owner: string | null;
|
|
6
|
+
visibility: 'private' | null;
|
|
4
7
|
}
|
|
8
|
+
export declare const TITLE_MAX = 60;
|
|
5
9
|
export declare function getThreadState(connector: string, chatId: string): Promise<ThreadSelection>;
|
|
10
|
+
export declare function getThreadStates(connector: string, chatIds: string[]): Promise<Map<string, ThreadSelection>>;
|
|
6
11
|
export declare function setThreadState(connector: string, chatId: string, patch: Partial<ThreadSelection>): Promise<void>;
|
|
7
12
|
export declare function readAndTouchThreadState(connector: string, chatId: string): Promise<ThreadSelection>;
|
|
13
|
+
export declare function findChatByConvId(connector: string, convId: string): Promise<ChatLookup>;
|
|
14
|
+
export type ChatLookup = {
|
|
15
|
+
found: 'one';
|
|
16
|
+
chatId: string;
|
|
17
|
+
} | {
|
|
18
|
+
found: 'none';
|
|
19
|
+
} | {
|
|
20
|
+
found: 'ambiguous';
|
|
21
|
+
};
|
|
8
22
|
export declare function pruneThreadState(connector: string, cutoffIso: string): Promise<void>;
|
package/dist/thread-state.js
CHANGED
|
@@ -1,26 +1,63 @@
|
|
|
1
1
|
import { getEm } from "./db.js";
|
|
2
|
-
|
|
2
|
+
import { getLogger } from '@coffer-org/sdk/logger';
|
|
3
|
+
const log = getLogger('thread-state');
|
|
4
|
+
export const TITLE_MAX = 60;
|
|
5
|
+
function capTitle(title) {
|
|
6
|
+
const t = title.trim();
|
|
7
|
+
return t.length <= TITLE_MAX ? t : `${t.slice(0, TITLE_MAX - 1)}…`;
|
|
8
|
+
}
|
|
9
|
+
const EMPTY = { agentId: null, presetId: null, title: null, owner: null, visibility: null };
|
|
10
|
+
function toSelection(row) {
|
|
11
|
+
return {
|
|
12
|
+
agentId: row.agent_id,
|
|
13
|
+
presetId: row.preset_id,
|
|
14
|
+
title: row.title,
|
|
15
|
+
owner: row.owner,
|
|
16
|
+
visibility: row.visibility === 'private' ? 'private' : null,
|
|
17
|
+
};
|
|
18
|
+
}
|
|
3
19
|
export async function getThreadState(connector, chatId) {
|
|
4
20
|
const em = getEm().fork();
|
|
5
21
|
const row = (await em.findOne('_ThreadState', { connector, chat_id: chatId }));
|
|
6
22
|
if (!row)
|
|
7
23
|
return { ...EMPTY };
|
|
8
|
-
return
|
|
24
|
+
return toSelection(row);
|
|
25
|
+
}
|
|
26
|
+
export async function getThreadStates(connector, chatIds) {
|
|
27
|
+
if (chatIds.length === 0)
|
|
28
|
+
return new Map();
|
|
29
|
+
const em = getEm().fork();
|
|
30
|
+
const rows = (await em.find('_ThreadState', { connector, chat_id: { $in: chatIds } }));
|
|
31
|
+
return new Map(rows.map((row) => [row.chat_id, toSelection(row)]));
|
|
9
32
|
}
|
|
10
33
|
export async function setThreadState(connector, chatId, patch) {
|
|
11
34
|
const em = getEm().fork();
|
|
12
|
-
const
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
35
|
+
const data = { updated_at: new Date().toISOString() };
|
|
36
|
+
if ('agentId' in patch)
|
|
37
|
+
data['agent_id'] = patch.agentId ?? null;
|
|
38
|
+
if ('presetId' in patch)
|
|
39
|
+
data['preset_id'] = patch.presetId ?? null;
|
|
40
|
+
if ('title' in patch)
|
|
41
|
+
data['title'] = patch.title ? capTitle(patch.title) : null;
|
|
42
|
+
if ('owner' in patch)
|
|
43
|
+
data['owner'] = patch.owner ?? null;
|
|
44
|
+
if ('visibility' in patch)
|
|
45
|
+
data['visibility'] = patch.visibility ?? null;
|
|
17
46
|
const existing = await em.findOne('_ThreadState', { connector, chat_id: chatId });
|
|
18
|
-
const data = { agent_id: next.agentId, preset_id: next.presetId, updated_at: new Date().toISOString() };
|
|
19
47
|
if (existing) {
|
|
20
48
|
em.assign(existing, data);
|
|
21
49
|
}
|
|
22
50
|
else {
|
|
23
|
-
em.persist(em.create('_ThreadState', {
|
|
51
|
+
em.persist(em.create('_ThreadState', {
|
|
52
|
+
connector,
|
|
53
|
+
chat_id: chatId,
|
|
54
|
+
agent_id: data['agent_id'] ?? null,
|
|
55
|
+
preset_id: data['preset_id'] ?? null,
|
|
56
|
+
title: data['title'] ?? null,
|
|
57
|
+
owner: data['owner'] ?? null,
|
|
58
|
+
visibility: data['visibility'] ?? null,
|
|
59
|
+
updated_at: data['updated_at'],
|
|
60
|
+
}));
|
|
24
61
|
}
|
|
25
62
|
await em.flush();
|
|
26
63
|
}
|
|
@@ -31,9 +68,32 @@ export async function readAndTouchThreadState(connector, chatId) {
|
|
|
31
68
|
return { ...EMPTY };
|
|
32
69
|
em.assign(row, { updated_at: new Date().toISOString() });
|
|
33
70
|
await em.flush();
|
|
34
|
-
return
|
|
71
|
+
return toSelection(row);
|
|
72
|
+
}
|
|
73
|
+
export async function findChatByConvId(connector, convId) {
|
|
74
|
+
const suffix = `:${convId}`;
|
|
75
|
+
const em = getEm().fork();
|
|
76
|
+
const like = { $like: `%${suffix}` };
|
|
77
|
+
const stateRows = (await em.find('_ThreadState', { connector, chat_id: like }));
|
|
78
|
+
const stateMatches = matchingChatIds(stateRows, suffix);
|
|
79
|
+
if (stateMatches.length > 0)
|
|
80
|
+
return pickUnambiguous(stateMatches, connector, convId);
|
|
81
|
+
const messageRows = (await em.find('_ThreadMessage', { connector, chat_id: like }));
|
|
82
|
+
const messageMatches = matchingChatIds(messageRows, suffix);
|
|
83
|
+
return pickUnambiguous(messageMatches, connector, convId);
|
|
84
|
+
}
|
|
85
|
+
function matchingChatIds(rows, suffix) {
|
|
86
|
+
return [...new Set(rows.map((r) => r.chat_id).filter((id) => id.endsWith(suffix)))];
|
|
87
|
+
}
|
|
88
|
+
function pickUnambiguous(matches, connector, convId) {
|
|
89
|
+
if (matches.length === 0)
|
|
90
|
+
return { found: 'none' };
|
|
91
|
+
if (matches.length === 1)
|
|
92
|
+
return { found: 'one', chatId: matches[0] };
|
|
93
|
+
log.warn(`ambiguous convId "${convId}" for connector "${connector}": ${matches.length} chats match`);
|
|
94
|
+
return { found: 'ambiguous' };
|
|
35
95
|
}
|
|
36
96
|
export async function pruneThreadState(connector, cutoffIso) {
|
|
37
97
|
const em = getEm().fork();
|
|
38
|
-
await em.nativeDelete('_ThreadState', { connector, updated_at: { $lt: cutoffIso } });
|
|
98
|
+
await em.nativeDelete('_ThreadState', { connector, updated_at: { $lt: cutoffIso }, owner: null, visibility: null });
|
|
39
99
|
}
|
package/dist/thread-store.d.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
+
export declare const HIDDEN_ROLES: readonly ["reasoning", "suggestions", "context"];
|
|
1
2
|
export declare const SIDECAR_ROLES: readonly ["reasoning", "suggestions"];
|
|
2
3
|
export interface StoredMsg {
|
|
3
4
|
msgId: string;
|
|
4
|
-
role: 'user' | 'assistant' | 'reasoning' | 'suggestions';
|
|
5
|
+
role: 'user' | 'assistant' | 'reasoning' | 'suggestions' | 'context';
|
|
5
6
|
sender: string | null;
|
|
6
7
|
text: string;
|
|
7
8
|
attachments?: StoredAttachment[];
|
|
@@ -25,7 +26,7 @@ export declare function putThreadMessage(m: {
|
|
|
25
26
|
connector: string;
|
|
26
27
|
chatId: string;
|
|
27
28
|
msgId: string;
|
|
28
|
-
role: 'user' | 'assistant' | 'reasoning' | 'suggestions';
|
|
29
|
+
role: 'user' | 'assistant' | 'reasoning' | 'suggestions' | 'context';
|
|
29
30
|
sender?: string | null;
|
|
30
31
|
attachments?: StoredAttachment[];
|
|
31
32
|
text: string;
|
|
@@ -34,4 +35,5 @@ export declare function putThreadMessage(m: {
|
|
|
34
35
|
}): Promise<void>;
|
|
35
36
|
export declare function pruneThreadMessages(connector: string, cutoffTs: number): Promise<void>;
|
|
36
37
|
export declare function listThreadMessages(connector: string, chatId: string, limit?: number): Promise<StoredMsg[]>;
|
|
37
|
-
export declare function
|
|
38
|
+
export declare function countUserTurns(connector: string, chatId: string): Promise<number>;
|
|
39
|
+
export declare function listAllThreadChats(connector: string): Promise<ThreadChat[]>;
|
package/dist/thread-store.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { getEm } from "./db.js";
|
|
2
|
+
export const HIDDEN_ROLES = ['reasoning', 'suggestions', 'context'];
|
|
2
3
|
export const SIDECAR_ROLES = ['reasoning', 'suggestions'];
|
|
3
4
|
function toStored(r) {
|
|
4
5
|
let attachments;
|
|
@@ -50,7 +51,7 @@ export async function pruneThreadMessages(connector, cutoffTs) {
|
|
|
50
51
|
}
|
|
51
52
|
export async function listThreadMessages(connector, chatId, limit = 200) {
|
|
52
53
|
const em = getEm().fork();
|
|
53
|
-
const visible = (await em.find('_ThreadMessage', { connector, chat_id: chatId, role: { $nin:
|
|
54
|
+
const visible = (await em.find('_ThreadMessage', { connector, chat_id: chatId, role: { $nin: HIDDEN_ROLES } }, { orderBy: { ts: 'desc', msg_id: 'desc' }, limit }));
|
|
54
55
|
const oldest = visible.at(-1)?.ts;
|
|
55
56
|
const sidecars = oldest === undefined
|
|
56
57
|
? []
|
|
@@ -60,17 +61,14 @@ export async function listThreadMessages(connector, chatId, limit = 200) {
|
|
|
60
61
|
.reverse()
|
|
61
62
|
.map(toStored);
|
|
62
63
|
}
|
|
63
|
-
export async function
|
|
64
|
+
export async function countUserTurns(connector, chatId) {
|
|
64
65
|
const em = getEm().fork();
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
const last = chatIdPrefix.charCodeAt(chatIdPrefix.length - 1);
|
|
69
|
-
const upper = chatIdPrefix.slice(0, -1) + String.fromCharCode(last + 1);
|
|
70
|
-
const rows = (await em.find('_ThreadMessage', { connector, chat_id: { $gte: chatIdPrefix, $lt: upper } }, { orderBy: { ts: 'asc' } }));
|
|
66
|
+
return em.count('_ThreadMessage', { connector, chat_id: chatId, role: 'user' });
|
|
67
|
+
}
|
|
68
|
+
function groupIntoChats(rows) {
|
|
71
69
|
const byChat = new Map();
|
|
72
70
|
for (const r of rows) {
|
|
73
|
-
if (
|
|
71
|
+
if (HIDDEN_ROLES.includes(r.role))
|
|
74
72
|
continue;
|
|
75
73
|
const cur = byChat.get(r.chat_id);
|
|
76
74
|
if (!cur) {
|
|
@@ -90,3 +88,8 @@ export async function listThreadChats(connector, chatIdPrefix) {
|
|
|
90
88
|
}
|
|
91
89
|
return [...byChat.values()].sort((a, b) => b.lastTs - a.lastTs);
|
|
92
90
|
}
|
|
91
|
+
export async function listAllThreadChats(connector) {
|
|
92
|
+
const em = getEm().fork();
|
|
93
|
+
const rows = (await em.find('_ThreadMessage', { connector }, { orderBy: { ts: 'asc' } }));
|
|
94
|
+
return groupIntoChats(rows);
|
|
95
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export interface BeginTurnOpts {
|
|
2
|
+
supersede: boolean;
|
|
3
|
+
}
|
|
4
|
+
export declare function beginTurn(connector: string, chatId: string, opts: BeginTurnOpts): Promise<{
|
|
5
|
+
signal: AbortSignal;
|
|
6
|
+
end(): void;
|
|
7
|
+
}>;
|
|
8
|
+
export declare function abortTurn(connector: string, chatId: string): boolean;
|