@coffer-org/plugin-orchestrator 2.1.1 → 2.2.1
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/runtime/agent-capabilities.d.ts +2 -0
- package/dist/runtime/agent-capabilities.js +131 -0
- package/dist/runtime/attachments.d.ts +2 -0
- package/dist/runtime/attachments.js +11 -0
- package/dist/runtime/index.d.ts +3 -1
- package/dist/runtime/index.js +2 -0
- package/dist/runtime/pipeline.js +14 -3
- package/dist/runtime/types.d.ts +9 -2
- package/package.json +3 -3
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { collectMcpTools } from '@coffer-org/server/mcp-tools';
|
|
2
|
+
import { LocalClient } from '@coffer-org/server/mcp-local';
|
|
3
|
+
const ok = (data) => ({
|
|
4
|
+
content: [{ type: 'text', text: JSON.stringify(data, null, 2) }],
|
|
5
|
+
});
|
|
6
|
+
const fail = (text) => ({ content: [{ type: 'text', text }], isError: true });
|
|
7
|
+
function textOf(result) {
|
|
8
|
+
const block = result.content.find((b) => b.type === 'text');
|
|
9
|
+
return block?.type === 'text' ? block.text : '';
|
|
10
|
+
}
|
|
11
|
+
function resultData(result) {
|
|
12
|
+
try {
|
|
13
|
+
return JSON.parse(textOf(result));
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
return undefined;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
function fieldType(items, key) {
|
|
20
|
+
for (const item of items) {
|
|
21
|
+
if (!item || typeof item !== 'object')
|
|
22
|
+
continue;
|
|
23
|
+
const raw = item;
|
|
24
|
+
if (raw.key === key && raw.type)
|
|
25
|
+
return raw.type;
|
|
26
|
+
if (raw.kind === 'group' && Array.isArray(raw.fields)) {
|
|
27
|
+
const nested = fieldType(raw.fields, key);
|
|
28
|
+
if (nested)
|
|
29
|
+
return nested;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return undefined;
|
|
33
|
+
}
|
|
34
|
+
async function findFileField(library, shelf, key) {
|
|
35
|
+
const schema = (await new LocalClient().getSchema());
|
|
36
|
+
const lib = schema.find((item) => item.id === library);
|
|
37
|
+
const sh = lib?.shelves?.find((item) => item.shelf === shelf);
|
|
38
|
+
return sh?.fields ? fieldType(sh.fields, key) : undefined;
|
|
39
|
+
}
|
|
40
|
+
function parseStoredValue(value) {
|
|
41
|
+
if (typeof value !== 'string')
|
|
42
|
+
return value;
|
|
43
|
+
try {
|
|
44
|
+
return JSON.parse(value);
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
return value;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
function fileItems(value) {
|
|
51
|
+
const parsed = parseStoredValue(value);
|
|
52
|
+
if (Array.isArray(parsed))
|
|
53
|
+
return parsed.filter((item) => !!item && typeof item === 'object');
|
|
54
|
+
return parsed && typeof parsed === 'object' ? [parsed] : [];
|
|
55
|
+
}
|
|
56
|
+
function findTool(defs, name) {
|
|
57
|
+
return defs.find((def) => def.server === 'coffer' && def.bareName === name);
|
|
58
|
+
}
|
|
59
|
+
export async function makeAttachmentCapabilities(attachments) {
|
|
60
|
+
if (!attachments.length)
|
|
61
|
+
return { tools: [] };
|
|
62
|
+
const defs = await collectMcpTools();
|
|
63
|
+
const getRecord = findTool(defs, 'get_record');
|
|
64
|
+
const updateRecord = findTool(defs, 'update_record');
|
|
65
|
+
if (!getRecord || !updateRecord)
|
|
66
|
+
return { tools: [] };
|
|
67
|
+
const attach = {
|
|
68
|
+
name: 'mcp__coffer__attach_conversation_attachments',
|
|
69
|
+
description: 'Attach files already received in the current conversation to a file/image/document field in a Coffer record. ' +
|
|
70
|
+
'Use attachment_names for selected files, or omit it to attach every current-conversation attachment. ' +
|
|
71
|
+
'The default mode appends to a multiple field; use mode="replace" only when explicitly requested. ' +
|
|
72
|
+
'This is the unified attachment tool for every connector — never use a local path or invent an upload name.',
|
|
73
|
+
inputSchema: {
|
|
74
|
+
library: { type: 'string', description: 'Library identifier, e.g. things' },
|
|
75
|
+
shelf: { type: 'string', description: 'Shelf identifier, e.g. item' },
|
|
76
|
+
id: { type: 'integer', description: 'Record id' },
|
|
77
|
+
field: { type: 'string', description: 'File/image/document field key' },
|
|
78
|
+
attachment_names: {
|
|
79
|
+
type: 'array',
|
|
80
|
+
items: { type: 'string' },
|
|
81
|
+
description: 'Exact server-generated names from the current conversation; omit to use all attachments',
|
|
82
|
+
},
|
|
83
|
+
mode: { type: 'string', enum: ['append', 'replace'], description: 'append by default; replace only on explicit request' },
|
|
84
|
+
},
|
|
85
|
+
handler: async (args) => {
|
|
86
|
+
const library = String(args['library'] ?? '');
|
|
87
|
+
const shelf = String(args['shelf'] ?? '');
|
|
88
|
+
const id = Number(args['id']);
|
|
89
|
+
const field = String(args['field'] ?? '');
|
|
90
|
+
const mode = args['mode'] === 'replace' ? 'replace' : 'append';
|
|
91
|
+
if (!library || !shelf || !field || !Number.isInteger(id))
|
|
92
|
+
return fail('library, shelf, field and integer id are required');
|
|
93
|
+
const names = Array.isArray(args['attachment_names'])
|
|
94
|
+
? args['attachment_names'].filter((name) => typeof name === 'string')
|
|
95
|
+
: attachments.map((item) => item.name);
|
|
96
|
+
const uniqueNames = [...new Set(names)];
|
|
97
|
+
const refs = uniqueNames.map((name) => attachments.find((item) => item.name === name));
|
|
98
|
+
if (refs.some((ref) => !ref)) {
|
|
99
|
+
const missing = uniqueNames.filter((name) => !attachments.some((item) => item.name === name));
|
|
100
|
+
return fail(`Attachment is not available in the current conversation: ${missing.join(', ')}`);
|
|
101
|
+
}
|
|
102
|
+
if (!refs.length)
|
|
103
|
+
return fail('No conversation attachments were selected');
|
|
104
|
+
const type = await findFileField(library, shelf, field);
|
|
105
|
+
if (!type || type.prim !== 'file')
|
|
106
|
+
return fail(`Field is not a file field: ${library}/${shelf}.${field}`);
|
|
107
|
+
const multiple = type.hints?.multiple === true;
|
|
108
|
+
const currentResult = await getRecord.handler({ library, shelf, id });
|
|
109
|
+
if (currentResult.isError)
|
|
110
|
+
return currentResult;
|
|
111
|
+
const current = resultData(currentResult);
|
|
112
|
+
if (!current || typeof current !== 'object')
|
|
113
|
+
return fail(`Record not found: ${library}/${shelf}/${id}`);
|
|
114
|
+
const existing = fileItems(current[field]);
|
|
115
|
+
const incoming = refs.map((ref) => ({ name: ref.name }));
|
|
116
|
+
if (!multiple && mode === 'append' && existing.length)
|
|
117
|
+
return fail(`Field already contains a file; use mode="replace" to replace it`);
|
|
118
|
+
if (!multiple && incoming.length > 1)
|
|
119
|
+
return fail(`Field accepts one file; select one attachment or use a multiple field`);
|
|
120
|
+
const next = mode === 'replace'
|
|
121
|
+
? incoming
|
|
122
|
+
: [...existing, ...incoming.filter((item) => !existing.some((old) => old.name === item.name))];
|
|
123
|
+
const value = multiple ? next : next[0] ?? null;
|
|
124
|
+
const updated = await updateRecord.handler({ library, shelf, id, fields: { [field]: value } });
|
|
125
|
+
if (updated.isError)
|
|
126
|
+
return updated;
|
|
127
|
+
return ok({ attached: incoming.map((item) => item.name), mode, library, shelf, id, field, record: resultData(updated) });
|
|
128
|
+
},
|
|
129
|
+
};
|
|
130
|
+
return { tools: [attach] };
|
|
131
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { saveUploadBytes } from '@coffer-org/server/uploads';
|
|
2
|
+
export const attachmentMaterializer = {
|
|
3
|
+
async store(bytes, opts = {}) {
|
|
4
|
+
const stored = await saveUploadBytes(bytes, opts);
|
|
5
|
+
return {
|
|
6
|
+
name: stored.name,
|
|
7
|
+
...(stored.mime ? { mime: stored.mime } : {}),
|
|
8
|
+
size: stored.size,
|
|
9
|
+
};
|
|
10
|
+
},
|
|
11
|
+
};
|
package/dist/runtime/index.d.ts
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import type { PluginHooks } from '@coffer-org/server/plugin-hooks';
|
|
2
2
|
export { handleIncoming } from './pipeline.ts';
|
|
3
|
+
export { attachmentMaterializer } from './attachments.ts';
|
|
4
|
+
export { makeAttachmentCapabilities } from './agent-capabilities.ts';
|
|
3
5
|
export type { PipelineDeps, RunAgentFn } from './pipeline.ts';
|
|
4
6
|
export { buildPolicy, loadGatePolicy } from './config.ts';
|
|
5
7
|
export { makeLiveChannel, plainRender } from './live-message.ts';
|
|
6
8
|
export type { LiveChannelOps, LiveChannelOpts, RenderFn } from './live-message.ts';
|
|
7
9
|
export { chunk } from './format.ts';
|
|
8
|
-
export type { Connector, IncomingConversation, GatePolicy, ReplyContext, ReplyPayload, ReplyChannel, ConvMessage, SystemLayer, AgentRequest, AgentResult, } from './types.ts';
|
|
10
|
+
export type { Connector, IncomingConversation, GatePolicy, ReplyContext, ReplyPayload, ReplyChannel, AttachmentRef, AttachmentMaterializer, AgentToolDefinition, AgentToolProvider, ConvMessage, SystemLayer, AgentRequest, AgentResult, } from './types.ts';
|
|
9
11
|
export declare const serverHooks: PluginHooks;
|
package/dist/runtime/index.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { openLogDb } from "./db.js";
|
|
2
2
|
import { setLogDb } from "./pipeline.js";
|
|
3
3
|
export { handleIncoming } from "./pipeline.js";
|
|
4
|
+
export { attachmentMaterializer } from "./attachments.js";
|
|
5
|
+
export { makeAttachmentCapabilities } from "./agent-capabilities.js";
|
|
4
6
|
export { buildPolicy, loadGatePolicy } from "./config.js";
|
|
5
7
|
export { makeLiveChannel, plainRender } from "./live-message.js";
|
|
6
8
|
export { chunk } from "./format.js";
|
package/dist/runtime/pipeline.js
CHANGED
|
@@ -3,6 +3,8 @@ import { join } from 'node:path';
|
|
|
3
3
|
import { loadAllowed, saveAllowed, isAllowed, addAllowed, makeThrottle } from "./allow.js";
|
|
4
4
|
import { loadGatePolicy } from "./config.js";
|
|
5
5
|
import { buildSystem } from "./system-assembly.js";
|
|
6
|
+
import { attachmentMaterializer } from "./attachments.js";
|
|
7
|
+
import { makeAttachmentCapabilities } from "./agent-capabilities.js";
|
|
6
8
|
import { getLogger } from '@coffer-org/sdk/logger';
|
|
7
9
|
const log = getLogger('orchestrator');
|
|
8
10
|
const stateDir = () => process.env['ORCHESTRATOR_STATE_DIR']
|
|
@@ -71,11 +73,18 @@ export async function handleIncoming(connector, conversation, deps) {
|
|
|
71
73
|
if (policy.triggerPrefix && !last.content.toLowerCase().startsWith(policy.triggerPrefix.toLowerCase()))
|
|
72
74
|
return;
|
|
73
75
|
const queryText = policy.triggerPrefix ? last.content.slice(policy.triggerPrefix.length).trim() : last.content.trim();
|
|
74
|
-
if (!queryText)
|
|
76
|
+
if (!queryText) {
|
|
77
|
+
if (conversation.prepareAttachments && !policy.triggerPrefix) {
|
|
78
|
+
await safeCall(() => conversation.prepareAttachments(attachmentMaterializer), messages, 'prepareAttachments');
|
|
79
|
+
}
|
|
75
80
|
return;
|
|
76
|
-
|
|
77
|
-
|
|
81
|
+
}
|
|
82
|
+
const preparedMessages = conversation.prepareAttachments
|
|
83
|
+
? await safeCall(() => conversation.prepareAttachments(attachmentMaterializer), messages, 'prepareAttachments')
|
|
78
84
|
: messages;
|
|
85
|
+
const agentMessages = policy.triggerPrefix
|
|
86
|
+
? preparedMessages.map((m, i) => (i === preparedMessages.length - 1 ? { ...m, content: queryText } : m))
|
|
87
|
+
: preparedMessages;
|
|
79
88
|
const base = await agentBase();
|
|
80
89
|
const system = buildSystem({
|
|
81
90
|
base,
|
|
@@ -89,9 +98,11 @@ export async function handleIncoming(connector, conversation, deps) {
|
|
|
89
98
|
return;
|
|
90
99
|
let result;
|
|
91
100
|
try {
|
|
101
|
+
const toolProvider = await makeAttachmentCapabilities(agentMessages.flatMap((m) => m.attachments ?? []));
|
|
92
102
|
result = await agent({
|
|
93
103
|
system,
|
|
94
104
|
messages: agentMessages,
|
|
105
|
+
toolProvider,
|
|
95
106
|
onDelta: (acc) => { try {
|
|
96
107
|
ch.update(acc);
|
|
97
108
|
}
|
package/dist/runtime/types.d.ts
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
|
-
export type { ConvMessage, SystemLayer, AgentRequest, AgentResult } from '@coffer-org/plugin-claude-agent/runtime';
|
|
2
|
-
import type { ConvMessage } from '@coffer-org/plugin-claude-agent/runtime';
|
|
1
|
+
export type { AttachmentRef, AgentToolDefinition, AgentToolProvider, ConvMessage, SystemLayer, AgentRequest, AgentResult, } from '@coffer-org/plugin-claude-agent/runtime';
|
|
2
|
+
import type { AttachmentRef, ConvMessage } from '@coffer-org/plugin-claude-agent/runtime';
|
|
3
|
+
export interface AttachmentMaterializer {
|
|
4
|
+
store(bytes: Uint8Array, opts?: {
|
|
5
|
+
originalName?: string;
|
|
6
|
+
mime?: string;
|
|
7
|
+
}): Promise<AttachmentRef>;
|
|
8
|
+
}
|
|
3
9
|
export interface IncomingConversation {
|
|
4
10
|
connectorId: string;
|
|
5
11
|
chatId: string;
|
|
@@ -10,6 +16,7 @@ export interface IncomingConversation {
|
|
|
10
16
|
displayName?: string;
|
|
11
17
|
};
|
|
12
18
|
messages: ConvMessage[];
|
|
19
|
+
prepareAttachments?: (materializer: AttachmentMaterializer) => Promise<ConvMessage[]>;
|
|
13
20
|
}
|
|
14
21
|
export interface ReplyContext {
|
|
15
22
|
parentMsgId: string | null;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@coffer-org/plugin-orchestrator",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.2.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"engines": {
|
|
6
6
|
"node": ">=24"
|
|
@@ -26,8 +26,8 @@
|
|
|
26
26
|
},
|
|
27
27
|
"dependencies": {
|
|
28
28
|
"@coffer-org/sdk": "^2.1.1",
|
|
29
|
-
"@coffer-org/server": "^2.
|
|
30
|
-
"@coffer-org/plugin-claude-agent": "^2.
|
|
29
|
+
"@coffer-org/server": "^2.3.0",
|
|
30
|
+
"@coffer-org/plugin-claude-agent": "^2.5.2"
|
|
31
31
|
},
|
|
32
32
|
"coffer": {
|
|
33
33
|
"schema": "dist/schema.js"
|