@animalabs/agent-framework 0.1.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/LICENSE +21 -0
- package/README.md +233 -0
- package/dist/src/agent.d.ts +130 -0
- package/dist/src/agent.d.ts.map +1 -0
- package/dist/src/agent.js +406 -0
- package/dist/src/agent.js.map +1 -0
- package/dist/src/api/mcp-server.d.ts +37 -0
- package/dist/src/api/mcp-server.d.ts.map +1 -0
- package/dist/src/api/mcp-server.js +598 -0
- package/dist/src/api/mcp-server.js.map +1 -0
- package/dist/src/api/server.d.ts +83 -0
- package/dist/src/api/server.d.ts.map +1 -0
- package/dist/src/api/server.js +785 -0
- package/dist/src/api/server.js.map +1 -0
- package/dist/src/api/types.d.ts +395 -0
- package/dist/src/api/types.d.ts.map +1 -0
- package/dist/src/api/types.js +5 -0
- package/dist/src/api/types.js.map +1 -0
- package/dist/src/framework.d.ts +309 -0
- package/dist/src/framework.d.ts.map +1 -0
- package/dist/src/framework.js +2309 -0
- package/dist/src/framework.js.map +1 -0
- package/dist/src/gate/event-gate.d.ts +75 -0
- package/dist/src/gate/event-gate.d.ts.map +1 -0
- package/dist/src/gate/event-gate.js +503 -0
- package/dist/src/gate/event-gate.js.map +1 -0
- package/dist/src/gate/index.d.ts +3 -0
- package/dist/src/gate/index.d.ts.map +1 -0
- package/dist/src/gate/index.js +2 -0
- package/dist/src/gate/index.js.map +1 -0
- package/dist/src/gate/types.d.ts +82 -0
- package/dist/src/gate/types.d.ts.map +1 -0
- package/dist/src/gate/types.js +9 -0
- package/dist/src/gate/types.js.map +1 -0
- package/dist/src/index.d.ts +17 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +18 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/mcpl/channel-registry.d.ts +141 -0
- package/dist/src/mcpl/channel-registry.d.ts.map +1 -0
- package/dist/src/mcpl/channel-registry.js +647 -0
- package/dist/src/mcpl/channel-registry.js.map +1 -0
- package/dist/src/mcpl/checkpoint-manager.d.ts +79 -0
- package/dist/src/mcpl/checkpoint-manager.d.ts.map +1 -0
- package/dist/src/mcpl/checkpoint-manager.js +430 -0
- package/dist/src/mcpl/checkpoint-manager.js.map +1 -0
- package/dist/src/mcpl/errors.d.ts +52 -0
- package/dist/src/mcpl/errors.d.ts.map +1 -0
- package/dist/src/mcpl/errors.js +93 -0
- package/dist/src/mcpl/errors.js.map +1 -0
- package/dist/src/mcpl/feature-set-manager.d.ts +91 -0
- package/dist/src/mcpl/feature-set-manager.d.ts.map +1 -0
- package/dist/src/mcpl/feature-set-manager.js +250 -0
- package/dist/src/mcpl/feature-set-manager.js.map +1 -0
- package/dist/src/mcpl/hook-orchestrator.d.ts +51 -0
- package/dist/src/mcpl/hook-orchestrator.d.ts.map +1 -0
- package/dist/src/mcpl/hook-orchestrator.js +217 -0
- package/dist/src/mcpl/hook-orchestrator.js.map +1 -0
- package/dist/src/mcpl/index.d.ts +14 -0
- package/dist/src/mcpl/index.d.ts.map +1 -0
- package/dist/src/mcpl/index.js +21 -0
- package/dist/src/mcpl/index.js.map +1 -0
- package/dist/src/mcpl/inference-router.d.ts +67 -0
- package/dist/src/mcpl/inference-router.d.ts.map +1 -0
- package/dist/src/mcpl/inference-router.js +249 -0
- package/dist/src/mcpl/inference-router.js.map +1 -0
- package/dist/src/mcpl/push-handler.d.ts +60 -0
- package/dist/src/mcpl/push-handler.d.ts.map +1 -0
- package/dist/src/mcpl/push-handler.js +162 -0
- package/dist/src/mcpl/push-handler.js.map +1 -0
- package/dist/src/mcpl/scope-manager.d.ts +76 -0
- package/dist/src/mcpl/scope-manager.d.ts.map +1 -0
- package/dist/src/mcpl/scope-manager.js +213 -0
- package/dist/src/mcpl/scope-manager.js.map +1 -0
- package/dist/src/mcpl/server-connection.d.ts +174 -0
- package/dist/src/mcpl/server-connection.d.ts.map +1 -0
- package/dist/src/mcpl/server-connection.js +559 -0
- package/dist/src/mcpl/server-connection.js.map +1 -0
- package/dist/src/mcpl/server-registry.d.ts +63 -0
- package/dist/src/mcpl/server-registry.d.ts.map +1 -0
- package/dist/src/mcpl/server-registry.js +119 -0
- package/dist/src/mcpl/server-registry.js.map +1 -0
- package/dist/src/mcpl/types.d.ts +755 -0
- package/dist/src/mcpl/types.d.ts.map +1 -0
- package/dist/src/mcpl/types.js +48 -0
- package/dist/src/mcpl/types.js.map +1 -0
- package/dist/src/module-registry.d.ts +93 -0
- package/dist/src/module-registry.d.ts.map +1 -0
- package/dist/src/module-registry.js +386 -0
- package/dist/src/module-registry.js.map +1 -0
- package/dist/src/modules/api/index.d.ts +22 -0
- package/dist/src/modules/api/index.d.ts.map +1 -0
- package/dist/src/modules/api/index.js +63 -0
- package/dist/src/modules/api/index.js.map +1 -0
- package/dist/src/modules/discord/discord-js-client.d.ts +58 -0
- package/dist/src/modules/discord/discord-js-client.d.ts.map +1 -0
- package/dist/src/modules/discord/discord-js-client.js +273 -0
- package/dist/src/modules/discord/discord-js-client.js.map +1 -0
- package/dist/src/modules/discord/index.d.ts +82 -0
- package/dist/src/modules/discord/index.d.ts.map +1 -0
- package/dist/src/modules/discord/index.js +959 -0
- package/dist/src/modules/discord/index.js.map +1 -0
- package/dist/src/modules/discord/types.d.ts +327 -0
- package/dist/src/modules/discord/types.d.ts.map +1 -0
- package/dist/src/modules/discord/types.js +5 -0
- package/dist/src/modules/discord/types.js.map +1 -0
- package/dist/src/modules/index.d.ts +11 -0
- package/dist/src/modules/index.d.ts.map +1 -0
- package/dist/src/modules/index.js +7 -0
- package/dist/src/modules/index.js.map +1 -0
- package/dist/src/modules/workspace/index.d.ts +59 -0
- package/dist/src/modules/workspace/index.d.ts.map +1 -0
- package/dist/src/modules/workspace/index.js +831 -0
- package/dist/src/modules/workspace/index.js.map +1 -0
- package/dist/src/modules/workspace/sync.d.ts +48 -0
- package/dist/src/modules/workspace/sync.d.ts.map +1 -0
- package/dist/src/modules/workspace/sync.js +253 -0
- package/dist/src/modules/workspace/sync.js.map +1 -0
- package/dist/src/modules/workspace/types.d.ts +166 -0
- package/dist/src/modules/workspace/types.d.ts.map +1 -0
- package/dist/src/modules/workspace/types.js +5 -0
- package/dist/src/modules/workspace/types.js.map +1 -0
- package/dist/src/modules/workspace/watcher.d.ts +43 -0
- package/dist/src/modules/workspace/watcher.d.ts.map +1 -0
- package/dist/src/modules/workspace/watcher.js +120 -0
- package/dist/src/modules/workspace/watcher.js.map +1 -0
- package/dist/src/queue.d.ts +50 -0
- package/dist/src/queue.d.ts.map +1 -0
- package/dist/src/queue.js +95 -0
- package/dist/src/queue.js.map +1 -0
- package/dist/src/safe-slice.d.ts +8 -0
- package/dist/src/safe-slice.d.ts.map +1 -0
- package/dist/src/safe-slice.js +18 -0
- package/dist/src/safe-slice.js.map +1 -0
- package/dist/src/types/agent.d.ts +118 -0
- package/dist/src/types/agent.d.ts.map +1 -0
- package/dist/src/types/agent.js +2 -0
- package/dist/src/types/agent.js.map +1 -0
- package/dist/src/types/events.d.ts +220 -0
- package/dist/src/types/events.d.ts.map +1 -0
- package/dist/src/types/events.js +2 -0
- package/dist/src/types/events.js.map +1 -0
- package/dist/src/types/framework.d.ts +184 -0
- package/dist/src/types/framework.d.ts.map +1 -0
- package/dist/src/types/framework.js +2 -0
- package/dist/src/types/framework.js.map +1 -0
- package/dist/src/types/index.d.ts +7 -0
- package/dist/src/types/index.d.ts.map +1 -0
- package/dist/src/types/index.js +7 -0
- package/dist/src/types/index.js.map +1 -0
- package/dist/src/types/inference-log.d.ts +64 -0
- package/dist/src/types/inference-log.d.ts.map +1 -0
- package/dist/src/types/inference-log.js +5 -0
- package/dist/src/types/inference-log.js.map +1 -0
- package/dist/src/types/module.d.ts +302 -0
- package/dist/src/types/module.d.ts.map +1 -0
- package/dist/src/types/module.js +2 -0
- package/dist/src/types/module.js.map +1 -0
- package/dist/src/types/trace.d.ts +173 -0
- package/dist/src/types/trace.d.ts.map +1 -0
- package/dist/src/types/trace.js +2 -0
- package/dist/src/types/trace.js.map +1 -0
- package/dist/test/event-gate.test.d.ts +2 -0
- package/dist/test/event-gate.test.d.ts.map +1 -0
- package/dist/test/event-gate.test.js +535 -0
- package/dist/test/event-gate.test.js.map +1 -0
- package/dist/test/framework.test.d.ts +5 -0
- package/dist/test/framework.test.d.ts.map +1 -0
- package/dist/test/framework.test.js +738 -0
- package/dist/test/framework.test.js.map +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -0
- package/package.json +58 -0
- package/src/agent.ts +512 -0
- package/src/api/mcp-server.ts +687 -0
- package/src/api/server.ts +1023 -0
- package/src/api/types.ts +542 -0
- package/src/framework.ts +2784 -0
- package/src/gate/event-gate.ts +623 -0
- package/src/gate/index.ts +12 -0
- package/src/gate/types.ts +112 -0
- package/src/index.ts +63 -0
- package/src/mcpl/channel-registry.ts +824 -0
- package/src/mcpl/checkpoint-manager.ts +545 -0
- package/src/mcpl/errors.ts +114 -0
- package/src/mcpl/feature-set-manager.ts +333 -0
- package/src/mcpl/hook-orchestrator.ts +281 -0
- package/src/mcpl/index.ts +134 -0
- package/src/mcpl/inference-router.ts +335 -0
- package/src/mcpl/push-handler.ts +232 -0
- package/src/mcpl/scope-manager.ts +262 -0
- package/src/mcpl/server-connection.ts +694 -0
- package/src/mcpl/server-registry.ts +144 -0
- package/src/mcpl/types.ts +991 -0
- package/src/module-registry.ts +547 -0
- package/src/modules/api/index.ts +94 -0
- package/src/modules/discord/discord-js-client.ts +353 -0
- package/src/modules/discord/index.ts +1146 -0
- package/src/modules/discord/types.ts +465 -0
- package/src/modules/index.ts +43 -0
- package/src/modules/workspace/index.ts +969 -0
- package/src/modules/workspace/sync.ts +314 -0
- package/src/modules/workspace/types.ts +202 -0
- package/src/modules/workspace/watcher.ts +142 -0
- package/src/queue.ts +106 -0
- package/src/safe-slice.ts +18 -0
- package/src/types/agent.ts +119 -0
- package/src/types/events.ts +252 -0
- package/src/types/framework.ts +211 -0
- package/src/types/index.ts +6 -0
- package/src/types/inference-log.ts +64 -0
- package/src/types/module.ts +367 -0
- package/src/types/trace.ts +196 -0
|
@@ -0,0 +1,623 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* EventGate — file-driven inference gating with per-policy debounce.
|
|
3
|
+
*
|
|
4
|
+
* Reads policies from a `gate.json` config file. Policies are evaluated in
|
|
5
|
+
* order — first match wins. Each policy specifies a behavior: "always"
|
|
6
|
+
* (trigger immediately), "skip" (don't trigger), or { debounce: ms }
|
|
7
|
+
* (batch events per-policy, deliver when timer fires).
|
|
8
|
+
*
|
|
9
|
+
* The gate controls inference triggering only. Events always enter context
|
|
10
|
+
* and are always dispatched to modules — the gate decides whether to spend
|
|
11
|
+
* inference tokens, not whether the agent should know about the event.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { existsSync, readFileSync, statSync, writeFileSync, mkdirSync } from 'node:fs';
|
|
15
|
+
import { dirname } from 'node:path';
|
|
16
|
+
|
|
17
|
+
import type { ToolDefinition } from '../types/events.js';
|
|
18
|
+
import type {
|
|
19
|
+
GateConfig,
|
|
20
|
+
GatePolicy,
|
|
21
|
+
GatePolicyMatch,
|
|
22
|
+
GateBehavior,
|
|
23
|
+
GateDecision,
|
|
24
|
+
GateEventInfo,
|
|
25
|
+
GatePolicyStats,
|
|
26
|
+
GateStatus,
|
|
27
|
+
} from './types.js';
|
|
28
|
+
|
|
29
|
+
// Re-export types consumers need
|
|
30
|
+
export type { GateConfig, GateOptions, GateDecision, GateEventInfo } from './types.js';
|
|
31
|
+
|
|
32
|
+
// ---------------------------------------------------------------------------
|
|
33
|
+
// Constants
|
|
34
|
+
// ---------------------------------------------------------------------------
|
|
35
|
+
|
|
36
|
+
/** Max content length stored in pending debounce events. */
|
|
37
|
+
const MAX_CONTENT_SNIPPET = 200;
|
|
38
|
+
/** Max content length shown in onWake trace summaries. */
|
|
39
|
+
const MAX_TRACE_SNIPPET = 80;
|
|
40
|
+
/** Max events buffered during inference before oldest are dropped. */
|
|
41
|
+
const MAX_INFERENCE_BUFFER = 100;
|
|
42
|
+
/** Minimum interval between filesystem checks for config changes (ms). */
|
|
43
|
+
const RELOAD_THROTTLE_MS = 1000;
|
|
44
|
+
/** Minimum debounce value (ms). */
|
|
45
|
+
const MIN_DEBOUNCE_MS = 100;
|
|
46
|
+
/** Maximum debounce value (ms). */
|
|
47
|
+
const MAX_DEBOUNCE_MS = 300_000;
|
|
48
|
+
|
|
49
|
+
// ---------------------------------------------------------------------------
|
|
50
|
+
// Default config
|
|
51
|
+
// ---------------------------------------------------------------------------
|
|
52
|
+
|
|
53
|
+
const DEFAULT_CONFIG: GateConfig = {
|
|
54
|
+
policies: [],
|
|
55
|
+
default: 'always',
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
// ---------------------------------------------------------------------------
|
|
59
|
+
// Internal types
|
|
60
|
+
// ---------------------------------------------------------------------------
|
|
61
|
+
|
|
62
|
+
interface PendingEvent {
|
|
63
|
+
policyName: string;
|
|
64
|
+
content: string;
|
|
65
|
+
eventType: string;
|
|
66
|
+
timestamp: number;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
interface DebounceState {
|
|
70
|
+
timer: ReturnType<typeof setTimeout>;
|
|
71
|
+
events: PendingEvent[];
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
interface CompiledPolicy {
|
|
75
|
+
policy: GatePolicy;
|
|
76
|
+
filterRegex?: RegExp;
|
|
77
|
+
sourceRegex?: RegExp;
|
|
78
|
+
channelRegex?: RegExp;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
interface PolicyStats {
|
|
82
|
+
matchCount: number;
|
|
83
|
+
lastMatchTimestamp: number | null;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// ---------------------------------------------------------------------------
|
|
87
|
+
// Trace event shape (subset — avoids importing the full TraceEvent union)
|
|
88
|
+
// ---------------------------------------------------------------------------
|
|
89
|
+
|
|
90
|
+
interface TraceEventLike {
|
|
91
|
+
type: string;
|
|
92
|
+
[key: string]: unknown;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// ---------------------------------------------------------------------------
|
|
96
|
+
// Glob matching (simple * wildcards)
|
|
97
|
+
// ---------------------------------------------------------------------------
|
|
98
|
+
|
|
99
|
+
function compileGlob(pattern: string): RegExp {
|
|
100
|
+
// Escape everything except *, then replace * with .*
|
|
101
|
+
const escaped = pattern.replace(/[.+?^${}()|[\]\\]/g, '\\$&');
|
|
102
|
+
if (!pattern.includes('*')) {
|
|
103
|
+
return new RegExp('^' + escaped + '$');
|
|
104
|
+
}
|
|
105
|
+
return new RegExp('^' + escaped.replace(/\*/g, '.*') + '$');
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// ---------------------------------------------------------------------------
|
|
109
|
+
// Config validation
|
|
110
|
+
// ---------------------------------------------------------------------------
|
|
111
|
+
|
|
112
|
+
function validateConfig(raw: unknown): GateConfig {
|
|
113
|
+
if (!raw || typeof raw !== 'object') {
|
|
114
|
+
throw new Error('gate.json must be a JSON object');
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const obj = raw as Record<string, unknown>;
|
|
118
|
+
|
|
119
|
+
const defaultBehavior = obj.default ?? 'always';
|
|
120
|
+
if (defaultBehavior !== 'always' && defaultBehavior !== 'skip') {
|
|
121
|
+
throw new Error(`gate.json "default" must be "always" or "skip", got: ${defaultBehavior}`);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const policies: GatePolicy[] = [];
|
|
125
|
+
if (Array.isArray(obj.policies)) {
|
|
126
|
+
for (const p of obj.policies) {
|
|
127
|
+
policies.push(validatePolicy(p));
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return { policies, default: defaultBehavior };
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function validatePolicy(raw: unknown): GatePolicy {
|
|
135
|
+
if (!raw || typeof raw !== 'object') {
|
|
136
|
+
throw new Error('Each gate policy must be an object');
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const obj = raw as Record<string, unknown>;
|
|
140
|
+
|
|
141
|
+
if (typeof obj.name !== 'string' || !obj.name) {
|
|
142
|
+
throw new Error('Gate policy must have a "name" string');
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Validate behavior
|
|
146
|
+
let behavior: GateBehavior;
|
|
147
|
+
if (obj.behavior === 'always' || obj.behavior === 'skip') {
|
|
148
|
+
behavior = obj.behavior;
|
|
149
|
+
} else if (obj.behavior && typeof obj.behavior === 'object') {
|
|
150
|
+
const b = obj.behavior as Record<string, unknown>;
|
|
151
|
+
if (typeof b.debounce === 'number' && b.debounce > 0) {
|
|
152
|
+
if (b.debounce < MIN_DEBOUNCE_MS) {
|
|
153
|
+
throw new Error(`Policy "${obj.name}": debounce must be >= ${MIN_DEBOUNCE_MS}ms, got ${b.debounce}`);
|
|
154
|
+
}
|
|
155
|
+
if (b.debounce > MAX_DEBOUNCE_MS) {
|
|
156
|
+
throw new Error(`Policy "${obj.name}": debounce must be <= ${MAX_DEBOUNCE_MS}ms, got ${b.debounce}`);
|
|
157
|
+
}
|
|
158
|
+
behavior = { debounce: b.debounce };
|
|
159
|
+
} else {
|
|
160
|
+
throw new Error(`Policy "${obj.name}": debounce must be a positive number`);
|
|
161
|
+
}
|
|
162
|
+
} else {
|
|
163
|
+
behavior = 'always';
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Validate match (lenient — missing fields mean "match all")
|
|
167
|
+
const match: GatePolicyMatch = {};
|
|
168
|
+
if (obj.match && typeof obj.match === 'object') {
|
|
169
|
+
const m = obj.match as Record<string, unknown>;
|
|
170
|
+
if (Array.isArray(m.scope)) match.scope = m.scope.filter(s => typeof s === 'string');
|
|
171
|
+
if (typeof m.source === 'string') match.source = m.source;
|
|
172
|
+
if (typeof m.channel === 'string') match.channel = m.channel;
|
|
173
|
+
if (m.filter && typeof m.filter === 'object') {
|
|
174
|
+
const f = m.filter as Record<string, unknown>;
|
|
175
|
+
if ((f.type === 'text' || f.type === 'regex') && typeof f.pattern === 'string') {
|
|
176
|
+
match.filter = { type: f.type, pattern: f.pattern };
|
|
177
|
+
if (f.type === 'regex') {
|
|
178
|
+
try { new RegExp(f.pattern as string); } catch (e) {
|
|
179
|
+
throw new Error(`Policy "${obj.name}": invalid regex pattern: ${e}`);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return { name: obj.name, match, behavior };
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// ---------------------------------------------------------------------------
|
|
190
|
+
// EventGate
|
|
191
|
+
// ---------------------------------------------------------------------------
|
|
192
|
+
|
|
193
|
+
export class EventGate {
|
|
194
|
+
private configPath: string;
|
|
195
|
+
private config: GateConfig;
|
|
196
|
+
private compiledPolicies: CompiledPolicy[] = [];
|
|
197
|
+
private configMtime: number = 0;
|
|
198
|
+
private lastReloadCheck: number = 0;
|
|
199
|
+
private configSource: 'file' | 'initial' | 'default' = 'default';
|
|
200
|
+
private lastReloadTimestamp: number | null = null;
|
|
201
|
+
private configErrors: string[] = [];
|
|
202
|
+
|
|
203
|
+
// Debounce state
|
|
204
|
+
private debounceTimers = new Map<string, DebounceState>();
|
|
205
|
+
|
|
206
|
+
// Inference buffering
|
|
207
|
+
private inferring = new Set<string>();
|
|
208
|
+
private inferenceBuffer: PendingEvent[] = [];
|
|
209
|
+
|
|
210
|
+
// Per-policy stats
|
|
211
|
+
private stats = new Map<string, PolicyStats>();
|
|
212
|
+
|
|
213
|
+
// Dependency-injected callbacks
|
|
214
|
+
private emitTrace: (event: TraceEventLike) => void;
|
|
215
|
+
private addMessageFn: (participant: string, content: Array<{ type: 'text'; text: string }>, metadata?: Record<string, unknown>) => unknown;
|
|
216
|
+
private requestInferenceFn: (agentName: string, reason: string, source: string) => void;
|
|
217
|
+
private getAgentNamesFn: () => string[];
|
|
218
|
+
|
|
219
|
+
constructor(opts: {
|
|
220
|
+
configPath: string;
|
|
221
|
+
initialConfig?: GateConfig;
|
|
222
|
+
emitTrace: (event: TraceEventLike) => void;
|
|
223
|
+
addMessage: (participant: string, content: Array<{ type: 'text'; text: string }>, metadata?: Record<string, unknown>) => unknown;
|
|
224
|
+
requestInference: (agentName: string, reason: string, source: string) => void;
|
|
225
|
+
getAgentNames: () => string[];
|
|
226
|
+
}) {
|
|
227
|
+
this.configPath = opts.configPath;
|
|
228
|
+
this.emitTrace = opts.emitTrace;
|
|
229
|
+
this.addMessageFn = opts.addMessage;
|
|
230
|
+
this.requestInferenceFn = opts.requestInference;
|
|
231
|
+
this.getAgentNamesFn = opts.getAgentNames;
|
|
232
|
+
|
|
233
|
+
// Seed config file if it doesn't exist
|
|
234
|
+
if (opts.initialConfig && !existsSync(this.configPath)) {
|
|
235
|
+
try {
|
|
236
|
+
mkdirSync(dirname(this.configPath), { recursive: true });
|
|
237
|
+
writeFileSync(this.configPath, JSON.stringify(opts.initialConfig, null, 2) + '\n');
|
|
238
|
+
} catch (err) {
|
|
239
|
+
this.configErrors.push(`Failed to seed gate.json: ${err}`);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Load config
|
|
244
|
+
this.config = this.loadConfig() ?? opts.initialConfig ?? { ...DEFAULT_CONFIG };
|
|
245
|
+
this.compiledPolicies = this.compilePolicies(this.config);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// =========================================================================
|
|
249
|
+
// Config loading
|
|
250
|
+
// =========================================================================
|
|
251
|
+
|
|
252
|
+
private loadConfig(): GateConfig | null {
|
|
253
|
+
if (!existsSync(this.configPath)) {
|
|
254
|
+
return null;
|
|
255
|
+
}
|
|
256
|
+
try {
|
|
257
|
+
const raw = readFileSync(this.configPath, 'utf-8');
|
|
258
|
+
const stat = statSync(this.configPath);
|
|
259
|
+
this.configMtime = stat.mtimeMs;
|
|
260
|
+
const config = validateConfig(JSON.parse(raw));
|
|
261
|
+
this.configSource = 'file';
|
|
262
|
+
this.configErrors = [];
|
|
263
|
+
return config;
|
|
264
|
+
} catch (err) {
|
|
265
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
266
|
+
this.configErrors = [msg];
|
|
267
|
+
this.emitTrace({
|
|
268
|
+
type: 'gate:config-error',
|
|
269
|
+
error: msg,
|
|
270
|
+
configPath: this.configPath,
|
|
271
|
+
timestamp: Date.now(),
|
|
272
|
+
});
|
|
273
|
+
return null; // Caller keeps previous config
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
private compilePolicies(config: GateConfig): CompiledPolicy[] {
|
|
278
|
+
return config.policies.map(policy => {
|
|
279
|
+
const compiled: CompiledPolicy = { policy };
|
|
280
|
+
if (policy.match.filter?.type === 'regex') {
|
|
281
|
+
try { compiled.filterRegex = new RegExp(policy.match.filter.pattern, 'i'); } catch { /* skip */ }
|
|
282
|
+
}
|
|
283
|
+
if (policy.match.source) {
|
|
284
|
+
compiled.sourceRegex = compileGlob(policy.match.source);
|
|
285
|
+
}
|
|
286
|
+
if (policy.match.channel) {
|
|
287
|
+
compiled.channelRegex = compileGlob(policy.match.channel);
|
|
288
|
+
}
|
|
289
|
+
return compiled;
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
private reloadIfChanged(): void {
|
|
294
|
+
const now = Date.now();
|
|
295
|
+
if (now - this.lastReloadCheck < RELOAD_THROTTLE_MS) return;
|
|
296
|
+
this.lastReloadCheck = now;
|
|
297
|
+
|
|
298
|
+
try {
|
|
299
|
+
if (!existsSync(this.configPath)) return;
|
|
300
|
+
const stat = statSync(this.configPath);
|
|
301
|
+
if (stat.mtimeMs === this.configMtime) return;
|
|
302
|
+
|
|
303
|
+
const newConfig = this.loadConfig();
|
|
304
|
+
if (!newConfig) return; // Parse error — keep previous config
|
|
305
|
+
|
|
306
|
+
// Clean up debounce timers for removed policies
|
|
307
|
+
const newPolicyNames = new Set(newConfig.policies.map(p => p.name));
|
|
308
|
+
for (const [name, state] of this.debounceTimers) {
|
|
309
|
+
if (!newPolicyNames.has(name)) {
|
|
310
|
+
clearTimeout(state.timer);
|
|
311
|
+
if (state.events.length > 0) {
|
|
312
|
+
this.deliverEvents(state.events);
|
|
313
|
+
}
|
|
314
|
+
this.debounceTimers.delete(name);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// Reset stats for removed policies
|
|
319
|
+
for (const name of this.stats.keys()) {
|
|
320
|
+
if (!newPolicyNames.has(name)) {
|
|
321
|
+
this.stats.delete(name);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
this.config = newConfig;
|
|
326
|
+
this.compiledPolicies = this.compilePolicies(newConfig);
|
|
327
|
+
this.lastReloadTimestamp = now;
|
|
328
|
+
|
|
329
|
+
this.emitTrace({
|
|
330
|
+
type: 'gate:config-reloaded',
|
|
331
|
+
configPath: this.configPath,
|
|
332
|
+
policyCount: newConfig.policies.length,
|
|
333
|
+
timestamp: now,
|
|
334
|
+
});
|
|
335
|
+
} catch {
|
|
336
|
+
// Ignore stat errors
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// =========================================================================
|
|
341
|
+
// Policy matching — first match wins
|
|
342
|
+
// =========================================================================
|
|
343
|
+
|
|
344
|
+
private matchPolicy(info: GateEventInfo): GatePolicy | null {
|
|
345
|
+
for (const compiled of this.compiledPolicies) {
|
|
346
|
+
if (this.compiledMatches(compiled, info)) {
|
|
347
|
+
return compiled.policy;
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
return null;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
private compiledMatches(compiled: CompiledPolicy, info: GateEventInfo): boolean {
|
|
354
|
+
const match = compiled.policy.match;
|
|
355
|
+
|
|
356
|
+
// Scope check
|
|
357
|
+
if (match.scope && match.scope.length > 0 && !match.scope.includes(info.eventType)) {
|
|
358
|
+
return false;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// Source check (serverId)
|
|
362
|
+
if (compiled.sourceRegex && !compiled.sourceRegex.test(info.serverId)) {
|
|
363
|
+
return false;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// Channel check
|
|
367
|
+
if (compiled.channelRegex && !compiled.channelRegex.test(info.channelId)) {
|
|
368
|
+
return false;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// Content filter check
|
|
372
|
+
if (match.filter) {
|
|
373
|
+
if (match.filter.type === 'text') {
|
|
374
|
+
if (!info.content.toLowerCase().includes(match.filter.pattern.toLowerCase())) {
|
|
375
|
+
return false;
|
|
376
|
+
}
|
|
377
|
+
} else if (compiled.filterRegex) {
|
|
378
|
+
if (!compiled.filterRegex.test(info.content)) {
|
|
379
|
+
return false;
|
|
380
|
+
}
|
|
381
|
+
} else {
|
|
382
|
+
return false; // Regex failed to compile
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
return true;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// =========================================================================
|
|
390
|
+
// Evaluate — main entry point
|
|
391
|
+
// =========================================================================
|
|
392
|
+
|
|
393
|
+
evaluate(info: GateEventInfo): GateDecision {
|
|
394
|
+
this.reloadIfChanged();
|
|
395
|
+
|
|
396
|
+
const policy = this.matchPolicy(info);
|
|
397
|
+
|
|
398
|
+
if (!policy) {
|
|
399
|
+
// No policy matched — use default
|
|
400
|
+
const trigger = (this.config.default ?? 'always') === 'always';
|
|
401
|
+
return { trigger, policyName: null, behavior: this.config.default ?? 'always' };
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// Record stats
|
|
405
|
+
const policyStats = this.stats.get(policy.name) ?? { matchCount: 0, lastMatchTimestamp: null };
|
|
406
|
+
policyStats.matchCount++;
|
|
407
|
+
policyStats.lastMatchTimestamp = Date.now();
|
|
408
|
+
this.stats.set(policy.name, policyStats);
|
|
409
|
+
|
|
410
|
+
// Emit trace
|
|
411
|
+
this.emitTrace({
|
|
412
|
+
type: 'gate:policy-matched',
|
|
413
|
+
policyName: policy.name,
|
|
414
|
+
behavior: typeof policy.behavior === 'object' ? `debounce:${policy.behavior.debounce}` : policy.behavior,
|
|
415
|
+
eventType: info.eventType,
|
|
416
|
+
source: info.serverId || undefined,
|
|
417
|
+
timestamp: Date.now(),
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
if (policy.behavior === 'skip') {
|
|
421
|
+
return { trigger: false, policyName: policy.name, behavior: 'skip' };
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
if (policy.behavior === 'always') {
|
|
425
|
+
return { trigger: true, policyName: policy.name, behavior: 'always' };
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
// Debounce
|
|
429
|
+
this.handleDebounce(policy, info);
|
|
430
|
+
return { trigger: false, policyName: policy.name, behavior: policy.behavior };
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
// =========================================================================
|
|
434
|
+
// shouldTriggerInference callback adapter
|
|
435
|
+
// =========================================================================
|
|
436
|
+
|
|
437
|
+
/**
|
|
438
|
+
* Returns a callback compatible with McplServerConfig.shouldTriggerInference.
|
|
439
|
+
* Used by the framework to wire the gate into PushHandler and ChannelRegistry.
|
|
440
|
+
*/
|
|
441
|
+
asShouldTriggerCallback(): (content: string, metadata: Record<string, unknown>) => boolean {
|
|
442
|
+
return (content: string, metadata: Record<string, unknown>): boolean => {
|
|
443
|
+
const decision = this.evaluate({
|
|
444
|
+
content,
|
|
445
|
+
eventType: (metadata.eventType as string) ?? 'unknown',
|
|
446
|
+
serverId: (metadata.serverId as string) ?? '',
|
|
447
|
+
channelId: (metadata.channelId as string) ?? '',
|
|
448
|
+
metadata,
|
|
449
|
+
});
|
|
450
|
+
return decision.trigger;
|
|
451
|
+
};
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
// =========================================================================
|
|
455
|
+
// Debounce
|
|
456
|
+
// =========================================================================
|
|
457
|
+
|
|
458
|
+
private handleDebounce(policy: GatePolicy, info: GateEventInfo): void {
|
|
459
|
+
const debounceMs = (policy.behavior as { debounce: number }).debounce;
|
|
460
|
+
|
|
461
|
+
const event: PendingEvent = {
|
|
462
|
+
policyName: policy.name,
|
|
463
|
+
content: info.content.length > MAX_CONTENT_SNIPPET
|
|
464
|
+
? info.content.slice(0, MAX_CONTENT_SNIPPET) + '...'
|
|
465
|
+
: info.content,
|
|
466
|
+
eventType: info.eventType,
|
|
467
|
+
timestamp: Date.now(),
|
|
468
|
+
};
|
|
469
|
+
|
|
470
|
+
const existing = this.debounceTimers.get(policy.name);
|
|
471
|
+
if (existing) {
|
|
472
|
+
clearTimeout(existing.timer);
|
|
473
|
+
existing.events.push(event);
|
|
474
|
+
existing.timer = setTimeout(() => this.fireDebounce(policy.name), debounceMs);
|
|
475
|
+
} else {
|
|
476
|
+
const timer = setTimeout(() => this.fireDebounce(policy.name), debounceMs);
|
|
477
|
+
this.debounceTimers.set(policy.name, { timer, events: [event] });
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
private fireDebounce(policyName: string): void {
|
|
482
|
+
const state = this.debounceTimers.get(policyName);
|
|
483
|
+
if (!state || state.events.length === 0) {
|
|
484
|
+
this.debounceTimers.delete(policyName);
|
|
485
|
+
return;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
const events = state.events;
|
|
489
|
+
this.debounceTimers.delete(policyName);
|
|
490
|
+
|
|
491
|
+
// If any agent is currently inferring, buffer for later (with cap)
|
|
492
|
+
if (this.inferring.size > 0) {
|
|
493
|
+
this.bufferForInference(events);
|
|
494
|
+
return;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
this.deliverEvents(events);
|
|
498
|
+
|
|
499
|
+
this.emitTrace({
|
|
500
|
+
type: 'gate:debounce-delivered',
|
|
501
|
+
policyName,
|
|
502
|
+
eventCount: events.length,
|
|
503
|
+
timestamp: Date.now(),
|
|
504
|
+
});
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
// =========================================================================
|
|
508
|
+
// Event delivery
|
|
509
|
+
// =========================================================================
|
|
510
|
+
|
|
511
|
+
private deliverEvents(events: PendingEvent[]): void {
|
|
512
|
+
if (events.length === 0) return;
|
|
513
|
+
|
|
514
|
+
const policyNames = [...new Set(events.map(e => e.policyName))];
|
|
515
|
+
const lines = events
|
|
516
|
+
.map(e => `- [${e.policyName}] (${e.eventType}): ${e.content}`)
|
|
517
|
+
.join('\n');
|
|
518
|
+
|
|
519
|
+
const text = `[Gate: ${events.length} event${events.length > 1 ? 's' : ''} matched]\n\n${lines}`;
|
|
520
|
+
|
|
521
|
+
this.addMessageFn('user', [{ type: 'text', text }], {
|
|
522
|
+
source: 'gate:debounce',
|
|
523
|
+
policies: policyNames,
|
|
524
|
+
});
|
|
525
|
+
|
|
526
|
+
for (const agentName of this.getAgentNamesFn()) {
|
|
527
|
+
this.requestInferenceFn(agentName, 'gate:debounce', 'gate');
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
// =========================================================================
|
|
532
|
+
// Inference lifecycle
|
|
533
|
+
// =========================================================================
|
|
534
|
+
|
|
535
|
+
onInferenceStarted(agentName: string): void {
|
|
536
|
+
this.inferring.add(agentName);
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
onInferenceEnded(agentName: string): void {
|
|
540
|
+
this.inferring.delete(agentName);
|
|
541
|
+
|
|
542
|
+
// Flush inference buffer if no agents are inferring
|
|
543
|
+
if (this.inferring.size === 0 && this.inferenceBuffer.length > 0) {
|
|
544
|
+
const events = this.inferenceBuffer.splice(0);
|
|
545
|
+
// Defer to avoid re-entrancy inside trace callbacks
|
|
546
|
+
queueMicrotask(() => this.deliverEvents(events));
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
// =========================================================================
|
|
551
|
+
// Inference buffer cap
|
|
552
|
+
// =========================================================================
|
|
553
|
+
|
|
554
|
+
/** Called internally when debounce fires during inference. */
|
|
555
|
+
private bufferForInference(events: PendingEvent[]): void {
|
|
556
|
+
for (const event of events) {
|
|
557
|
+
if (this.inferenceBuffer.length >= MAX_INFERENCE_BUFFER) {
|
|
558
|
+
this.inferenceBuffer.shift(); // Drop oldest
|
|
559
|
+
}
|
|
560
|
+
this.inferenceBuffer.push(event);
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
// =========================================================================
|
|
565
|
+
// Tool: gate:status
|
|
566
|
+
// =========================================================================
|
|
567
|
+
|
|
568
|
+
getToolDefinition(): ToolDefinition {
|
|
569
|
+
return {
|
|
570
|
+
name: 'gate:status',
|
|
571
|
+
description: 'Show the current EventGate configuration, per-policy match counts, debounce state, and any config errors.',
|
|
572
|
+
inputSchema: {
|
|
573
|
+
type: 'object',
|
|
574
|
+
},
|
|
575
|
+
};
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
async handleToolCall(): Promise<{ success: boolean; data?: GateStatus; error?: string }> {
|
|
579
|
+
const status = this.getStatus();
|
|
580
|
+
return { success: true, data: status };
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
getStatus(): GateStatus {
|
|
584
|
+
const policies: GatePolicyStats[] = this.config.policies.map(p => {
|
|
585
|
+
const stats = this.stats.get(p.name);
|
|
586
|
+
const debounce = this.debounceTimers.get(p.name);
|
|
587
|
+
const result: GatePolicyStats = {
|
|
588
|
+
name: p.name,
|
|
589
|
+
behavior: p.behavior,
|
|
590
|
+
matchCount: stats?.matchCount ?? 0,
|
|
591
|
+
lastMatchTimestamp: stats?.lastMatchTimestamp ?? null,
|
|
592
|
+
};
|
|
593
|
+
if (typeof p.behavior === 'object' && 'debounce' in p.behavior) {
|
|
594
|
+
result.debounceState = {
|
|
595
|
+
pendingCount: debounce?.events.length ?? 0,
|
|
596
|
+
nextDeliveryMs: null, // Timer internals not easily inspectable
|
|
597
|
+
};
|
|
598
|
+
}
|
|
599
|
+
return result;
|
|
600
|
+
});
|
|
601
|
+
|
|
602
|
+
return {
|
|
603
|
+
configPath: this.configPath,
|
|
604
|
+
configSource: this.configSource,
|
|
605
|
+
lastReloadTimestamp: this.lastReloadTimestamp,
|
|
606
|
+
default: this.config.default ?? 'always',
|
|
607
|
+
policies,
|
|
608
|
+
errors: [...this.configErrors],
|
|
609
|
+
};
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
// =========================================================================
|
|
613
|
+
// Cleanup
|
|
614
|
+
// =========================================================================
|
|
615
|
+
|
|
616
|
+
dispose(): void {
|
|
617
|
+
for (const state of this.debounceTimers.values()) {
|
|
618
|
+
clearTimeout(state.timer);
|
|
619
|
+
}
|
|
620
|
+
this.debounceTimers.clear();
|
|
621
|
+
this.inferenceBuffer = [];
|
|
622
|
+
}
|
|
623
|
+
}
|