@anteros/core 0.0.42 → 0.0.44
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/database/replication.ts +614 -26
- package/lib/agent.ts +279 -39
- package/lib/agentMemory.ts +103 -0
- package/lib/agents.ts +25 -2
- package/lib/define.ts +1 -1
- package/lib/mcpClient.ts +128 -0
- package/lib/providers.ts +169 -66
- package/lib/skills.ts +481 -0
- package/package.json +1 -1
- package/server/agents.ts +27 -6
- package/server/api.ts +74 -1
- package/tests/agent-mcp.test.ts +116 -0
- package/tests/agent-memory.test.ts +6 -6
- package/tests/agent-provider.test.ts +288 -0
- package/tests/agent-runtime.test.ts +369 -0
- package/tests/agent-skills.test.ts +177 -0
- package/tests/agent-summarize.test.ts +124 -0
- package/tests/agent-working-memory.test.ts +5 -3
- package/tests/agent.test.ts +18 -16
- package/tests/agents-api.test.ts +1 -1
- package/tests/api-private-fields.test.ts +196 -0
- package/tests/file-collection-replication.test.ts +2 -0
- package/tests/fixtures/agent-tenant/agents/support.agent.ts +1 -1
- package/tests/fixtures/agent-tenant/agents/weather.agent.ts +1 -1
- package/tests/fixtures/agents-api/agents/assistant.agent.ts +1 -1
- package/tests/fixtures/agents-api/agents/plain.agent.ts +1 -1
- package/tests/fixtures/agents-api/agents/private.agent.ts +1 -1
- package/tests/fixtures/repl-only/app.ts +3 -0
- package/tests/fixtures/replication-meta/agents/chat.agent.ts +1 -1
- package/tests/fixtures/skills-tenant/agents/docs.agent.ts +23 -0
- package/tests/fixtures/skills-tenant/agents/incomplete.agent.ts +14 -0
- package/tests/fixtures/skills-tenant/broken/bad-name.md +6 -0
- package/tests/fixtures/skills-tenant/broken/dup-a.md +6 -0
- package/tests/fixtures/skills-tenant/broken/dup-b.md +6 -0
- package/tests/fixtures/skills-tenant/broken/empty-body.md +4 -0
- package/tests/fixtures/skills-tenant/broken/long-compat.md +7 -0
- package/tests/fixtures/skills-tenant/broken/long-description.md +6 -0
- package/tests/fixtures/skills-tenant/broken/mismatch/SKILL.md +6 -0
- package/tests/fixtures/skills-tenant/broken/no-description.md +5 -0
- package/tests/fixtures/skills-tenant/broken/no-name.md +5 -0
- package/tests/fixtures/skills-tenant/broken/plain.md +4 -0
- package/tests/fixtures/skills-tenant/broken/unknown-key.md +9 -0
- package/tests/fixtures/skills-tenant/docs/pdf-forms.md +18 -0
- package/tests/fixtures/skills-tenant/guide/ml.md +9 -0
- package/tests/fixtures/skills-tenant/guides/pdf/SKILL.md +9 -0
- package/tests/fixtures/skills-tenant/runbooks/incident.md +9 -0
- package/tests/fixtures/skills-tenant/runbooks/notes.txt +2 -0
- package/tests/replication-change-stream.test.ts +240 -0
- package/tests/replication-control.test.ts +2 -0
- package/tests/replication-initial-sync.test.ts +2 -0
- package/tests/replication-meta.test.ts +3 -0
- package/tests/replication.test.ts +2 -0
- package/tests/vars-replication.test.ts +2 -0
- package/types/agent.d.ts +237 -7
- package/types/replication.d.ts +41 -4
package/lib/agents.ts
CHANGED
|
@@ -3,8 +3,10 @@ import path from "path";
|
|
|
3
3
|
import fs from "fs/promises";
|
|
4
4
|
import { cfg } from "../server/config";
|
|
5
5
|
import { Agent } from "./agent";
|
|
6
|
-
import { InMemoryAgentMemory, MongoAgentMemory, RedisAgentMemory } from "./agentMemory";
|
|
6
|
+
import { InMemoryAgentMemory, MongoAgentMemory, RedisAgentMemory, summarize } from "./agentMemory";
|
|
7
7
|
import { importDefinition } from "./load";
|
|
8
|
+
import { resolveSkills } from "./skills";
|
|
9
|
+
import { closeMcpClients } from "./mcpClient";
|
|
8
10
|
import { logger } from "../utils/logger";
|
|
9
11
|
import type { useRest } from "../database/rest";
|
|
10
12
|
import type { AgentDefinition, TenantAgents } from "../types/agent";
|
|
@@ -23,6 +25,8 @@ const definitions: AgentDefinition[] = [];
|
|
|
23
25
|
async function syncAgents(): Promise<void> {
|
|
24
26
|
registry.clear();
|
|
25
27
|
definitions.length = 0;
|
|
28
|
+
// A re-scan reconnects the remote MCP servers (they are cached per URL)
|
|
29
|
+
await closeMcpClients().catch(() => {});
|
|
26
30
|
const memories: Array<{ _tenant_: string; collection: string }> = [];
|
|
27
31
|
|
|
28
32
|
for (const tenant of cfg.tenants ?? []) {
|
|
@@ -48,7 +52,23 @@ async function syncAgents(): Promise<void> {
|
|
|
48
52
|
}
|
|
49
53
|
|
|
50
54
|
try {
|
|
51
|
-
|
|
55
|
+
// `skills` are read **once**, here: a run injects the documents into
|
|
56
|
+
// its prompt, it never touches the filesystem
|
|
57
|
+
const baseDir = path.join(process.cwd(), tenant.dir);
|
|
58
|
+
const skills = await resolveSkills(definition.skills, { baseDir, agentId: id });
|
|
59
|
+
|
|
60
|
+
// A referenced sub-agent brings its own declaration: its `skills` are
|
|
61
|
+
// resolved here too, so delegating is the same as running it directly
|
|
62
|
+
// (and still never reads a file at run time)
|
|
63
|
+
for (const referenced of definition.agents
|
|
64
|
+
? (Array.isArray(definition.agents) ? definition.agents : [definition.agents])
|
|
65
|
+
: []) {
|
|
66
|
+
if (referenced && !referenced._skills_?.length) {
|
|
67
|
+
referenced._skills_ = await resolveSkills(referenced.skills, { baseDir, agentId: referenced.id });
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const instance = new Agent({ ...definition, id, _skills_: skills }, { tenant: tenant.id });
|
|
52
72
|
registry.set(key, instance);
|
|
53
73
|
definitions.push(instance.getConfig());
|
|
54
74
|
|
|
@@ -144,6 +164,9 @@ const agents = {
|
|
|
144
164
|
InMemory: InMemoryAgentMemory,
|
|
145
165
|
Mongo: MongoAgentMemory,
|
|
146
166
|
Redis: RedisAgentMemory,
|
|
167
|
+
// A `load` processor, not a store: it compacts a thread with **your** model
|
|
168
|
+
// and prompt. Every store above is a backend; this one shapes the prompt.
|
|
169
|
+
summarize,
|
|
147
170
|
},
|
|
148
171
|
};
|
|
149
172
|
|
package/lib/define.ts
CHANGED
|
@@ -181,7 +181,7 @@ export function Workflow<TData = any>(workflow: WorkflowDefinition<TData>): Work
|
|
|
181
181
|
* id: 'weather',
|
|
182
182
|
* description: 'Answers weather questions with a live forecast.',
|
|
183
183
|
* instructions: 'You are a concise weather assistant.',
|
|
184
|
-
* provider: { model: 'gpt-4o-mini',
|
|
184
|
+
* provider: { model: 'gpt-4o-mini', protocol: 'openai' },
|
|
185
185
|
* tools: { forecast },
|
|
186
186
|
* })
|
|
187
187
|
* ```
|
package/lib/mcpClient.ts
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
2
|
+
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
|
|
3
|
+
import { logger } from "../utils/logger";
|
|
4
|
+
import type { AgentTool } from "../types/agent";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The **MCP client** — the other direction of the protocol the framework serves.
|
|
8
|
+
*
|
|
9
|
+
* An agent can consume a remote MCP server's tools (`mcp: ['https://host/mcp']`):
|
|
10
|
+
* they are listed once, adapted into ordinary `AgentTool`s, and called over the
|
|
11
|
+
* Streamable HTTP transport. The whole point is that nothing downstream knows the
|
|
12
|
+
* difference — a remote tool goes through the same validation, the same audit, the
|
|
13
|
+
* same timeouts and the same error reporting as a local one.
|
|
14
|
+
*
|
|
15
|
+
* A server is connected **lazily** (on the first run that needs it, never at boot:
|
|
16
|
+
* a remote that is down must not slow a boot down) and memoized **per server**, so
|
|
17
|
+
* a failure is retried on the next run while the servers that answered keep serving.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
/** Name we announce during `initialize` — the protocol requires one. */
|
|
21
|
+
const CLIENT_NAME = "anteros";
|
|
22
|
+
const CLIENT_VERSION = "1.0.0";
|
|
23
|
+
|
|
24
|
+
/** A remote server has to answer `initialize` + `tools/list` within this. */
|
|
25
|
+
const MCP_TIMEOUT_MS = 5_000;
|
|
26
|
+
|
|
27
|
+
/** Live clients, by URL — closed by `closeMcpClients()` (a reload, a shutdown). */
|
|
28
|
+
const clients = new Map<string, Client>();
|
|
29
|
+
/** Resolved tools per server, and the in-flight promise of one still connecting. */
|
|
30
|
+
const servers = new Map<string, { tools: AgentTool[] } | { pending: Promise<AgentTool[]> }>();
|
|
31
|
+
|
|
32
|
+
/** `['https://a/mcp']` or `{ docs: 'https://a/mcp' }` → what to connect to. */
|
|
33
|
+
function serverList(declared: string[] | Record<string, string> | undefined): Array<{ name: string; url: string }> {
|
|
34
|
+
if (!declared) return [];
|
|
35
|
+
if (Array.isArray(declared)) {
|
|
36
|
+
return declared
|
|
37
|
+
.filter((url) => typeof url === "string" && url.trim())
|
|
38
|
+
.map((url) => ({ name: String(url).trim(), url: String(url).trim() }));
|
|
39
|
+
}
|
|
40
|
+
return Object.entries(declared)
|
|
41
|
+
.filter(([, url]) => typeof url === "string" && url.trim())
|
|
42
|
+
.map(([name, url]) => ({ name, url: String(url).trim() }));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Give up on a promise after `ms` — an unreachable server must not hold a run. */
|
|
46
|
+
async function bounded<T>(work: Promise<T>, ms: number, label: string): Promise<T> {
|
|
47
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
48
|
+
try {
|
|
49
|
+
return await Promise.race([
|
|
50
|
+
work,
|
|
51
|
+
new Promise<never>((_resolve, reject) => {
|
|
52
|
+
timer = setTimeout(() => reject(new Error(`${label} did not answer within ${ms}ms`)), ms);
|
|
53
|
+
}),
|
|
54
|
+
]);
|
|
55
|
+
} finally {
|
|
56
|
+
if (timer) clearTimeout(timer);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Connect, list, and adapt — one server's worth of work. */
|
|
61
|
+
async function loadTools(name: string, url: string): Promise<AgentTool[]> {
|
|
62
|
+
const client = new Client({ name: CLIENT_NAME, version: CLIENT_VERSION }, { capabilities: {} });
|
|
63
|
+
await bounded(client.connect(new StreamableHTTPClientTransport(new URL(url))), MCP_TIMEOUT_MS, `MCP server '${name}'`);
|
|
64
|
+
|
|
65
|
+
const listed = await bounded(client.listTools(), MCP_TIMEOUT_MS, `MCP server '${name}'`);
|
|
66
|
+
clients.set(url, client);
|
|
67
|
+
|
|
68
|
+
return (listed?.tools ?? []).map((tool: any) => ({
|
|
69
|
+
id: tool.name,
|
|
70
|
+
description: tool.description ?? `Remote MCP tool '${tool.name}'.`,
|
|
71
|
+
// The protocol carries a plain JSON Schema, which `validateWithSchema` reads
|
|
72
|
+
inputSchema: tool.inputSchema ?? { type: "object", properties: {} },
|
|
73
|
+
execute: async (args: any) => {
|
|
74
|
+
const result: any = await client.callTool({ name: tool.name, arguments: args ?? {} });
|
|
75
|
+
// `serializeToolResult` already flattens MCP `{ content: [...] }`
|
|
76
|
+
if (result?.isError) throw new Error(result?.content?.[0]?.text ?? `Remote tool '${tool.name}' failed`);
|
|
77
|
+
return result;
|
|
78
|
+
},
|
|
79
|
+
})) as AgentTool[];
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* The tools of the declared servers, connected on demand. A server that fails is
|
|
84
|
+
* reported and contributes nothing (the agent keeps its own tools); the next run
|
|
85
|
+
* tries again — nothing is cached but a success.
|
|
86
|
+
*/
|
|
87
|
+
async function mcpToolsFor(declared: string[] | Record<string, string> | undefined): Promise<AgentTool[]> {
|
|
88
|
+
const out: AgentTool[] = [];
|
|
89
|
+
|
|
90
|
+
for (const { name, url } of serverList(declared)) {
|
|
91
|
+
let entry = servers.get(url);
|
|
92
|
+
if (!entry) {
|
|
93
|
+
const pending = loadTools(name, url)
|
|
94
|
+
.then((tools) => {
|
|
95
|
+
servers.set(url, { tools });
|
|
96
|
+
logger.file("info", "agent mcp: server connected", { server: name, url, tools: tools.length });
|
|
97
|
+
return tools;
|
|
98
|
+
})
|
|
99
|
+
.catch((err: any) => {
|
|
100
|
+
servers.delete(url); // a failure is not memoized: the next run retries
|
|
101
|
+
logger.file("warn", "agent mcp: server unavailable", { server: name, url, error: err?.message });
|
|
102
|
+
return [] as AgentTool[];
|
|
103
|
+
});
|
|
104
|
+
entry = { pending };
|
|
105
|
+
servers.set(url, entry);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
out.push(...("tools" in entry ? entry.tools : await entry.pending));
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return out;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** Close every live MCP client — called on a reload, and by a shutdown. */
|
|
115
|
+
async function closeMcpClients(): Promise<void> {
|
|
116
|
+
for (const client of clients.values()) {
|
|
117
|
+
try { await client.close(); } catch { /* already gone */ }
|
|
118
|
+
}
|
|
119
|
+
clients.clear();
|
|
120
|
+
servers.clear();
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export {
|
|
124
|
+
MCP_TIMEOUT_MS,
|
|
125
|
+
closeMcpClients,
|
|
126
|
+
mcpToolsFor,
|
|
127
|
+
serverList,
|
|
128
|
+
};
|