@animalabs/agent-framework 0.5.7 → 0.6.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/src/framework.d.ts +67 -0
- package/dist/src/framework.d.ts.map +1 -1
- package/dist/src/framework.js +469 -26
- package/dist/src/framework.js.map +1 -1
- package/dist/src/gate/event-gate.d.ts +44 -1
- package/dist/src/gate/event-gate.d.ts.map +1 -1
- package/dist/src/gate/event-gate.js +152 -8
- package/dist/src/gate/event-gate.js.map +1 -1
- package/dist/src/gate/gate-script-worker.d.ts +2 -0
- package/dist/src/gate/gate-script-worker.d.ts.map +1 -0
- package/dist/src/gate/gate-script-worker.js +69 -0
- package/dist/src/gate/gate-script-worker.js.map +1 -0
- package/dist/src/gate/gate-script.d.ts +44 -0
- package/dist/src/gate/gate-script.d.ts.map +1 -0
- package/dist/src/gate/gate-script.js +164 -0
- package/dist/src/gate/gate-script.js.map +1 -0
- package/dist/src/gate/types.d.ts +18 -3
- package/dist/src/gate/types.d.ts.map +1 -1
- package/dist/src/mcpl/channel-registry.d.ts +43 -1
- package/dist/src/mcpl/channel-registry.d.ts.map +1 -1
- package/dist/src/mcpl/channel-registry.js +82 -8
- package/dist/src/mcpl/channel-registry.js.map +1 -1
- package/dist/src/mcpl/checkpoint-manager.d.ts +7 -0
- package/dist/src/mcpl/checkpoint-manager.d.ts.map +1 -1
- package/dist/src/mcpl/checkpoint-manager.js +15 -0
- package/dist/src/mcpl/checkpoint-manager.js.map +1 -1
- package/dist/src/mcpl/push-handler.d.ts.map +1 -1
- package/dist/src/mcpl/push-handler.js +1 -0
- package/dist/src/mcpl/push-handler.js.map +1 -1
- package/dist/src/mcpl/server-connection.d.ts +35 -30
- package/dist/src/mcpl/server-connection.d.ts.map +1 -1
- package/dist/src/mcpl/server-connection.js +145 -206
- package/dist/src/mcpl/server-connection.js.map +1 -1
- package/dist/src/mcpl/transport.d.ts +98 -0
- package/dist/src/mcpl/transport.d.ts.map +1 -0
- package/dist/src/mcpl/transport.js +281 -0
- package/dist/src/mcpl/transport.js.map +1 -0
- package/dist/src/mcpl/types.d.ts +42 -2
- package/dist/src/mcpl/types.d.ts.map +1 -1
- package/dist/src/mcpl/types.js.map +1 -1
- package/dist/src/module-registry.d.ts +2 -0
- package/dist/src/module-registry.d.ts.map +1 -1
- package/dist/src/module-registry.js +9 -2
- package/dist/src/module-registry.js.map +1 -1
- package/dist/src/prose-segments.d.ts +18 -0
- package/dist/src/prose-segments.d.ts.map +1 -0
- package/dist/src/prose-segments.js +39 -0
- package/dist/src/prose-segments.js.map +1 -0
- package/dist/src/runtime/liveness-watchdog-worker.d.ts +2 -0
- package/dist/src/runtime/liveness-watchdog-worker.d.ts.map +1 -0
- package/dist/src/runtime/liveness-watchdog-worker.js +57 -0
- package/dist/src/runtime/liveness-watchdog-worker.js.map +1 -0
- package/dist/src/runtime/liveness-watchdog.d.ts +36 -0
- package/dist/src/runtime/liveness-watchdog.d.ts.map +1 -0
- package/dist/src/runtime/liveness-watchdog.js +106 -0
- package/dist/src/runtime/liveness-watchdog.js.map +1 -0
- package/dist/src/types/agent.d.ts +10 -0
- package/dist/src/types/agent.d.ts.map +1 -1
- package/dist/src/types/framework.d.ts +14 -0
- package/dist/src/types/framework.d.ts.map +1 -1
- package/dist/src/types/module.d.ts +12 -0
- package/dist/src/types/module.d.ts.map +1 -1
- package/dist/test/channel-registry-routing.test.js +108 -1
- package/dist/test/channel-registry-routing.test.js.map +1 -1
- package/dist/test/checkpoint-attribution.test.d.ts +2 -0
- package/dist/test/checkpoint-attribution.test.d.ts.map +1 -0
- package/dist/test/checkpoint-attribution.test.js +40 -0
- package/dist/test/checkpoint-attribution.test.js.map +1 -0
- package/dist/test/event-gate-policy-mutation.test.d.ts +2 -0
- package/dist/test/event-gate-policy-mutation.test.d.ts.map +1 -0
- package/dist/test/event-gate-policy-mutation.test.js +137 -0
- package/dist/test/event-gate-policy-mutation.test.js.map +1 -0
- package/dist/test/event-gate-script.test.d.ts +2 -0
- package/dist/test/event-gate-script.test.d.ts.map +1 -0
- package/dist/test/event-gate-script.test.js +95 -0
- package/dist/test/event-gate-script.test.js.map +1 -0
- package/dist/test/event-gate-tags.test.d.ts +2 -0
- package/dist/test/event-gate-tags.test.d.ts.map +1 -0
- package/dist/test/event-gate-tags.test.js +76 -0
- package/dist/test/event-gate-tags.test.js.map +1 -0
- package/dist/test/liveness-watchdog.test.d.ts +2 -0
- package/dist/test/liveness-watchdog.test.d.ts.map +1 -0
- package/dist/test/liveness-watchdog.test.js +78 -0
- package/dist/test/liveness-watchdog.test.js.map +1 -0
- package/dist/test/mcpl-ws-transport.test.d.ts +2 -0
- package/dist/test/mcpl-ws-transport.test.d.ts.map +1 -0
- package/dist/test/mcpl-ws-transport.test.js +155 -0
- package/dist/test/mcpl-ws-transport.test.js.map +1 -0
- package/dist/test/prose-segments.test.d.ts +2 -0
- package/dist/test/prose-segments.test.d.ts.map +1 -0
- package/dist/test/prose-segments.test.js +52 -0
- package/dist/test/prose-segments.test.js.map +1 -0
- package/dist/test/trunk-channel-routing.test.d.ts +19 -0
- package/dist/test/trunk-channel-routing.test.d.ts.map +1 -0
- package/dist/test/trunk-channel-routing.test.js +175 -0
- package/dist/test/trunk-channel-routing.test.js.map +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +2 -2
- package/src/framework.ts +509 -24
- package/src/gate/event-gate.ts +153 -8
- package/src/gate/gate-script-worker.ts +73 -0
- package/src/gate/gate-script.ts +180 -0
- package/src/gate/types.ts +19 -3
- package/src/mcpl/channel-registry.ts +112 -8
- package/src/mcpl/checkpoint-manager.ts +16 -0
- package/src/mcpl/push-handler.ts +1 -0
- package/src/mcpl/server-connection.ts +153 -224
- package/src/mcpl/transport.ts +307 -0
- package/src/mcpl/types.ts +49 -2
- package/src/module-registry.ts +13 -2
- package/src/prose-segments.ts +42 -0
- package/src/runtime/liveness-watchdog-worker.ts +66 -0
- package/src/runtime/liveness-watchdog.ts +122 -0
- package/src/types/agent.ts +10 -0
- package/src/types/framework.ts +15 -0
- package/src/types/module.ts +13 -0
package/src/framework.ts
CHANGED
|
@@ -53,6 +53,7 @@ import { ChannelRegistry } from './mcpl/channel-registry.js';
|
|
|
53
53
|
import { ConversationRouter } from './mcpl/conversation-router.js';
|
|
54
54
|
import { safeSlice } from './safe-slice.js';
|
|
55
55
|
import { toolResultDataToHistoryString } from './tool-result-history.js';
|
|
56
|
+
import { splitProseSegments } from './prose-segments.js';
|
|
56
57
|
import { CheckpointManager } from './mcpl/checkpoint-manager.js';
|
|
57
58
|
import { isToolAllowed } from './mcpl/tool-policy.js';
|
|
58
59
|
import { EventGate } from './gate/event-gate.js';
|
|
@@ -211,6 +212,16 @@ export class AgentFramework {
|
|
|
211
212
|
private inferencePolicy: InferencePolicy;
|
|
212
213
|
private errorPolicy: ErrorPolicy;
|
|
213
214
|
private pendingRequests: InferenceRequest[] = [];
|
|
215
|
+
/**
|
|
216
|
+
* Per-agent channel that triggered the agent's CURRENT inference turn, if any
|
|
217
|
+
* (item-3 redux). Read by the ChannelRegistry's `activeChannelResolver` to
|
|
218
|
+
* route a single-trunk agent's plain-text speech back to the channel it is
|
|
219
|
+
* answering, instead of the process-global most-recent-inbound locus that a
|
|
220
|
+
* concurrent message elsewhere can hijack. Set at turn start (or cleared for a
|
|
221
|
+
* heartbeat / no-trigger turn) in startAgentStream; overwritten by the next
|
|
222
|
+
* turn. Never read between turns (a given agent runs one turn at a time).
|
|
223
|
+
*/
|
|
224
|
+
private activeTriggerChannels: Map<string, string> = new Map();
|
|
214
225
|
private running = false;
|
|
215
226
|
private loopPromise: Promise<void> | null = null;
|
|
216
227
|
private traceListeners: TraceEventListener[] = [];
|
|
@@ -237,6 +248,9 @@ export class AgentFramework {
|
|
|
237
248
|
private turnCounters: Map<string, number> = new Map(); // agentName → next turnIndex
|
|
238
249
|
private redoStacks: Map<string, RedoEntry[]> = new Map(); // agentName → redo entries
|
|
239
250
|
|
|
251
|
+
/** Liveness watchdog (fail hard on a wedged main thread). Null unless enabled. */
|
|
252
|
+
private livenessWatchdog: import('./runtime/liveness-watchdog.js').LivenessWatchdog | null = null;
|
|
253
|
+
|
|
240
254
|
// MCPL subsystems (null when no mcplServers configured)
|
|
241
255
|
private mcplServerRegistry: McplServerRegistry | null = null;
|
|
242
256
|
private featureSetManager: FeatureSetManager | null = null;
|
|
@@ -309,6 +323,7 @@ export class AgentFramework {
|
|
|
309
323
|
queryMessages: (filter) => this.queryMessages(filter),
|
|
310
324
|
pushEvent: (event) => this.pushEvent(event),
|
|
311
325
|
onTrace: (listener) => this.onTrace(listener),
|
|
326
|
+
callTool: (call) => this.executeToolCall(call),
|
|
312
327
|
});
|
|
313
328
|
}
|
|
314
329
|
|
|
@@ -444,6 +459,19 @@ export class AgentFramework {
|
|
|
444
459
|
});
|
|
445
460
|
}
|
|
446
461
|
|
|
462
|
+
// Liveness watchdog: fail hard if the main thread wedges (opt-in).
|
|
463
|
+
if (config.watchdog?.enabled) {
|
|
464
|
+
const { LivenessWatchdog } = await import('./runtime/liveness-watchdog.js');
|
|
465
|
+
framework.livenessWatchdog = new LivenessWatchdog({
|
|
466
|
+
enabled: true,
|
|
467
|
+
thresholdMs: config.watchdog.thresholdMs,
|
|
468
|
+
action: config.watchdog.action,
|
|
469
|
+
reportPath: config.watchdog.reportPath
|
|
470
|
+
?? (config.storePath ? join(config.storePath, 'watchdog-wedge.jsonl') : undefined),
|
|
471
|
+
});
|
|
472
|
+
framework.livenessWatchdog.start();
|
|
473
|
+
}
|
|
474
|
+
|
|
447
475
|
// Initialize MCPL subsystems if configured
|
|
448
476
|
if (config.mcplServers && config.mcplServers.length > 0) {
|
|
449
477
|
// Validate tool prefixes: no collisions with module names or between servers
|
|
@@ -529,6 +557,7 @@ export class AgentFramework {
|
|
|
529
557
|
|
|
530
558
|
// Dispose EventGate (clear debounce timers)
|
|
531
559
|
this.eventGate?.dispose();
|
|
560
|
+
this.livenessWatchdog?.stop();
|
|
532
561
|
|
|
533
562
|
// Stop modules and MCPL servers in parallel
|
|
534
563
|
const shutdownPromises: Promise<void>[] = [this.moduleRegistry.stopAll()];
|
|
@@ -658,7 +687,12 @@ export class AgentFramework {
|
|
|
658
687
|
const moduleTools = this.moduleRegistry.getAllTools();
|
|
659
688
|
const channelTools = this.channelRegistry?.getChannelTools() ?? [];
|
|
660
689
|
const gateTools = this.eventGate
|
|
661
|
-
? [
|
|
690
|
+
? [
|
|
691
|
+
this.eventGate.getToolDefinition(),
|
|
692
|
+
...AgentFramework.SLEEP_TOOLS,
|
|
693
|
+
...AgentFramework.WAKE_RULE_TOOLS,
|
|
694
|
+
AgentFramework.EVENT_TAGS_TOOL,
|
|
695
|
+
]
|
|
662
696
|
: [];
|
|
663
697
|
if (this.mcplTools.length === 0 && channelTools.length === 0 && gateTools.length === 0) {
|
|
664
698
|
return moduleTools;
|
|
@@ -763,6 +797,45 @@ export class AgentFramework {
|
|
|
763
797
|
return this.moduleRegistry.getModule(name);
|
|
764
798
|
}
|
|
765
799
|
|
|
800
|
+
// =========================================================================
|
|
801
|
+
// Wake-rule surface (gate policy mutation) — backs wake_add_rule /
|
|
802
|
+
// wake_remove_rule and lets host modules compose higher-level wake modes
|
|
803
|
+
// (e.g. an "every-message-debounced" channel mode) without reaching into
|
|
804
|
+
// the private EventGate.
|
|
805
|
+
// =========================================================================
|
|
806
|
+
|
|
807
|
+
/**
|
|
808
|
+
* Add or replace (upsert) a gate policy at runtime. Validated and persisted
|
|
809
|
+
* to gate.json; hot-applied in memory. Throws if no gate is configured or the
|
|
810
|
+
* policy is invalid. `position: 'prepend'` puts a wake rule ahead of broad
|
|
811
|
+
* defer/debounce rules (first match wins).
|
|
812
|
+
*/
|
|
813
|
+
addGatePolicy(
|
|
814
|
+
rawPolicy: unknown,
|
|
815
|
+
options?: { position?: 'append' | 'prepend' },
|
|
816
|
+
): import('./gate/types.js').GatePolicy {
|
|
817
|
+
if (!this.eventGate) {
|
|
818
|
+
throw new Error('No EventGate configured (FrameworkConfig.gate is unset).');
|
|
819
|
+
}
|
|
820
|
+
return this.eventGate.addPolicy(rawPolicy, options);
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
/**
|
|
824
|
+
* Remove a gate policy by name at runtime. Returns false if it didn't exist.
|
|
825
|
+
* Throws if no gate is configured.
|
|
826
|
+
*/
|
|
827
|
+
removeGatePolicy(name: string): boolean {
|
|
828
|
+
if (!this.eventGate) {
|
|
829
|
+
throw new Error('No EventGate configured (FrameworkConfig.gate is unset).');
|
|
830
|
+
}
|
|
831
|
+
return this.eventGate.removePolicy(name);
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
/** Current gate policy names (freshest on-disk view). Empty when no gate. */
|
|
835
|
+
getGatePolicyNames(): string[] {
|
|
836
|
+
return this.eventGate?.listPolicyNames() ?? [];
|
|
837
|
+
}
|
|
838
|
+
|
|
766
839
|
/**
|
|
767
840
|
* Get the Membrane instance.
|
|
768
841
|
*/
|
|
@@ -1037,6 +1110,154 @@ export class AgentFramework {
|
|
|
1037
1110
|
},
|
|
1038
1111
|
];
|
|
1039
1112
|
|
|
1113
|
+
/** Synthesized wake-rule tools: add/remove a gate.json policy at runtime.
|
|
1114
|
+
* Present when a gate is wired. They write validated policies into the
|
|
1115
|
+
* hot-reloaded gate.json (same validation as load), so a rule takes effect
|
|
1116
|
+
* immediately and survives restart. */
|
|
1117
|
+
private static readonly WAKE_RULE_TOOLS: import('./types/index.js').ToolDefinition[] = [
|
|
1118
|
+
{
|
|
1119
|
+
name: 'wake_add_rule',
|
|
1120
|
+
description:
|
|
1121
|
+
'Add or replace a wake rule (a gate.json policy) at runtime — no need to ' +
|
|
1122
|
+
'hand-edit the file. The rule is validated and hot-applied immediately. ' +
|
|
1123
|
+
'A rule with the same `name` replaces the existing one in place. Use ' +
|
|
1124
|
+
'`position: "prepend"` to put a wake rule ahead of broad defer/debounce ' +
|
|
1125
|
+
'rules (first match wins). Two common shapes:\n' +
|
|
1126
|
+
'• Watch a FILE/workspace path: match on `mount` + `pathGlob`, e.g. ' +
|
|
1127
|
+
'`{ name: "watch-notes", match: { scope: ["workspace:modified"], mount: "project", pathGlob: "notes/*.md" }, behavior: "always" }`.\n' +
|
|
1128
|
+
'• Watch a CHANNEL: match on `source` + `channel` (and/or `tagsAny: ["chat:ambient"]`), ' +
|
|
1129
|
+
'e.g. `{ name: "watch-cairn", match: { source: "discord", channel: "discord:*:12345", tagsAny: ["chat:ambient"] }, behavior: { debounce: 60000 } }` ' +
|
|
1130
|
+
'(the channel must be subscribed for ambient events to arrive — see channel mode).',
|
|
1131
|
+
inputSchema: {
|
|
1132
|
+
type: 'object',
|
|
1133
|
+
properties: {
|
|
1134
|
+
name: { type: 'string', description: 'Unique rule name. Reusing a name replaces that rule.' },
|
|
1135
|
+
match: {
|
|
1136
|
+
type: 'object',
|
|
1137
|
+
description: 'Match criteria (all AND together). Omitted fields match anything.',
|
|
1138
|
+
properties: {
|
|
1139
|
+
scope: { type: 'array', items: { type: 'string' }, description: 'Event types, e.g. ["mcpl:channel-incoming","workspace:modified"].' },
|
|
1140
|
+
source: { type: 'string', description: 'Integration/serverId, glob ok (e.g. "discord").' },
|
|
1141
|
+
channel: { type: 'string', description: 'Channel id, glob ok (e.g. "discord:*:12345").' },
|
|
1142
|
+
mount: { type: 'string', description: 'Workspace mount name, glob ok (workspace:* events).' },
|
|
1143
|
+
pathGlob: { type: 'string', description: 'Glob over touched paths (workspace:* events).' },
|
|
1144
|
+
tagsAny: { type: 'array', items: { type: 'string' }, description: 'Match if ANY tag matches (globs ok).' },
|
|
1145
|
+
tagsAll: { type: 'array', items: { type: 'string' }, description: 'Match only if EVERY tag matches.' },
|
|
1146
|
+
tagsNone: { type: 'array', items: { type: 'string' }, description: 'Match only if NONE match.' },
|
|
1147
|
+
metadataTrue: { type: 'array', items: { type: 'string' }, description: 'Match if ANY listed metadata field is truthy.' },
|
|
1148
|
+
filter: {
|
|
1149
|
+
type: 'object',
|
|
1150
|
+
description: 'Content filter.',
|
|
1151
|
+
properties: {
|
|
1152
|
+
type: { type: 'string', enum: ['text', 'regex'] },
|
|
1153
|
+
pattern: { type: 'string' },
|
|
1154
|
+
},
|
|
1155
|
+
required: ['type', 'pattern'],
|
|
1156
|
+
},
|
|
1157
|
+
},
|
|
1158
|
+
},
|
|
1159
|
+
behavior: {
|
|
1160
|
+
type: 'string',
|
|
1161
|
+
enum: ['always', 'defer', 'skip'],
|
|
1162
|
+
description:
|
|
1163
|
+
'Simple behavior: "always" (wake now) or "defer" (don\'t wake; still enters ' +
|
|
1164
|
+
'context). For debounce / rate-limit / sampling, use debounceMs / rateLimit / ' +
|
|
1165
|
+
'passiveSample below instead of this field. Exactly one behavior must be given.',
|
|
1166
|
+
},
|
|
1167
|
+
debounceMs: {
|
|
1168
|
+
type: 'number',
|
|
1169
|
+
description: 'Shorthand for { debounce: ms }: wake once after ms of quiet (100–300000).',
|
|
1170
|
+
},
|
|
1171
|
+
rateLimit: {
|
|
1172
|
+
type: 'object',
|
|
1173
|
+
description: 'Token-bucket wake: at most `tokens` wakes per window; refills one per refillIntervalMs.',
|
|
1174
|
+
properties: {
|
|
1175
|
+
tokens: { type: 'number', description: 'Bucket capacity (> 0).' },
|
|
1176
|
+
refillIntervalMs: { type: 'number', description: 'Ms between token refills (> 0).' },
|
|
1177
|
+
keyBy: { type: 'string', description: 'Metadata field to partition buckets by (e.g. "channelId").' },
|
|
1178
|
+
},
|
|
1179
|
+
required: ['tokens', 'refillIntervalMs'],
|
|
1180
|
+
},
|
|
1181
|
+
passiveSample: {
|
|
1182
|
+
type: 'object',
|
|
1183
|
+
description: 'Wake every Nth matching event.',
|
|
1184
|
+
properties: {
|
|
1185
|
+
every: { type: 'number', description: 'Fire every N matches (positive integer).' },
|
|
1186
|
+
keyBy: { type: 'string', description: 'Metadata field for separate per-key counters.' },
|
|
1187
|
+
},
|
|
1188
|
+
required: ['every'],
|
|
1189
|
+
},
|
|
1190
|
+
position: {
|
|
1191
|
+
type: 'string',
|
|
1192
|
+
enum: ['append', 'prepend'],
|
|
1193
|
+
description: 'Where to insert a NEW rule (ignored when replacing by name). Default "append".',
|
|
1194
|
+
},
|
|
1195
|
+
resets: {
|
|
1196
|
+
type: 'array',
|
|
1197
|
+
items: { type: 'string' },
|
|
1198
|
+
description: 'Names of other rules whose runtime state to clear when this rule fires.',
|
|
1199
|
+
},
|
|
1200
|
+
},
|
|
1201
|
+
required: ['name'],
|
|
1202
|
+
},
|
|
1203
|
+
},
|
|
1204
|
+
{
|
|
1205
|
+
name: 'wake_remove_rule',
|
|
1206
|
+
description:
|
|
1207
|
+
'Remove a wake rule (gate.json policy) by name at runtime. Any pending ' +
|
|
1208
|
+
'debounce batch for that rule is delivered first. Returns whether a rule ' +
|
|
1209
|
+
'was removed (false if no rule had that name).',
|
|
1210
|
+
inputSchema: {
|
|
1211
|
+
type: 'object',
|
|
1212
|
+
properties: {
|
|
1213
|
+
name: { type: 'string', description: 'The rule name to remove.' },
|
|
1214
|
+
},
|
|
1215
|
+
required: ['name'],
|
|
1216
|
+
},
|
|
1217
|
+
},
|
|
1218
|
+
];
|
|
1219
|
+
|
|
1220
|
+
/** Discoverability for event tags (MCPL RFC-001): the reserved chat:* core,
|
|
1221
|
+
* each connected server's declared tag ontology, and gate.js status. */
|
|
1222
|
+
private static readonly EVENT_TAGS_TOOL: import('./types/index.js').ToolDefinition = {
|
|
1223
|
+
name: 'event_tags',
|
|
1224
|
+
description:
|
|
1225
|
+
'List the event tags available for gating: the reserved cross-platform ' +
|
|
1226
|
+
'chat:* core, each connected server\'s declared tag ontology (descriptions, ' +
|
|
1227
|
+
'implications, suggested treatments), and the status of your programmable ' +
|
|
1228
|
+
'gate (gate.js). Use these tag names in gate.json policies (tagsAny / ' +
|
|
1229
|
+
'tagsAll / tagsNone) or in gate.js.',
|
|
1230
|
+
inputSchema: { type: 'object' },
|
|
1231
|
+
};
|
|
1232
|
+
|
|
1233
|
+
/** Reserved chat:* core vocabulary (MCPL RFC-001 §4) — short descriptions so
|
|
1234
|
+
* the agent can author rules without reading the spec. */
|
|
1235
|
+
private static readonly CHAT_CORE_TAGS: Record<string, string> = {
|
|
1236
|
+
'chat:addressed': 'Directed at you (umbrella: dm/mention/reply)',
|
|
1237
|
+
'chat:mention': 'You were explicitly @-mentioned',
|
|
1238
|
+
'chat:reply': 'A reply to your own message',
|
|
1239
|
+
'chat:dm': 'A direct/private message to you',
|
|
1240
|
+
'chat:ambient': 'Overheard in a followed channel; not addressed',
|
|
1241
|
+
'chat:broadcast': 'Channel-wide ping (@everyone / channel post)',
|
|
1242
|
+
'chat:to-self': 'Acts on your own content (reaction/reply to you)',
|
|
1243
|
+
'chat:from-human': 'Authored by a human',
|
|
1244
|
+
'chat:from-bot': 'Authored by a bot/automation',
|
|
1245
|
+
'chat:from-self': 'Your own message, echoed back',
|
|
1246
|
+
'chat:from-agent': 'Authored by another persona/agent',
|
|
1247
|
+
'chat:edited': 'An edit of an existing message',
|
|
1248
|
+
'chat:deleted': 'A deletion',
|
|
1249
|
+
'chat:reaction': 'An emoji reaction was added',
|
|
1250
|
+
'chat:reaction-remove': 'A reaction was removed',
|
|
1251
|
+
'chat:has-image': 'Has an image attachment',
|
|
1252
|
+
'chat:has-audio': 'Has an audio attachment',
|
|
1253
|
+
'chat:has-file': 'Has a file attachment',
|
|
1254
|
+
'chat:has-link': 'Contains a link',
|
|
1255
|
+
'chat:command': 'A slash/bot command invocation',
|
|
1256
|
+
'chat:private': 'Private conversation',
|
|
1257
|
+
'chat:group': 'Group (multi-party) conversation',
|
|
1258
|
+
'chat:thread': 'Occurred in a thread',
|
|
1259
|
+
};
|
|
1260
|
+
|
|
1040
1261
|
/** Refusal category → Discord reaction emoji. Unknown categories get 🛑. */
|
|
1041
1262
|
private static readonly REFUSAL_REACTIONS: Record<string, string> = {
|
|
1042
1263
|
bio: '☣️',
|
|
@@ -2115,6 +2336,9 @@ export class AgentFramework {
|
|
|
2115
2336
|
reason: 'mcpl:channel-incoming',
|
|
2116
2337
|
source: event.serverId,
|
|
2117
2338
|
timestamp: Date.now(),
|
|
2339
|
+
// Route this turn's auto-published speech back to THIS channel, not
|
|
2340
|
+
// the global most-recent-inbound locus (item-3 redux, trunk agents).
|
|
2341
|
+
channelId: event.channelId,
|
|
2118
2342
|
});
|
|
2119
2343
|
}
|
|
2120
2344
|
}
|
|
@@ -2209,6 +2433,9 @@ export class AgentFramework {
|
|
|
2209
2433
|
reason: 'mcpl:channel-incoming',
|
|
2210
2434
|
source: event.serverId,
|
|
2211
2435
|
timestamp: Date.now(),
|
|
2436
|
+
// A fork's home channel wins in routeSpeech regardless, but carry the
|
|
2437
|
+
// triggering channel too so the trunk/active path stays consistent.
|
|
2438
|
+
channelId: event.channelId,
|
|
2212
2439
|
});
|
|
2213
2440
|
}
|
|
2214
2441
|
}
|
|
@@ -2390,6 +2617,20 @@ export class AgentFramework {
|
|
|
2390
2617
|
const id = this.addMessage('user', event.content, metadata);
|
|
2391
2618
|
this.emitTrace({ type: 'message:added', messageId: id, source: 'mcpl:push-event' });
|
|
2392
2619
|
|
|
2620
|
+
// Some push events carry a channel of origin (Discord DMs arrive here, not
|
|
2621
|
+
// via channels/incoming, because discord-mcpl forwards them with the channel
|
|
2622
|
+
// closed). Register + open that channel so routeSpeech can resolve it, and
|
|
2623
|
+
// thread it onto the wake request so the reply routes back to it instead of
|
|
2624
|
+
// the global locus (item-3 redux, DM sub-case).
|
|
2625
|
+
const triggerChannel = this.derivePushEventChannel(event.origin);
|
|
2626
|
+
if (triggerChannel && this.channelRegistry) {
|
|
2627
|
+
this.channelRegistry.ensureChannelRegistered(
|
|
2628
|
+
event.serverId,
|
|
2629
|
+
triggerChannel.channelId,
|
|
2630
|
+
triggerChannel.label,
|
|
2631
|
+
);
|
|
2632
|
+
}
|
|
2633
|
+
|
|
2393
2634
|
if (event.triggerInference) {
|
|
2394
2635
|
// Default broadcast excludes conversation forks (channel-driven).
|
|
2395
2636
|
const targetAgents = event.targetAgents
|
|
@@ -2400,11 +2641,50 @@ export class AgentFramework {
|
|
|
2400
2641
|
reason: 'mcpl:push-event',
|
|
2401
2642
|
source: event.serverId,
|
|
2402
2643
|
timestamp: Date.now(),
|
|
2644
|
+
channelId: triggerChannel?.channelId,
|
|
2403
2645
|
});
|
|
2404
2646
|
}
|
|
2405
2647
|
}
|
|
2406
2648
|
}
|
|
2407
2649
|
|
|
2650
|
+
/**
|
|
2651
|
+
* Derive the MCPL composite channel id (the outbound routing locus) for a
|
|
2652
|
+
* push event from its server-defined `origin`, if it carries one.
|
|
2653
|
+
*
|
|
2654
|
+
* Discord DMs are the motivating case: discord-mcpl forwards them via
|
|
2655
|
+
* push/event (the DM channel is closed), so they never pass through
|
|
2656
|
+
* channels/incoming and their channel is never registered — leaving the
|
|
2657
|
+
* agent's reply with nowhere to route (item-3 redux, DM sub-case). Prefers an
|
|
2658
|
+
* explicit `origin.mcplChannelId` (a surface declaring its own composite id —
|
|
2659
|
+
* the surface-agnostic contract); otherwise reconstructs the Discord form
|
|
2660
|
+
* `discord:{guildId|dm}:{channelId}` from origin fields, matching
|
|
2661
|
+
* discord-mcpl's `mcplChannelId()` / `parseMcplChannelId()` convention so the
|
|
2662
|
+
* fix works even against a discord-mcpl build that predates `mcplChannelId`.
|
|
2663
|
+
* Returns undefined for push events with no channel provenance (heartbeats,
|
|
2664
|
+
* timers), which correctly keep the global fallback.
|
|
2665
|
+
*/
|
|
2666
|
+
private derivePushEventChannel(
|
|
2667
|
+
origin: Record<string, unknown> | undefined,
|
|
2668
|
+
): { channelId: string; label?: string } | undefined {
|
|
2669
|
+
if (!origin) return undefined;
|
|
2670
|
+
const label = typeof origin.channelName === 'string' ? origin.channelName : undefined;
|
|
2671
|
+
|
|
2672
|
+
const explicit = origin.mcplChannelId;
|
|
2673
|
+
if (typeof explicit === 'string' && explicit) {
|
|
2674
|
+
return { channelId: explicit, label };
|
|
2675
|
+
}
|
|
2676
|
+
|
|
2677
|
+
// Discord fallback: reconstruct the composite from origin parts. `guildId`
|
|
2678
|
+
// is null for a DM (→ 'dm'); a real guild id for a non-open guild channel.
|
|
2679
|
+
if (origin.source === 'discord' && typeof origin.channelId === 'string' && origin.channelId) {
|
|
2680
|
+
const guild =
|
|
2681
|
+
typeof origin.guildId === 'string' && origin.guildId ? origin.guildId : 'dm';
|
|
2682
|
+
return { channelId: `discord:${guild}:${origin.channelId}`, label };
|
|
2683
|
+
}
|
|
2684
|
+
|
|
2685
|
+
return undefined;
|
|
2686
|
+
}
|
|
2687
|
+
|
|
2408
2688
|
private async processInferenceRequests(): Promise<void> {
|
|
2409
2689
|
if (this.pendingRequests.length === 0) {
|
|
2410
2690
|
return;
|
|
@@ -2465,7 +2745,19 @@ export class AgentFramework {
|
|
|
2465
2745
|
|
|
2466
2746
|
// Start streaming inference (non-blocking — driveStream runs in background)
|
|
2467
2747
|
const trigger = requests[0];
|
|
2468
|
-
|
|
2748
|
+
// Route this turn's auto-published speech to the channel that triggered
|
|
2749
|
+
// it (item-3 redux). A batched wake may carry several triggering channels
|
|
2750
|
+
// (messages arrived in >1 channel while the agent was busy/idle) — reply
|
|
2751
|
+
// in the MOST-RECENT one: it matches the legacy last-inbound semantics and
|
|
2752
|
+
// is the message the agent is most likely answering. The common case is
|
|
2753
|
+
// 1 message → 1 channel (unambiguous). Non-channel wakes carry no
|
|
2754
|
+
// channelId, leaving the field undefined → global fallback. `reduce`
|
|
2755
|
+
// keeps the last defined channelId across the (FIFO-ordered) batch.
|
|
2756
|
+
const triggerChannel = requests.reduce<string | undefined>(
|
|
2757
|
+
(acc, r) => r.channelId ?? acc,
|
|
2758
|
+
undefined,
|
|
2759
|
+
);
|
|
2760
|
+
await this.startAgentStream(agent, { ...trigger, channelId: triggerChannel });
|
|
2469
2761
|
}
|
|
2470
2762
|
}
|
|
2471
2763
|
|
|
@@ -2476,6 +2768,18 @@ export class AgentFramework {
|
|
|
2476
2768
|
this.redoStacks.delete(agent.name); // new work invalidates redo
|
|
2477
2769
|
}
|
|
2478
2770
|
|
|
2771
|
+
// Establish this turn's outbound routing locus (item-3 redux). Set it to the
|
|
2772
|
+
// triggering channel for a channel/DM-triggered turn; clear it otherwise so
|
|
2773
|
+
// a heartbeat / no-trigger turn doesn't inherit a previous turn's channel and
|
|
2774
|
+
// instead falls back to the global default. A given agent runs one turn at a
|
|
2775
|
+
// time, so a set here is only read during THIS turn's routeSpeech /
|
|
2776
|
+
// buildChannelContext; retries re-run with the same trigger, re-setting it.
|
|
2777
|
+
if (trigger?.channelId) {
|
|
2778
|
+
this.activeTriggerChannels.set(agent.name, trigger.channelId);
|
|
2779
|
+
} else {
|
|
2780
|
+
this.activeTriggerChannels.delete(agent.name);
|
|
2781
|
+
}
|
|
2782
|
+
|
|
2479
2783
|
this.emitTrace({ type: 'inference:started', agentName: agent.name });
|
|
2480
2784
|
this.eventGate?.onInferenceStarted(agent.name);
|
|
2481
2785
|
|
|
@@ -2888,23 +3192,36 @@ export class AgentFramework {
|
|
|
2888
3192
|
.map((b) => (b as unknown as { name?: string }).name)
|
|
2889
3193
|
.filter((n): n is string => typeof n === 'string');
|
|
2890
3194
|
const silenced = toolNames.some((n) => SILENCING.has(bare(n)));
|
|
2891
|
-
|
|
2892
|
-
|
|
2893
|
-
|
|
2894
|
-
|
|
2895
|
-
|
|
3195
|
+
|
|
3196
|
+
// Split the turn's content into ordered prose segments, broken at
|
|
3197
|
+
// each tool_use / tool_result boundary. `response.content` holds the
|
|
3198
|
+
// WHOLE turn's blocks in provider order — earlier tool rounds
|
|
3199
|
+
// included (see the trailing-slice note above, which slices exactly
|
|
3200
|
+
// because the full turn is present) — so a left-to-right walk
|
|
3201
|
+
// reconstructs the emission order. Interleaved prose
|
|
3202
|
+
// ("msgA → [tool] → msgB → [tool] → msgC") is then delivered as
|
|
3203
|
+
// separate messages IN ORDER instead of being collapsed into one
|
|
3204
|
+
// trailing post (item 4). The silencing rule stays turn-wide: an
|
|
3205
|
+
// explicit send / skip suppresses ALL auto-routed prose this turn,
|
|
3206
|
+
// preserving the double-post guard.
|
|
3207
|
+
const segments = splitProseSegments(response.content);
|
|
3208
|
+
|
|
3209
|
+
if (silenced || segments.length === 0) {
|
|
2896
3210
|
console.error(
|
|
2897
3211
|
`[routing] ${agent.name}: tool-call turn [${toolNames.join(', ') || 'none'}] -> NOT routed ` +
|
|
2898
3212
|
`(${silenced ? 'silencing tool / explicit send' : 'no prose'})`,
|
|
2899
3213
|
);
|
|
2900
3214
|
} else {
|
|
2901
3215
|
console.error(
|
|
2902
|
-
`[routing] ${agent.name}: tool-call turn [${toolNames.join(', ')}] -> routing
|
|
3216
|
+
`[routing] ${agent.name}: tool-call turn [${toolNames.join(', ')}] -> routing ${segments.length} prose segment(s) in order`,
|
|
2903
3217
|
);
|
|
2904
|
-
|
|
2905
|
-
|
|
2906
|
-
|
|
2907
|
-
|
|
3218
|
+
// Deliver sequentially (await each) so the segments land in order.
|
|
3219
|
+
for (const seg of segments) {
|
|
3220
|
+
try {
|
|
3221
|
+
await this.channelRegistry.routeSpeech(agent.name, seg);
|
|
3222
|
+
} catch (err) {
|
|
3223
|
+
console.error('speech routing failed:', err);
|
|
3224
|
+
}
|
|
2908
3225
|
}
|
|
2909
3226
|
}
|
|
2910
3227
|
}
|
|
@@ -3242,6 +3559,18 @@ export class AgentFramework {
|
|
|
3242
3559
|
return;
|
|
3243
3560
|
}
|
|
3244
3561
|
|
|
3562
|
+
// Route wake-rule tools (runtime gate.json policy add/remove)
|
|
3563
|
+
if ((enrichedCall.name === 'wake_add_rule' || enrichedCall.name === 'wake_remove_rule') && this.eventGate) {
|
|
3564
|
+
this.dispatchWakeRuleToolCall(agentName, enrichedCall);
|
|
3565
|
+
return;
|
|
3566
|
+
}
|
|
3567
|
+
|
|
3568
|
+
// Route event_tags (tag/ontology discovery)
|
|
3569
|
+
if (enrichedCall.name === 'event_tags' && this.eventGate) {
|
|
3570
|
+
this.dispatchEventTagsToolCall(agentName, enrichedCall);
|
|
3571
|
+
return;
|
|
3572
|
+
}
|
|
3573
|
+
|
|
3245
3574
|
// Route synthesized 'think' (private reasoning) and 'skip_reply' (deliberate
|
|
3246
3575
|
// stay-silent) tools — handled by the channel registry like the other
|
|
3247
3576
|
// synthesized channel tools, but they aren't `channel_`-prefixed so they
|
|
@@ -3463,7 +3792,7 @@ export class AgentFramework {
|
|
|
3463
3792
|
|
|
3464
3793
|
// (2) Agent-facing chronicle marker (no inference triggered → no loop).
|
|
3465
3794
|
const agent = this.agents.get(agentName);
|
|
3466
|
-
if (agent) {
|
|
3795
|
+
if (agent && process.env.SUPPRESS_INFERENCE_FAILED_MARKER !== '1') {
|
|
3467
3796
|
try {
|
|
3468
3797
|
agent.getContextManager().addMessage(
|
|
3469
3798
|
'user',
|
|
@@ -3562,6 +3891,22 @@ export class AgentFramework {
|
|
|
3562
3891
|
}
|
|
3563
3892
|
},
|
|
3564
3893
|
shouldTriggerInference: triggerFilter,
|
|
3894
|
+
// Route a conversation fork's plain-text speech to its HOME channel, not
|
|
3895
|
+
// the process-global most-recent-inbound locus (item 3). The trunk agent
|
|
3896
|
+
// has no home entry, so this returns undefined and routeSpeech falls back
|
|
3897
|
+
// to defaultPublishChannel. `conversationAgentHomes` is the permanent
|
|
3898
|
+
// spawn-time binding; `channelForAgent` is the router's live binding as a
|
|
3899
|
+
// belt-and-suspenders fallback.
|
|
3900
|
+
homeChannelResolver: (agentName) =>
|
|
3901
|
+
this.conversationAgentHomes.get(agentName)
|
|
3902
|
+
?? this.conversationRouter?.channelForAgent(agentName),
|
|
3903
|
+
// Route a single TRUNK agent's plain-text speech to the channel that
|
|
3904
|
+
// triggered its CURRENT turn (item-3 redux). connectome-host runs every
|
|
3905
|
+
// agent as a trunk (it never exposes conversation forks), so without this
|
|
3906
|
+
// a reply falls back to the process-global most-recent-inbound locus and
|
|
3907
|
+
// a concurrent message in another channel hijacks it. Empty for
|
|
3908
|
+
// heartbeat / no-trigger turns → correct global fallback.
|
|
3909
|
+
activeChannelResolver: (agentName) => this.activeTriggerChannels.get(agentName),
|
|
3565
3910
|
// A text-only turn whose speech couldn't be delivered must not vanish
|
|
3566
3911
|
// silently: record a `[discord-send-failed]` marker in chronicle so the
|
|
3567
3912
|
// agent sees, on her next turn, that her reply never reached the human.
|
|
@@ -3894,14 +4239,20 @@ export class AgentFramework {
|
|
|
3894
4239
|
const startTime = Date.now();
|
|
3895
4240
|
const args = (call.input && typeof call.input === 'object') ? call.input as Record<string, unknown> : {};
|
|
3896
4241
|
|
|
3897
|
-
// Build state params for stateful tools (Step 8)
|
|
4242
|
+
// Build state params for stateful tools (Step 8).
|
|
4243
|
+
// Host-managed state is single-valued per server, so inject the host-managed
|
|
4244
|
+
// set's `state` for any call; otherwise fall back to a server-managed set's
|
|
4245
|
+
// opaque `checkpoint`. Never blindly pick "first stateful" — when a server
|
|
4246
|
+
// mixes host-managed (e.g. a feed) and server-managed (e.g. post/undo) sets,
|
|
4247
|
+
// that misattributes the feed's state.
|
|
3898
4248
|
let stateParams: { state?: unknown; checkpoint?: string } | undefined;
|
|
3899
4249
|
if (this.checkpointManager) {
|
|
3900
|
-
const
|
|
3901
|
-
if (
|
|
3902
|
-
|
|
3903
|
-
|
|
3904
|
-
|
|
4250
|
+
const hostFs = this.checkpointManager.getHostManagedFeatureSet(serverId);
|
|
4251
|
+
if (hostFs) {
|
|
4252
|
+
stateParams = { state: this.checkpointManager.getCurrentState(serverId, hostFs) };
|
|
4253
|
+
} else {
|
|
4254
|
+
const fs = this.checkpointManager.getStatefulFeatureSet(serverId);
|
|
4255
|
+
if (fs) {
|
|
3905
4256
|
const cp = this.checkpointManager.getCurrentCheckpoint(serverId, fs);
|
|
3906
4257
|
if (cp) stateParams = { checkpoint: cp };
|
|
3907
4258
|
}
|
|
@@ -3913,10 +4264,17 @@ export class AgentFramework {
|
|
|
3913
4264
|
const durationMs = Date.now() - startTime;
|
|
3914
4265
|
this.emitTrace({ type: 'tool:completed', module: `mcpl:${serverId}`, tool: toolName, callId: call.id, durationMs });
|
|
3915
4266
|
|
|
3916
|
-
// Record checkpoint from stateful tool response (Step 8)
|
|
4267
|
+
// Record checkpoint from stateful tool response (Step 8).
|
|
4268
|
+
// Attribute to the set the server tagged (result.state.featureSet) first;
|
|
4269
|
+
// else the host-managed set (host-managed checkpoints carry data/patch);
|
|
4270
|
+
// else the single stateful set. Avoids recording a feed checkpoint onto a
|
|
4271
|
+
// server-managed set just because it registered first.
|
|
3917
4272
|
if (result.state && this.checkpointManager) {
|
|
3918
|
-
const
|
|
3919
|
-
|
|
4273
|
+
const tagged = result.state.featureSet;
|
|
4274
|
+
const fs = tagged
|
|
4275
|
+
?? this.checkpointManager.getHostManagedFeatureSet(serverId)
|
|
4276
|
+
?? this.checkpointManager.getStatefulFeatureSet(serverId);
|
|
4277
|
+
if (fs && this.checkpointManager.isStateful(serverId, fs)) {
|
|
3920
4278
|
this.checkpointManager.recordCheckpoint(serverId, fs, result.state);
|
|
3921
4279
|
}
|
|
3922
4280
|
}
|
|
@@ -4037,6 +4395,91 @@ export class AgentFramework {
|
|
|
4037
4395
|
});
|
|
4038
4396
|
}
|
|
4039
4397
|
|
|
4398
|
+
/**
|
|
4399
|
+
* Handle the synthesized `wake_add_rule` / `wake_remove_rule` tools: mutate
|
|
4400
|
+
* the hot-reloaded gate.json at runtime. Validation lives in the EventGate
|
|
4401
|
+
* (same path as gate.json load), so an invalid rule is surfaced as a tool
|
|
4402
|
+
* error and nothing is written.
|
|
4403
|
+
*/
|
|
4404
|
+
private dispatchWakeRuleToolCall(agentName: string, call: ToolCall): void {
|
|
4405
|
+
this.emitTrace({ type: 'tool:started', module: 'gate', tool: call.name, callId: call.id, input: call.input });
|
|
4406
|
+
const finish = (result: ToolResult) => {
|
|
4407
|
+
this.emitTrace({
|
|
4408
|
+
type: result.isError ? 'tool:failed' : 'tool:completed',
|
|
4409
|
+
module: 'gate', tool: call.name, callId: call.id, durationMs: 0,
|
|
4410
|
+
...(result.isError ? { error: result.error } : {}),
|
|
4411
|
+
});
|
|
4412
|
+
this.pushEvent({ type: 'tool-result', callId: call.id, agentName, moduleName: 'gate', result });
|
|
4413
|
+
};
|
|
4414
|
+
|
|
4415
|
+
try {
|
|
4416
|
+
if (call.name === 'wake_remove_rule') {
|
|
4417
|
+
const input = (call.input ?? {}) as { name?: unknown };
|
|
4418
|
+
if (typeof input.name !== 'string' || !input.name) {
|
|
4419
|
+
finish({ success: false, error: 'wake_remove_rule: `name` (string) is required', isError: true });
|
|
4420
|
+
return;
|
|
4421
|
+
}
|
|
4422
|
+
const removed = this.removeGatePolicy(input.name);
|
|
4423
|
+
finish({
|
|
4424
|
+
success: true,
|
|
4425
|
+
data: { removed, name: input.name, policies: this.getGatePolicyNames() },
|
|
4426
|
+
});
|
|
4427
|
+
return;
|
|
4428
|
+
}
|
|
4429
|
+
|
|
4430
|
+
// wake_add_rule — assemble the canonical behavior from the typed fields
|
|
4431
|
+
// (exactly one), then let the gate's own validator do the authoritative
|
|
4432
|
+
// range/shape checks.
|
|
4433
|
+
const input = (call.input ?? {}) as {
|
|
4434
|
+
name?: unknown; match?: unknown; resets?: unknown; position?: unknown;
|
|
4435
|
+
behavior?: unknown; debounceMs?: unknown; rateLimit?: unknown; passiveSample?: unknown;
|
|
4436
|
+
};
|
|
4437
|
+
const behaviorSources = [
|
|
4438
|
+
input.behavior !== undefined ? 'behavior' : null,
|
|
4439
|
+
input.debounceMs !== undefined ? 'debounceMs' : null,
|
|
4440
|
+
input.rateLimit !== undefined ? 'rateLimit' : null,
|
|
4441
|
+
input.passiveSample !== undefined ? 'passiveSample' : null,
|
|
4442
|
+
].filter((s): s is string => s !== null);
|
|
4443
|
+
if (behaviorSources.length === 0) {
|
|
4444
|
+
finish({
|
|
4445
|
+
success: false,
|
|
4446
|
+
error: 'wake_add_rule: specify exactly one behavior — `behavior` ("always"/"defer"/"skip"), `debounceMs`, `rateLimit`, or `passiveSample`.',
|
|
4447
|
+
isError: true,
|
|
4448
|
+
});
|
|
4449
|
+
return;
|
|
4450
|
+
}
|
|
4451
|
+
if (behaviorSources.length > 1) {
|
|
4452
|
+
finish({
|
|
4453
|
+
success: false,
|
|
4454
|
+
error: `wake_add_rule: give only one behavior, got [${behaviorSources.join(', ')}].`,
|
|
4455
|
+
isError: true,
|
|
4456
|
+
});
|
|
4457
|
+
return;
|
|
4458
|
+
}
|
|
4459
|
+
const behavior: unknown =
|
|
4460
|
+
input.debounceMs !== undefined ? { debounce: input.debounceMs }
|
|
4461
|
+
: input.rateLimit !== undefined ? { rate_limit: input.rateLimit }
|
|
4462
|
+
: input.passiveSample !== undefined ? { passive_sample: input.passiveSample }
|
|
4463
|
+
: input.behavior;
|
|
4464
|
+
|
|
4465
|
+
const position = input.position === 'prepend' ? 'prepend' : 'append';
|
|
4466
|
+
const rawPolicy = {
|
|
4467
|
+
name: input.name,
|
|
4468
|
+
match: input.match ?? {},
|
|
4469
|
+
behavior,
|
|
4470
|
+
...(input.resets !== undefined ? { resets: input.resets } : {}),
|
|
4471
|
+
};
|
|
4472
|
+
const policy = this.addGatePolicy(rawPolicy, { position });
|
|
4473
|
+
finish({
|
|
4474
|
+
success: true,
|
|
4475
|
+
data: { added: policy.name, policies: this.getGatePolicyNames() },
|
|
4476
|
+
});
|
|
4477
|
+
} catch (err) {
|
|
4478
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
4479
|
+
finish({ success: false, error: msg, isError: true });
|
|
4480
|
+
}
|
|
4481
|
+
}
|
|
4482
|
+
|
|
4040
4483
|
/**
|
|
4041
4484
|
* Handle the synthesized `sleep` / `wake` tools. `sleep` arms the gate's
|
|
4042
4485
|
* suppression window, optionally announces in the sticky channel, and ends
|
|
@@ -4084,6 +4527,42 @@ export class AgentFramework {
|
|
|
4084
4527
|
finish({ success: true, data: { sleepingFor: human, until }, endTurn: true });
|
|
4085
4528
|
}
|
|
4086
4529
|
|
|
4530
|
+
/** Aggregate the event-tag vocabulary: reserved chat:* core + each connected
|
|
4531
|
+
* server's declared tag ontology + gate.js status. */
|
|
4532
|
+
private buildEventTagsResult(): Record<string, unknown> {
|
|
4533
|
+
const servers: Record<string, Record<string, unknown>> = {};
|
|
4534
|
+
for (const conn of this.mcplServerRegistry?.getAllServers() ?? []) {
|
|
4535
|
+
const declared = this.featureSetManager?.getDeclaredFeatureSets(conn.id) ?? {};
|
|
4536
|
+
const sets: Record<string, unknown> = {};
|
|
4537
|
+
for (const [name, decl] of Object.entries(declared)) {
|
|
4538
|
+
if (decl.tagOntology) sets[name] = decl.tagOntology;
|
|
4539
|
+
}
|
|
4540
|
+
if (Object.keys(sets).length > 0) servers[conn.id] = sets;
|
|
4541
|
+
}
|
|
4542
|
+
return {
|
|
4543
|
+
core: AgentFramework.CHAT_CORE_TAGS,
|
|
4544
|
+
servers,
|
|
4545
|
+
gateScript: this.eventGate?.getStatus().script ?? null,
|
|
4546
|
+
hint:
|
|
4547
|
+
'Use these in gate.json policies (match.tagsAny / tagsAll / tagsNone) or ' +
|
|
4548
|
+
'in gate.js. Unknown/undeclared tags are tolerated (open ontologies).',
|
|
4549
|
+
};
|
|
4550
|
+
}
|
|
4551
|
+
|
|
4552
|
+
private dispatchEventTagsToolCall(agentName: string, call: ToolCall): void {
|
|
4553
|
+
this.emitTrace({ type: 'tool:started', module: 'gate', tool: call.name, callId: call.id, input: call.input });
|
|
4554
|
+
let result: import('./types/events.js').ToolResult;
|
|
4555
|
+
try {
|
|
4556
|
+
result = { success: true, data: this.buildEventTagsResult() };
|
|
4557
|
+
this.emitTrace({ type: 'tool:completed', module: 'gate', tool: call.name, callId: call.id, durationMs: 0 });
|
|
4558
|
+
} catch (error) {
|
|
4559
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
4560
|
+
this.emitTrace({ type: 'tool:failed', module: 'gate', tool: call.name, callId: call.id, error: err.message });
|
|
4561
|
+
result = { success: false, error: err.message, isError: true };
|
|
4562
|
+
}
|
|
4563
|
+
this.pushEvent({ type: 'tool-result', callId: call.id, agentName, moduleName: 'gate', result });
|
|
4564
|
+
}
|
|
4565
|
+
|
|
4087
4566
|
private dispatchGateToolCall(agentName: string, call: ToolCall): void {
|
|
4088
4567
|
this.emitTrace({ type: 'tool:started', module: 'gate', tool: call.name, callId: call.id, input: call.input });
|
|
4089
4568
|
const startTime = Date.now();
|
|
@@ -4117,7 +4596,13 @@ export class AgentFramework {
|
|
|
4117
4596
|
const inferenceId = `${agent.name}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
4118
4597
|
return {
|
|
4119
4598
|
inferenceId,
|
|
4120
|
-
|
|
4599
|
+
// Conversation identity = the agent (a trunk agent IS its own conversation;
|
|
4600
|
+
// forks get their own agent). The turn's channel LOCUS — the "proper
|
|
4601
|
+
// conversation tracking" this once flagged as a TODO — is now tracked
|
|
4602
|
+
// per-agent in `activeTriggerChannels` and surfaced to the agent via
|
|
4603
|
+
// buildChannelContext (channels.defaultOutgoing) below, so the agent is
|
|
4604
|
+
// told the same channel its speech will route to (item-3 redux).
|
|
4605
|
+
conversationId: agent.name,
|
|
4121
4606
|
turnIndex: 0, // Simplified; needs per-conversation counter TODO
|
|
4122
4607
|
userMessage: null, // Could extract from trigger context
|
|
4123
4608
|
model: {
|
|
@@ -4126,7 +4611,7 @@ export class AgentFramework {
|
|
|
4126
4611
|
contextWindow: 200000,
|
|
4127
4612
|
capabilities: ['tools'],
|
|
4128
4613
|
},
|
|
4129
|
-
channels: this.channelRegistry?.buildChannelContext(),
|
|
4614
|
+
channels: this.channelRegistry?.buildChannelContext(agent.name),
|
|
4130
4615
|
};
|
|
4131
4616
|
}
|
|
4132
4617
|
}
|