@animalabs/agent-framework 0.20.0 → 0.21.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/src/framework.d.ts.map +1 -1
- package/dist/src/framework.js +6 -0
- package/dist/src/framework.js.map +1 -1
- package/dist/src/mcpl/conversation-router.d.ts +8 -6
- package/dist/src/mcpl/conversation-router.d.ts.map +1 -1
- package/dist/src/mcpl/conversation-router.js +12 -6
- package/dist/src/mcpl/conversation-router.js.map +1 -1
- package/dist/src/mcpl/references.d.ts.map +1 -1
- package/dist/src/mcpl/references.js +9 -4
- package/dist/src/mcpl/references.js.map +1 -1
- package/dist/src/mcpl/tool-lifecycle.d.ts +1 -1
- package/dist/src/mcpl/tool-lifecycle.d.ts.map +1 -1
- package/dist/src/mcpl/tool-lifecycle.js +19 -4
- package/dist/src/mcpl/tool-lifecycle.js.map +1 -1
- package/dist/src/mcpl/transport.d.ts +7 -4
- package/dist/src/mcpl/transport.d.ts.map +1 -1
- package/dist/src/mcpl/transport.js +45 -13
- package/dist/src/mcpl/transport.js.map +1 -1
- package/dist/src/mcpl/types.d.ts +3 -1
- package/dist/src/mcpl/types.d.ts.map +1 -1
- package/dist/src/mcpl/types.js.map +1 -1
- package/dist/test/conversation-router.test.js +33 -19
- package/dist/test/conversation-router.test.js.map +1 -1
- package/dist/test/conversation-routing.test.js +89 -0
- package/dist/test/conversation-routing.test.js.map +1 -1
- package/dist/test/mcpl-child-env.test.js +89 -0
- package/dist/test/mcpl-child-env.test.js.map +1 -1
- package/dist/test/mcpl-reference-fields.test.d.ts +2 -0
- package/dist/test/mcpl-reference-fields.test.d.ts.map +1 -0
- package/dist/test/mcpl-reference-fields.test.js +82 -0
- package/dist/test/mcpl-reference-fields.test.js.map +1 -0
- package/dist/test/tools-observe-parser-conformance.test.d.ts +2 -0
- package/dist/test/tools-observe-parser-conformance.test.d.ts.map +1 -0
- package/dist/test/tools-observe-parser-conformance.test.js +129 -0
- package/dist/test/tools-observe-parser-conformance.test.js.map +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +2 -2
- package/src/framework.ts +6 -0
- package/src/mcpl/conversation-router.ts +12 -8
- package/src/mcpl/references.ts +11 -3
- package/src/mcpl/tool-lifecycle.ts +19 -4
- package/src/mcpl/transport.ts +41 -9
- package/src/mcpl/types.ts +3 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@animalabs/agent-framework",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.21.1",
|
|
4
4
|
"description": "Multi-agent framework with pluggable modules, persistent state, and concurrent tool execution",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -47,7 +47,7 @@
|
|
|
47
47
|
"license": "MIT",
|
|
48
48
|
"dependencies": {
|
|
49
49
|
"@animalabs/chronicle": "^0.4.0",
|
|
50
|
-
"@animalabs/context-manager": "^0.
|
|
50
|
+
"@animalabs/context-manager": "^0.14.0",
|
|
51
51
|
"@animalabs/membrane": "^0.5.78",
|
|
52
52
|
"chokidar": "^4.0.3",
|
|
53
53
|
"discord.js": "^14.25.1",
|
package/src/framework.ts
CHANGED
|
@@ -6994,6 +6994,11 @@ export class AgentFramework {
|
|
|
6994
6994
|
if (this.conversationRouter && this.conversationAgentHomes.has(target.name)) {
|
|
6995
6995
|
metadata.triggered = event.triggerInference ?? false;
|
|
6996
6996
|
const id = target.getContextManager().addMessage('user', event.content, metadata);
|
|
6997
|
+
// A correction may target an older engagement. Refresh only the
|
|
6998
|
+
// binding whose agent actually received the message.
|
|
6999
|
+
if (this.conversationRouter.getBinding(event.channelId)?.agentName === target.name) {
|
|
7000
|
+
this.conversationRouter.touch(event.channelId);
|
|
7001
|
+
}
|
|
6997
7002
|
this.emitTrace({ type: 'message:added', messageId: id, source: 'mcpl:channel-incoming' });
|
|
6998
7003
|
if (event.triggerInference) {
|
|
6999
7004
|
this.pendingRequests.push({
|
|
@@ -7173,6 +7178,7 @@ export class AgentFramework {
|
|
|
7173
7178
|
messageMetadata.triggered = trigger;
|
|
7174
7179
|
|
|
7175
7180
|
const id = agent.getContextManager().addMessage('user', event.content, messageMetadata);
|
|
7181
|
+
router.touch(event.channelId);
|
|
7176
7182
|
this.emitTrace({ type: 'message:added', messageId: id, source: 'mcpl:channel-incoming' });
|
|
7177
7183
|
|
|
7178
7184
|
if (trigger) {
|
|
@@ -95,8 +95,6 @@ export interface IncomingMessageFacts {
|
|
|
95
95
|
mentioned: boolean;
|
|
96
96
|
/** Channel classification (see classifyChannel). */
|
|
97
97
|
kind: ChannelKind;
|
|
98
|
-
/** Clock injection for tests; defaults to Date.now(). */
|
|
99
|
-
now?: number;
|
|
100
98
|
}
|
|
101
99
|
|
|
102
100
|
export type RoutingDecision =
|
|
@@ -149,17 +147,15 @@ export class ConversationRouter {
|
|
|
149
147
|
}
|
|
150
148
|
|
|
151
149
|
/**
|
|
152
|
-
* Decide where an incoming message goes
|
|
153
|
-
*
|
|
154
|
-
*
|
|
155
|
-
*
|
|
150
|
+
* Decide where an incoming message goes without changing router state.
|
|
151
|
+
* The framework calls bind() after a fork exists and touch() after a
|
|
152
|
+
* message is delivered. Queries and failed deliveries leave idle clocks
|
|
153
|
+
* unchanged; a failed spawn doesn't leave a dangling route.
|
|
156
154
|
*/
|
|
157
155
|
route(facts: IncomingMessageFacts): RoutingDecision {
|
|
158
|
-
const now = facts.now ?? Date.now();
|
|
159
156
|
const existing = this.bindings.get(facts.channelId);
|
|
160
157
|
|
|
161
158
|
if (existing) {
|
|
162
|
-
existing.lastActivity = now;
|
|
163
159
|
return {
|
|
164
160
|
kind: 'existing',
|
|
165
161
|
agentName: existing.agentName,
|
|
@@ -202,6 +198,14 @@ export class ConversationRouter {
|
|
|
202
198
|
return binding;
|
|
203
199
|
}
|
|
204
200
|
|
|
201
|
+
/** Record delivered activity on an existing binding. Does not bind a
|
|
202
|
+
* channel or advance its generation. Call after successful delivery, not
|
|
203
|
+
* when previewing a route. The optional clock supports deterministic use. */
|
|
204
|
+
touch(channelId: string, now = Date.now()): void {
|
|
205
|
+
const binding = this.bindings.get(channelId);
|
|
206
|
+
if (binding) binding.lastActivity = now;
|
|
207
|
+
}
|
|
208
|
+
|
|
205
209
|
unbind(channelId: string): void {
|
|
206
210
|
this.bindings.delete(channelId);
|
|
207
211
|
}
|
package/src/mcpl/references.ts
CHANGED
|
@@ -56,12 +56,20 @@ function isRecord(x: unknown): x is Record<string, unknown> {
|
|
|
56
56
|
|
|
57
57
|
function takeString(
|
|
58
58
|
raw: unknown, field: keyof typeof REFERENCE_LIMITS & string,
|
|
59
|
-
out: { rejected: string[]
|
|
59
|
+
out: { rejected: string[] },
|
|
60
60
|
): string | undefined {
|
|
61
61
|
if (raw === undefined) return undefined;
|
|
62
62
|
if (typeof raw !== 'string') { out.rejected.push(field); return undefined; }
|
|
63
|
-
|
|
64
|
-
|
|
63
|
+
// JSON Schema maxLength counts Unicode code points, not UTF-16 units.
|
|
64
|
+
// Stop as soon as the optional field is invalid, without allocating a copy
|
|
65
|
+
// of an arbitrarily large server string. Display truncation happens later.
|
|
66
|
+
let length = 0;
|
|
67
|
+
for (const _codePoint of raw) {
|
|
68
|
+
if (++length > REFERENCE_LIMITS[field]) {
|
|
69
|
+
out.rejected.push(field);
|
|
70
|
+
return undefined;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
65
73
|
return raw;
|
|
66
74
|
}
|
|
67
75
|
|
|
@@ -106,7 +106,7 @@ export interface ToolLifecycleConfig {
|
|
|
106
106
|
export const DEFAULT_MAX_INPUT_BYTES = 16 * 1024;
|
|
107
107
|
|
|
108
108
|
/** Accepted sizes for a `tools/observe` request (RFC-007 §6.6 asks hosts to
|
|
109
|
-
* accept at least 64 rules, 64 paths per rule, 256-
|
|
109
|
+
* accept at least 64 rules, 64 paths per rule, 256-code-point strings). */
|
|
110
110
|
export const TOOL_OBSERVE_LIMITS = {
|
|
111
111
|
rules: 256,
|
|
112
112
|
pathsPerRule: 256,
|
|
@@ -128,6 +128,16 @@ function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
|
128
128
|
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
129
129
|
}
|
|
130
130
|
|
|
131
|
+
/** JSON Schema maxLength counts code points, not UTF-16 code units.
|
|
132
|
+
* Stop at the limit rather than allocating a copy of an oversized input. */
|
|
133
|
+
function exceedsCodePointLimit(value: string, limit: number): boolean {
|
|
134
|
+
let count = 0;
|
|
135
|
+
for (const _point of value) {
|
|
136
|
+
if (++count > limit) return true;
|
|
137
|
+
}
|
|
138
|
+
return false;
|
|
139
|
+
}
|
|
140
|
+
|
|
131
141
|
/**
|
|
132
142
|
* Validate `tools/observe` params. `rules` absent or null clears the filter
|
|
133
143
|
* (`{ok, rules: null}`); `[]` is a valid filter that reports nothing. Unknown
|
|
@@ -135,7 +145,9 @@ function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
|
135
145
|
* make a rule match more than its author wrote.
|
|
136
146
|
*/
|
|
137
147
|
export function parseToolObserveParams(params: unknown): ToolObserveParseResult {
|
|
138
|
-
|
|
148
|
+
// Omitted transport params normalize to {}, but explicit JSON null is
|
|
149
|
+
// not an object and must not clear an existing restrictive filter.
|
|
150
|
+
if (params === undefined) return { ok: true, rules: null };
|
|
139
151
|
if (!isPlainObject(params)) return { ok: false, message: 'tools/observe params must be an object' };
|
|
140
152
|
// Unknown members are rejected here too (MCP's own `_meta` excepted): a
|
|
141
153
|
// misspelled `rules` must not read as "no rules" and silently clear a
|
|
@@ -145,6 +157,9 @@ export function parseToolObserveParams(params: unknown): ToolObserveParseResult
|
|
|
145
157
|
return { ok: false, message: `tools/observe: unknown params member "${key}"` };
|
|
146
158
|
}
|
|
147
159
|
}
|
|
160
|
+
if (params._meta !== undefined && !isPlainObject(params._meta)) {
|
|
161
|
+
return { ok: false, message: 'tools/observe: _meta must be an object' };
|
|
162
|
+
}
|
|
148
163
|
const raw = params.rules;
|
|
149
164
|
if (raw === undefined || raw === null) return { ok: true, rules: null };
|
|
150
165
|
if (!Array.isArray(raw)) return { ok: false, message: 'tools/observe: rules must be an array or null' };
|
|
@@ -173,7 +188,7 @@ export function parseToolObserveParams(params: unknown): ToolObserveParseResult
|
|
|
173
188
|
continue;
|
|
174
189
|
}
|
|
175
190
|
if (typeof value !== 'string') return { ok: false, message: `tools/observe: ${at}.match.${key} must be a string` };
|
|
176
|
-
if (value
|
|
191
|
+
if (exceedsCodePointLimit(value, TOOL_OBSERVE_LIMITS.stringLength)) {
|
|
177
192
|
return { ok: false, message: `tools/observe: ${at}.match.${key} is too long`, data: { limit: 'stringLength' } };
|
|
178
193
|
}
|
|
179
194
|
(match as Record<string, string>)[key] = value;
|
|
@@ -196,7 +211,7 @@ export function parseToolObserveParams(params: unknown): ToolObserveParseResult
|
|
|
196
211
|
if (typeof path !== 'string' || path.length === 0) {
|
|
197
212
|
return { ok: false, message: `tools/observe: ${at}.input must hold non-empty strings` };
|
|
198
213
|
}
|
|
199
|
-
if (path
|
|
214
|
+
if (exceedsCodePointLimit(path, TOOL_OBSERVE_LIMITS.stringLength)) {
|
|
200
215
|
return { ok: false, message: `tools/observe: ${at}.input path is too long`, data: { limit: 'stringLength' } };
|
|
201
216
|
}
|
|
202
217
|
paths.push(path);
|
package/src/mcpl/transport.ts
CHANGED
|
@@ -104,10 +104,12 @@ export abstract class McplTransport extends EventEmitter {
|
|
|
104
104
|
/**
|
|
105
105
|
* Host variables a stdio child gets by default: just enough for an ordinary
|
|
106
106
|
* process to run (find binaries, a home dir, temp dir, locale, TLS roots,
|
|
107
|
-
* a display for GUI helpers). Everything else in the host env — notably API
|
|
107
|
+
* network proxies, a display for GUI helpers). Everything else in the host env — notably API
|
|
108
108
|
* keys and bot tokens — is NOT passed on; a server that needs a variable must
|
|
109
109
|
* declare it in its `env` (recipes can `${VAR}`-substitute from .env), or set
|
|
110
|
-
* `inheritEnv: true` to get the whole host env as before.
|
|
110
|
+
* `inheritEnv: true` to get the whole host env as before. Proxy URLs may include
|
|
111
|
+
* credentials. This limits accidental inheritance, not access by a hostile
|
|
112
|
+
* same-user child process; it is not a process isolation boundary.
|
|
111
113
|
*/
|
|
112
114
|
export const CHILD_ENV_ALLOWLIST = [
|
|
113
115
|
'PATH', 'HOME', 'USER', 'LOGNAME', 'SHELL', 'TERM', 'LANG', 'TZ',
|
|
@@ -115,24 +117,54 @@ export const CHILD_ENV_ALLOWLIST = [
|
|
|
115
117
|
'DISPLAY', 'WAYLAND_DISPLAY',
|
|
116
118
|
'XDG_RUNTIME_DIR', 'XDG_CONFIG_HOME', 'XDG_CACHE_HOME', 'XDG_DATA_HOME',
|
|
117
119
|
'SSL_CERT_FILE', 'SSL_CERT_DIR', 'NODE_EXTRA_CA_CERTS',
|
|
120
|
+
'REQUESTS_CA_BUNDLE', 'CURL_CA_BUNDLE',
|
|
121
|
+
'HTTP_PROXY', 'HTTPS_PROXY', 'NO_PROXY',
|
|
122
|
+
'http_proxy', 'https_proxy', 'no_proxy',
|
|
118
123
|
'__CF_USER_TEXT_ENCODING',
|
|
119
124
|
// Windows equivalents, harmless elsewhere.
|
|
120
125
|
'SYSTEMROOT', 'WINDIR', 'COMSPEC', 'PATHEXT', 'APPDATA', 'LOCALAPPDATA', 'USERPROFILE',
|
|
121
126
|
];
|
|
122
127
|
|
|
123
128
|
/** Environment for a stdio child: allowlisted host vars (+ LC_*), then the
|
|
124
|
-
* server's declared `env` on top.
|
|
129
|
+
* server's declared `env` on top. Windows names merge case-insensitively;
|
|
130
|
+
* POSIX names remain case-sensitive. `inheritEnv: true` restores full inheritance. */
|
|
125
131
|
export function buildChildEnv(
|
|
126
132
|
config: Pick<McplServerConfig, 'env' | 'inheritEnv'>,
|
|
127
133
|
hostEnv: NodeJS.ProcessEnv = process.env,
|
|
134
|
+
platform: NodeJS.Platform = process.platform,
|
|
128
135
|
): NodeJS.ProcessEnv {
|
|
129
|
-
|
|
130
|
-
const env: NodeJS.ProcessEnv = {};
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
136
|
+
const win = platform === 'win32';
|
|
137
|
+
const env: NodeJS.ProcessEnv = config.inheritEnv ? { ...hostEnv } : {};
|
|
138
|
+
if (!config.inheritEnv) {
|
|
139
|
+
for (const [key, value] of Object.entries(hostEnv)) {
|
|
140
|
+
if (value === undefined) continue;
|
|
141
|
+
// Windows commonly enumerates Path/SystemRoot rather than PATH/SYSTEMROOT.
|
|
142
|
+
const probe = win ? key.toUpperCase() : key;
|
|
143
|
+
if (CHILD_ENV_ALLOWLIST.includes(probe) || probe.startsWith('LC_')) env[key] = value;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
if (!win) return { ...env, ...config.env };
|
|
147
|
+
|
|
148
|
+
// Windows spawn folds environment names case-insensitively and may choose
|
|
149
|
+
// an earlier host spelling over a differently cased declared override.
|
|
150
|
+
// Select Node's first lexicographic spelling within each source, then layer
|
|
151
|
+
// declared values (including '') over host values. Source precedence and
|
|
152
|
+
// duplicate-name resolution are separate decisions.
|
|
153
|
+
const merged: NodeJS.ProcessEnv = {};
|
|
154
|
+
const spellings = new Map<string, string>();
|
|
155
|
+
for (const source of [env, config.env ?? {}]) {
|
|
156
|
+
const seen = new Set<string>();
|
|
157
|
+
for (const key of Object.keys(source).sort()) {
|
|
158
|
+
const canonical = key.toUpperCase();
|
|
159
|
+
if (seen.has(canonical)) continue;
|
|
160
|
+
seen.add(canonical);
|
|
161
|
+
const previous = spellings.get(canonical);
|
|
162
|
+
if (previous !== undefined && previous !== key) delete merged[previous];
|
|
163
|
+
merged[key] = source[key];
|
|
164
|
+
spellings.set(canonical, key);
|
|
165
|
+
}
|
|
134
166
|
}
|
|
135
|
-
return
|
|
167
|
+
return merged;
|
|
136
168
|
}
|
|
137
169
|
|
|
138
170
|
export class StdioTransport extends McplTransport {
|
package/src/mcpl/types.ts
CHANGED
|
@@ -229,7 +229,9 @@ export interface McplServerConfig {
|
|
|
229
229
|
/**
|
|
230
230
|
* Environment variables for the child process. Stdio children do NOT inherit
|
|
231
231
|
* the host environment wholesale: they get a small allowlist (PATH, HOME,
|
|
232
|
-
* LANG, LC_*, TMPDIR,
|
|
232
|
+
* LANG, LC_*, TMPDIR, TLS CA bundles, and HTTP(S) proxy settings — see
|
|
233
|
+
* CHILD_ENV_ALLOWLIST) plus exactly these. Proxy URLs can contain credentials.
|
|
234
|
+
* This limits accidental inheritance; it does not isolate a hostile child.
|
|
233
235
|
*/
|
|
234
236
|
env?: Record<string, string>;
|
|
235
237
|
|