@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/dist/src/framework.js
CHANGED
|
@@ -15,6 +15,7 @@ import { ChannelRegistry } from './mcpl/channel-registry.js';
|
|
|
15
15
|
import { ConversationRouter } from './mcpl/conversation-router.js';
|
|
16
16
|
import { safeSlice } from './safe-slice.js';
|
|
17
17
|
import { toolResultDataToHistoryString } from './tool-result-history.js';
|
|
18
|
+
import { splitProseSegments } from './prose-segments.js';
|
|
18
19
|
import { CheckpointManager } from './mcpl/checkpoint-manager.js';
|
|
19
20
|
import { isToolAllowed } from './mcpl/tool-policy.js';
|
|
20
21
|
import { EventGate } from './gate/event-gate.js';
|
|
@@ -104,6 +105,16 @@ export class AgentFramework {
|
|
|
104
105
|
inferencePolicy;
|
|
105
106
|
errorPolicy;
|
|
106
107
|
pendingRequests = [];
|
|
108
|
+
/**
|
|
109
|
+
* Per-agent channel that triggered the agent's CURRENT inference turn, if any
|
|
110
|
+
* (item-3 redux). Read by the ChannelRegistry's `activeChannelResolver` to
|
|
111
|
+
* route a single-trunk agent's plain-text speech back to the channel it is
|
|
112
|
+
* answering, instead of the process-global most-recent-inbound locus that a
|
|
113
|
+
* concurrent message elsewhere can hijack. Set at turn start (or cleared for a
|
|
114
|
+
* heartbeat / no-trigger turn) in startAgentStream; overwritten by the next
|
|
115
|
+
* turn. Never read between turns (a given agent runs one turn at a time).
|
|
116
|
+
*/
|
|
117
|
+
activeTriggerChannels = new Map();
|
|
107
118
|
running = false;
|
|
108
119
|
loopPromise = null;
|
|
109
120
|
traceListeners = [];
|
|
@@ -127,6 +138,8 @@ export class AgentFramework {
|
|
|
127
138
|
// Undo/redo state
|
|
128
139
|
turnCounters = new Map(); // agentName → next turnIndex
|
|
129
140
|
redoStacks = new Map(); // agentName → redo entries
|
|
141
|
+
/** Liveness watchdog (fail hard on a wedged main thread). Null unless enabled. */
|
|
142
|
+
livenessWatchdog = null;
|
|
130
143
|
// MCPL subsystems (null when no mcplServers configured)
|
|
131
144
|
mcplServerRegistry = null;
|
|
132
145
|
featureSetManager = null;
|
|
@@ -185,6 +198,7 @@ export class AgentFramework {
|
|
|
185
198
|
queryMessages: (filter) => this.queryMessages(filter),
|
|
186
199
|
pushEvent: (event) => this.pushEvent(event),
|
|
187
200
|
onTrace: (listener) => this.onTrace(listener),
|
|
201
|
+
callTool: (call) => this.executeToolCall(call),
|
|
188
202
|
});
|
|
189
203
|
}
|
|
190
204
|
/**
|
|
@@ -301,6 +315,18 @@ export class AgentFramework {
|
|
|
301
315
|
getAgentNames: () => [...framework.agents.keys()],
|
|
302
316
|
});
|
|
303
317
|
}
|
|
318
|
+
// Liveness watchdog: fail hard if the main thread wedges (opt-in).
|
|
319
|
+
if (config.watchdog?.enabled) {
|
|
320
|
+
const { LivenessWatchdog } = await import('./runtime/liveness-watchdog.js');
|
|
321
|
+
framework.livenessWatchdog = new LivenessWatchdog({
|
|
322
|
+
enabled: true,
|
|
323
|
+
thresholdMs: config.watchdog.thresholdMs,
|
|
324
|
+
action: config.watchdog.action,
|
|
325
|
+
reportPath: config.watchdog.reportPath
|
|
326
|
+
?? (config.storePath ? join(config.storePath, 'watchdog-wedge.jsonl') : undefined),
|
|
327
|
+
});
|
|
328
|
+
framework.livenessWatchdog.start();
|
|
329
|
+
}
|
|
304
330
|
// Initialize MCPL subsystems if configured
|
|
305
331
|
if (config.mcplServers && config.mcplServers.length > 0) {
|
|
306
332
|
// Validate tool prefixes: no collisions with module names or between servers
|
|
@@ -371,6 +397,7 @@ export class AgentFramework {
|
|
|
371
397
|
this.channelRegistry?.stopAll();
|
|
372
398
|
// Dispose EventGate (clear debounce timers)
|
|
373
399
|
this.eventGate?.dispose();
|
|
400
|
+
this.livenessWatchdog?.stop();
|
|
374
401
|
// Stop modules and MCPL servers in parallel
|
|
375
402
|
const shutdownPromises = [this.moduleRegistry.stopAll()];
|
|
376
403
|
if (this.mcplServerRegistry) {
|
|
@@ -487,7 +514,12 @@ export class AgentFramework {
|
|
|
487
514
|
const moduleTools = this.moduleRegistry.getAllTools();
|
|
488
515
|
const channelTools = this.channelRegistry?.getChannelTools() ?? [];
|
|
489
516
|
const gateTools = this.eventGate
|
|
490
|
-
? [
|
|
517
|
+
? [
|
|
518
|
+
this.eventGate.getToolDefinition(),
|
|
519
|
+
...AgentFramework.SLEEP_TOOLS,
|
|
520
|
+
...AgentFramework.WAKE_RULE_TOOLS,
|
|
521
|
+
AgentFramework.EVENT_TAGS_TOOL,
|
|
522
|
+
]
|
|
491
523
|
: [];
|
|
492
524
|
if (this.mcplTools.length === 0 && channelTools.length === 0 && gateTools.length === 0) {
|
|
493
525
|
return moduleTools;
|
|
@@ -580,6 +612,38 @@ export class AgentFramework {
|
|
|
580
612
|
getModule(name) {
|
|
581
613
|
return this.moduleRegistry.getModule(name);
|
|
582
614
|
}
|
|
615
|
+
// =========================================================================
|
|
616
|
+
// Wake-rule surface (gate policy mutation) — backs wake_add_rule /
|
|
617
|
+
// wake_remove_rule and lets host modules compose higher-level wake modes
|
|
618
|
+
// (e.g. an "every-message-debounced" channel mode) without reaching into
|
|
619
|
+
// the private EventGate.
|
|
620
|
+
// =========================================================================
|
|
621
|
+
/**
|
|
622
|
+
* Add or replace (upsert) a gate policy at runtime. Validated and persisted
|
|
623
|
+
* to gate.json; hot-applied in memory. Throws if no gate is configured or the
|
|
624
|
+
* policy is invalid. `position: 'prepend'` puts a wake rule ahead of broad
|
|
625
|
+
* defer/debounce rules (first match wins).
|
|
626
|
+
*/
|
|
627
|
+
addGatePolicy(rawPolicy, options) {
|
|
628
|
+
if (!this.eventGate) {
|
|
629
|
+
throw new Error('No EventGate configured (FrameworkConfig.gate is unset).');
|
|
630
|
+
}
|
|
631
|
+
return this.eventGate.addPolicy(rawPolicy, options);
|
|
632
|
+
}
|
|
633
|
+
/**
|
|
634
|
+
* Remove a gate policy by name at runtime. Returns false if it didn't exist.
|
|
635
|
+
* Throws if no gate is configured.
|
|
636
|
+
*/
|
|
637
|
+
removeGatePolicy(name) {
|
|
638
|
+
if (!this.eventGate) {
|
|
639
|
+
throw new Error('No EventGate configured (FrameworkConfig.gate is unset).');
|
|
640
|
+
}
|
|
641
|
+
return this.eventGate.removePolicy(name);
|
|
642
|
+
}
|
|
643
|
+
/** Current gate policy names (freshest on-disk view). Empty when no gate. */
|
|
644
|
+
getGatePolicyNames() {
|
|
645
|
+
return this.eventGate?.listPolicyNames() ?? [];
|
|
646
|
+
}
|
|
583
647
|
/**
|
|
584
648
|
* Get the Membrane instance.
|
|
585
649
|
*/
|
|
@@ -828,6 +892,147 @@ export class AgentFramework {
|
|
|
828
892
|
inputSchema: { type: 'object' },
|
|
829
893
|
},
|
|
830
894
|
];
|
|
895
|
+
/** Synthesized wake-rule tools: add/remove a gate.json policy at runtime.
|
|
896
|
+
* Present when a gate is wired. They write validated policies into the
|
|
897
|
+
* hot-reloaded gate.json (same validation as load), so a rule takes effect
|
|
898
|
+
* immediately and survives restart. */
|
|
899
|
+
static WAKE_RULE_TOOLS = [
|
|
900
|
+
{
|
|
901
|
+
name: 'wake_add_rule',
|
|
902
|
+
description: 'Add or replace a wake rule (a gate.json policy) at runtime — no need to ' +
|
|
903
|
+
'hand-edit the file. The rule is validated and hot-applied immediately. ' +
|
|
904
|
+
'A rule with the same `name` replaces the existing one in place. Use ' +
|
|
905
|
+
'`position: "prepend"` to put a wake rule ahead of broad defer/debounce ' +
|
|
906
|
+
'rules (first match wins). Two common shapes:\n' +
|
|
907
|
+
'• Watch a FILE/workspace path: match on `mount` + `pathGlob`, e.g. ' +
|
|
908
|
+
'`{ name: "watch-notes", match: { scope: ["workspace:modified"], mount: "project", pathGlob: "notes/*.md" }, behavior: "always" }`.\n' +
|
|
909
|
+
'• Watch a CHANNEL: match on `source` + `channel` (and/or `tagsAny: ["chat:ambient"]`), ' +
|
|
910
|
+
'e.g. `{ name: "watch-cairn", match: { source: "discord", channel: "discord:*:12345", tagsAny: ["chat:ambient"] }, behavior: { debounce: 60000 } }` ' +
|
|
911
|
+
'(the channel must be subscribed for ambient events to arrive — see channel mode).',
|
|
912
|
+
inputSchema: {
|
|
913
|
+
type: 'object',
|
|
914
|
+
properties: {
|
|
915
|
+
name: { type: 'string', description: 'Unique rule name. Reusing a name replaces that rule.' },
|
|
916
|
+
match: {
|
|
917
|
+
type: 'object',
|
|
918
|
+
description: 'Match criteria (all AND together). Omitted fields match anything.',
|
|
919
|
+
properties: {
|
|
920
|
+
scope: { type: 'array', items: { type: 'string' }, description: 'Event types, e.g. ["mcpl:channel-incoming","workspace:modified"].' },
|
|
921
|
+
source: { type: 'string', description: 'Integration/serverId, glob ok (e.g. "discord").' },
|
|
922
|
+
channel: { type: 'string', description: 'Channel id, glob ok (e.g. "discord:*:12345").' },
|
|
923
|
+
mount: { type: 'string', description: 'Workspace mount name, glob ok (workspace:* events).' },
|
|
924
|
+
pathGlob: { type: 'string', description: 'Glob over touched paths (workspace:* events).' },
|
|
925
|
+
tagsAny: { type: 'array', items: { type: 'string' }, description: 'Match if ANY tag matches (globs ok).' },
|
|
926
|
+
tagsAll: { type: 'array', items: { type: 'string' }, description: 'Match only if EVERY tag matches.' },
|
|
927
|
+
tagsNone: { type: 'array', items: { type: 'string' }, description: 'Match only if NONE match.' },
|
|
928
|
+
metadataTrue: { type: 'array', items: { type: 'string' }, description: 'Match if ANY listed metadata field is truthy.' },
|
|
929
|
+
filter: {
|
|
930
|
+
type: 'object',
|
|
931
|
+
description: 'Content filter.',
|
|
932
|
+
properties: {
|
|
933
|
+
type: { type: 'string', enum: ['text', 'regex'] },
|
|
934
|
+
pattern: { type: 'string' },
|
|
935
|
+
},
|
|
936
|
+
required: ['type', 'pattern'],
|
|
937
|
+
},
|
|
938
|
+
},
|
|
939
|
+
},
|
|
940
|
+
behavior: {
|
|
941
|
+
type: 'string',
|
|
942
|
+
enum: ['always', 'defer', 'skip'],
|
|
943
|
+
description: 'Simple behavior: "always" (wake now) or "defer" (don\'t wake; still enters ' +
|
|
944
|
+
'context). For debounce / rate-limit / sampling, use debounceMs / rateLimit / ' +
|
|
945
|
+
'passiveSample below instead of this field. Exactly one behavior must be given.',
|
|
946
|
+
},
|
|
947
|
+
debounceMs: {
|
|
948
|
+
type: 'number',
|
|
949
|
+
description: 'Shorthand for { debounce: ms }: wake once after ms of quiet (100–300000).',
|
|
950
|
+
},
|
|
951
|
+
rateLimit: {
|
|
952
|
+
type: 'object',
|
|
953
|
+
description: 'Token-bucket wake: at most `tokens` wakes per window; refills one per refillIntervalMs.',
|
|
954
|
+
properties: {
|
|
955
|
+
tokens: { type: 'number', description: 'Bucket capacity (> 0).' },
|
|
956
|
+
refillIntervalMs: { type: 'number', description: 'Ms between token refills (> 0).' },
|
|
957
|
+
keyBy: { type: 'string', description: 'Metadata field to partition buckets by (e.g. "channelId").' },
|
|
958
|
+
},
|
|
959
|
+
required: ['tokens', 'refillIntervalMs'],
|
|
960
|
+
},
|
|
961
|
+
passiveSample: {
|
|
962
|
+
type: 'object',
|
|
963
|
+
description: 'Wake every Nth matching event.',
|
|
964
|
+
properties: {
|
|
965
|
+
every: { type: 'number', description: 'Fire every N matches (positive integer).' },
|
|
966
|
+
keyBy: { type: 'string', description: 'Metadata field for separate per-key counters.' },
|
|
967
|
+
},
|
|
968
|
+
required: ['every'],
|
|
969
|
+
},
|
|
970
|
+
position: {
|
|
971
|
+
type: 'string',
|
|
972
|
+
enum: ['append', 'prepend'],
|
|
973
|
+
description: 'Where to insert a NEW rule (ignored when replacing by name). Default "append".',
|
|
974
|
+
},
|
|
975
|
+
resets: {
|
|
976
|
+
type: 'array',
|
|
977
|
+
items: { type: 'string' },
|
|
978
|
+
description: 'Names of other rules whose runtime state to clear when this rule fires.',
|
|
979
|
+
},
|
|
980
|
+
},
|
|
981
|
+
required: ['name'],
|
|
982
|
+
},
|
|
983
|
+
},
|
|
984
|
+
{
|
|
985
|
+
name: 'wake_remove_rule',
|
|
986
|
+
description: 'Remove a wake rule (gate.json policy) by name at runtime. Any pending ' +
|
|
987
|
+
'debounce batch for that rule is delivered first. Returns whether a rule ' +
|
|
988
|
+
'was removed (false if no rule had that name).',
|
|
989
|
+
inputSchema: {
|
|
990
|
+
type: 'object',
|
|
991
|
+
properties: {
|
|
992
|
+
name: { type: 'string', description: 'The rule name to remove.' },
|
|
993
|
+
},
|
|
994
|
+
required: ['name'],
|
|
995
|
+
},
|
|
996
|
+
},
|
|
997
|
+
];
|
|
998
|
+
/** Discoverability for event tags (MCPL RFC-001): the reserved chat:* core,
|
|
999
|
+
* each connected server's declared tag ontology, and gate.js status. */
|
|
1000
|
+
static EVENT_TAGS_TOOL = {
|
|
1001
|
+
name: 'event_tags',
|
|
1002
|
+
description: 'List the event tags available for gating: the reserved cross-platform ' +
|
|
1003
|
+
'chat:* core, each connected server\'s declared tag ontology (descriptions, ' +
|
|
1004
|
+
'implications, suggested treatments), and the status of your programmable ' +
|
|
1005
|
+
'gate (gate.js). Use these tag names in gate.json policies (tagsAny / ' +
|
|
1006
|
+
'tagsAll / tagsNone) or in gate.js.',
|
|
1007
|
+
inputSchema: { type: 'object' },
|
|
1008
|
+
};
|
|
1009
|
+
/** Reserved chat:* core vocabulary (MCPL RFC-001 §4) — short descriptions so
|
|
1010
|
+
* the agent can author rules without reading the spec. */
|
|
1011
|
+
static CHAT_CORE_TAGS = {
|
|
1012
|
+
'chat:addressed': 'Directed at you (umbrella: dm/mention/reply)',
|
|
1013
|
+
'chat:mention': 'You were explicitly @-mentioned',
|
|
1014
|
+
'chat:reply': 'A reply to your own message',
|
|
1015
|
+
'chat:dm': 'A direct/private message to you',
|
|
1016
|
+
'chat:ambient': 'Overheard in a followed channel; not addressed',
|
|
1017
|
+
'chat:broadcast': 'Channel-wide ping (@everyone / channel post)',
|
|
1018
|
+
'chat:to-self': 'Acts on your own content (reaction/reply to you)',
|
|
1019
|
+
'chat:from-human': 'Authored by a human',
|
|
1020
|
+
'chat:from-bot': 'Authored by a bot/automation',
|
|
1021
|
+
'chat:from-self': 'Your own message, echoed back',
|
|
1022
|
+
'chat:from-agent': 'Authored by another persona/agent',
|
|
1023
|
+
'chat:edited': 'An edit of an existing message',
|
|
1024
|
+
'chat:deleted': 'A deletion',
|
|
1025
|
+
'chat:reaction': 'An emoji reaction was added',
|
|
1026
|
+
'chat:reaction-remove': 'A reaction was removed',
|
|
1027
|
+
'chat:has-image': 'Has an image attachment',
|
|
1028
|
+
'chat:has-audio': 'Has an audio attachment',
|
|
1029
|
+
'chat:has-file': 'Has a file attachment',
|
|
1030
|
+
'chat:has-link': 'Contains a link',
|
|
1031
|
+
'chat:command': 'A slash/bot command invocation',
|
|
1032
|
+
'chat:private': 'Private conversation',
|
|
1033
|
+
'chat:group': 'Group (multi-party) conversation',
|
|
1034
|
+
'chat:thread': 'Occurred in a thread',
|
|
1035
|
+
};
|
|
831
1036
|
/** Refusal category → Discord reaction emoji. Unknown categories get 🛑. */
|
|
832
1037
|
static REFUSAL_REACTIONS = {
|
|
833
1038
|
bio: '☣️',
|
|
@@ -1722,6 +1927,9 @@ export class AgentFramework {
|
|
|
1722
1927
|
reason: 'mcpl:channel-incoming',
|
|
1723
1928
|
source: event.serverId,
|
|
1724
1929
|
timestamp: Date.now(),
|
|
1930
|
+
// Route this turn's auto-published speech back to THIS channel, not
|
|
1931
|
+
// the global most-recent-inbound locus (item-3 redux, trunk agents).
|
|
1932
|
+
channelId: event.channelId,
|
|
1725
1933
|
});
|
|
1726
1934
|
}
|
|
1727
1935
|
}
|
|
@@ -1800,6 +2008,9 @@ export class AgentFramework {
|
|
|
1800
2008
|
reason: 'mcpl:channel-incoming',
|
|
1801
2009
|
source: event.serverId,
|
|
1802
2010
|
timestamp: Date.now(),
|
|
2011
|
+
// A fork's home channel wins in routeSpeech regardless, but carry the
|
|
2012
|
+
// triggering channel too so the trunk/active path stays consistent.
|
|
2013
|
+
channelId: event.channelId,
|
|
1803
2014
|
});
|
|
1804
2015
|
}
|
|
1805
2016
|
}
|
|
@@ -1961,6 +2172,15 @@ export class AgentFramework {
|
|
|
1961
2172
|
};
|
|
1962
2173
|
const id = this.addMessage('user', event.content, metadata);
|
|
1963
2174
|
this.emitTrace({ type: 'message:added', messageId: id, source: 'mcpl:push-event' });
|
|
2175
|
+
// Some push events carry a channel of origin (Discord DMs arrive here, not
|
|
2176
|
+
// via channels/incoming, because discord-mcpl forwards them with the channel
|
|
2177
|
+
// closed). Register + open that channel so routeSpeech can resolve it, and
|
|
2178
|
+
// thread it onto the wake request so the reply routes back to it instead of
|
|
2179
|
+
// the global locus (item-3 redux, DM sub-case).
|
|
2180
|
+
const triggerChannel = this.derivePushEventChannel(event.origin);
|
|
2181
|
+
if (triggerChannel && this.channelRegistry) {
|
|
2182
|
+
this.channelRegistry.ensureChannelRegistered(event.serverId, triggerChannel.channelId, triggerChannel.label);
|
|
2183
|
+
}
|
|
1964
2184
|
if (event.triggerInference) {
|
|
1965
2185
|
// Default broadcast excludes conversation forks (channel-driven).
|
|
1966
2186
|
const targetAgents = event.targetAgents
|
|
@@ -1971,10 +2191,43 @@ export class AgentFramework {
|
|
|
1971
2191
|
reason: 'mcpl:push-event',
|
|
1972
2192
|
source: event.serverId,
|
|
1973
2193
|
timestamp: Date.now(),
|
|
2194
|
+
channelId: triggerChannel?.channelId,
|
|
1974
2195
|
});
|
|
1975
2196
|
}
|
|
1976
2197
|
}
|
|
1977
2198
|
}
|
|
2199
|
+
/**
|
|
2200
|
+
* Derive the MCPL composite channel id (the outbound routing locus) for a
|
|
2201
|
+
* push event from its server-defined `origin`, if it carries one.
|
|
2202
|
+
*
|
|
2203
|
+
* Discord DMs are the motivating case: discord-mcpl forwards them via
|
|
2204
|
+
* push/event (the DM channel is closed), so they never pass through
|
|
2205
|
+
* channels/incoming and their channel is never registered — leaving the
|
|
2206
|
+
* agent's reply with nowhere to route (item-3 redux, DM sub-case). Prefers an
|
|
2207
|
+
* explicit `origin.mcplChannelId` (a surface declaring its own composite id —
|
|
2208
|
+
* the surface-agnostic contract); otherwise reconstructs the Discord form
|
|
2209
|
+
* `discord:{guildId|dm}:{channelId}` from origin fields, matching
|
|
2210
|
+
* discord-mcpl's `mcplChannelId()` / `parseMcplChannelId()` convention so the
|
|
2211
|
+
* fix works even against a discord-mcpl build that predates `mcplChannelId`.
|
|
2212
|
+
* Returns undefined for push events with no channel provenance (heartbeats,
|
|
2213
|
+
* timers), which correctly keep the global fallback.
|
|
2214
|
+
*/
|
|
2215
|
+
derivePushEventChannel(origin) {
|
|
2216
|
+
if (!origin)
|
|
2217
|
+
return undefined;
|
|
2218
|
+
const label = typeof origin.channelName === 'string' ? origin.channelName : undefined;
|
|
2219
|
+
const explicit = origin.mcplChannelId;
|
|
2220
|
+
if (typeof explicit === 'string' && explicit) {
|
|
2221
|
+
return { channelId: explicit, label };
|
|
2222
|
+
}
|
|
2223
|
+
// Discord fallback: reconstruct the composite from origin parts. `guildId`
|
|
2224
|
+
// is null for a DM (→ 'dm'); a real guild id for a non-open guild channel.
|
|
2225
|
+
if (origin.source === 'discord' && typeof origin.channelId === 'string' && origin.channelId) {
|
|
2226
|
+
const guild = typeof origin.guildId === 'string' && origin.guildId ? origin.guildId : 'dm';
|
|
2227
|
+
return { channelId: `discord:${guild}:${origin.channelId}`, label };
|
|
2228
|
+
}
|
|
2229
|
+
return undefined;
|
|
2230
|
+
}
|
|
1978
2231
|
async processInferenceRequests() {
|
|
1979
2232
|
if (this.pendingRequests.length === 0) {
|
|
1980
2233
|
return;
|
|
@@ -2028,7 +2281,16 @@ export class AgentFramework {
|
|
|
2028
2281
|
}
|
|
2029
2282
|
// Start streaming inference (non-blocking — driveStream runs in background)
|
|
2030
2283
|
const trigger = requests[0];
|
|
2031
|
-
|
|
2284
|
+
// Route this turn's auto-published speech to the channel that triggered
|
|
2285
|
+
// it (item-3 redux). A batched wake may carry several triggering channels
|
|
2286
|
+
// (messages arrived in >1 channel while the agent was busy/idle) — reply
|
|
2287
|
+
// in the MOST-RECENT one: it matches the legacy last-inbound semantics and
|
|
2288
|
+
// is the message the agent is most likely answering. The common case is
|
|
2289
|
+
// 1 message → 1 channel (unambiguous). Non-channel wakes carry no
|
|
2290
|
+
// channelId, leaving the field undefined → global fallback. `reduce`
|
|
2291
|
+
// keeps the last defined channelId across the (FIFO-ordered) batch.
|
|
2292
|
+
const triggerChannel = requests.reduce((acc, r) => r.channelId ?? acc, undefined);
|
|
2293
|
+
await this.startAgentStream(agent, { ...trigger, channelId: triggerChannel });
|
|
2032
2294
|
}
|
|
2033
2295
|
}
|
|
2034
2296
|
async startAgentStream(agent, trigger, attempt = 0) {
|
|
@@ -2037,6 +2299,18 @@ export class AgentFramework {
|
|
|
2037
2299
|
this.recordTurnCheckpoint(agent.name);
|
|
2038
2300
|
this.redoStacks.delete(agent.name); // new work invalidates redo
|
|
2039
2301
|
}
|
|
2302
|
+
// Establish this turn's outbound routing locus (item-3 redux). Set it to the
|
|
2303
|
+
// triggering channel for a channel/DM-triggered turn; clear it otherwise so
|
|
2304
|
+
// a heartbeat / no-trigger turn doesn't inherit a previous turn's channel and
|
|
2305
|
+
// instead falls back to the global default. A given agent runs one turn at a
|
|
2306
|
+
// time, so a set here is only read during THIS turn's routeSpeech /
|
|
2307
|
+
// buildChannelContext; retries re-run with the same trigger, re-setting it.
|
|
2308
|
+
if (trigger?.channelId) {
|
|
2309
|
+
this.activeTriggerChannels.set(agent.name, trigger.channelId);
|
|
2310
|
+
}
|
|
2311
|
+
else {
|
|
2312
|
+
this.activeTriggerChannels.delete(agent.name);
|
|
2313
|
+
}
|
|
2040
2314
|
this.emitTrace({ type: 'inference:started', agentName: agent.name });
|
|
2041
2315
|
this.eventGate?.onInferenceStarted(agent.name);
|
|
2042
2316
|
try {
|
|
@@ -2401,21 +2675,32 @@ export class AgentFramework {
|
|
|
2401
2675
|
.map((b) => b.name)
|
|
2402
2676
|
.filter((n) => typeof n === 'string');
|
|
2403
2677
|
const silenced = toolNames.some((n) => SILENCING.has(bare(n)));
|
|
2404
|
-
|
|
2405
|
-
|
|
2406
|
-
|
|
2407
|
-
|
|
2408
|
-
|
|
2678
|
+
// Split the turn's content into ordered prose segments, broken at
|
|
2679
|
+
// each tool_use / tool_result boundary. `response.content` holds the
|
|
2680
|
+
// WHOLE turn's blocks in provider order — earlier tool rounds
|
|
2681
|
+
// included (see the trailing-slice note above, which slices exactly
|
|
2682
|
+
// because the full turn is present) — so a left-to-right walk
|
|
2683
|
+
// reconstructs the emission order. Interleaved prose
|
|
2684
|
+
// ("msgA → [tool] → msgB → [tool] → msgC") is then delivered as
|
|
2685
|
+
// separate messages IN ORDER instead of being collapsed into one
|
|
2686
|
+
// trailing post (item 4). The silencing rule stays turn-wide: an
|
|
2687
|
+
// explicit send / skip suppresses ALL auto-routed prose this turn,
|
|
2688
|
+
// preserving the double-post guard.
|
|
2689
|
+
const segments = splitProseSegments(response.content);
|
|
2690
|
+
if (silenced || segments.length === 0) {
|
|
2409
2691
|
console.error(`[routing] ${agent.name}: tool-call turn [${toolNames.join(', ') || 'none'}] -> NOT routed ` +
|
|
2410
2692
|
`(${silenced ? 'silencing tool / explicit send' : 'no prose'})`);
|
|
2411
2693
|
}
|
|
2412
2694
|
else {
|
|
2413
|
-
console.error(`[routing] ${agent.name}: tool-call turn [${toolNames.join(', ')}] -> routing
|
|
2414
|
-
|
|
2415
|
-
|
|
2416
|
-
|
|
2417
|
-
|
|
2418
|
-
|
|
2695
|
+
console.error(`[routing] ${agent.name}: tool-call turn [${toolNames.join(', ')}] -> routing ${segments.length} prose segment(s) in order`);
|
|
2696
|
+
// Deliver sequentially (await each) so the segments land in order.
|
|
2697
|
+
for (const seg of segments) {
|
|
2698
|
+
try {
|
|
2699
|
+
await this.channelRegistry.routeSpeech(agent.name, seg);
|
|
2700
|
+
}
|
|
2701
|
+
catch (err) {
|
|
2702
|
+
console.error('speech routing failed:', err);
|
|
2703
|
+
}
|
|
2419
2704
|
}
|
|
2420
2705
|
}
|
|
2421
2706
|
}
|
|
@@ -2731,6 +3016,16 @@ export class AgentFramework {
|
|
|
2731
3016
|
this.dispatchSleepToolCall(agentName, enrichedCall);
|
|
2732
3017
|
return;
|
|
2733
3018
|
}
|
|
3019
|
+
// Route wake-rule tools (runtime gate.json policy add/remove)
|
|
3020
|
+
if ((enrichedCall.name === 'wake_add_rule' || enrichedCall.name === 'wake_remove_rule') && this.eventGate) {
|
|
3021
|
+
this.dispatchWakeRuleToolCall(agentName, enrichedCall);
|
|
3022
|
+
return;
|
|
3023
|
+
}
|
|
3024
|
+
// Route event_tags (tag/ontology discovery)
|
|
3025
|
+
if (enrichedCall.name === 'event_tags' && this.eventGate) {
|
|
3026
|
+
this.dispatchEventTagsToolCall(agentName, enrichedCall);
|
|
3027
|
+
return;
|
|
3028
|
+
}
|
|
2734
3029
|
// Route synthesized 'think' (private reasoning) and 'skip_reply' (deliberate
|
|
2735
3030
|
// stay-silent) tools — handled by the channel registry like the other
|
|
2736
3031
|
// synthesized channel tools, but they aren't `channel_`-prefixed so they
|
|
@@ -2925,7 +3220,7 @@ export class AgentFramework {
|
|
|
2925
3220
|
console.error(`[inference-failed] agent=${agentName} consecutive=${streak}: ${reason}`);
|
|
2926
3221
|
// (2) Agent-facing chronicle marker (no inference triggered → no loop).
|
|
2927
3222
|
const agent = this.agents.get(agentName);
|
|
2928
|
-
if (agent) {
|
|
3223
|
+
if (agent && process.env.SUPPRESS_INFERENCE_FAILED_MARKER !== '1') {
|
|
2929
3224
|
try {
|
|
2930
3225
|
agent.getContextManager().addMessage('user', [{
|
|
2931
3226
|
type: 'text',
|
|
@@ -2986,6 +3281,21 @@ export class AgentFramework {
|
|
|
2986
3281
|
}
|
|
2987
3282
|
},
|
|
2988
3283
|
shouldTriggerInference: triggerFilter,
|
|
3284
|
+
// Route a conversation fork's plain-text speech to its HOME channel, not
|
|
3285
|
+
// the process-global most-recent-inbound locus (item 3). The trunk agent
|
|
3286
|
+
// has no home entry, so this returns undefined and routeSpeech falls back
|
|
3287
|
+
// to defaultPublishChannel. `conversationAgentHomes` is the permanent
|
|
3288
|
+
// spawn-time binding; `channelForAgent` is the router's live binding as a
|
|
3289
|
+
// belt-and-suspenders fallback.
|
|
3290
|
+
homeChannelResolver: (agentName) => this.conversationAgentHomes.get(agentName)
|
|
3291
|
+
?? this.conversationRouter?.channelForAgent(agentName),
|
|
3292
|
+
// Route a single TRUNK agent's plain-text speech to the channel that
|
|
3293
|
+
// triggered its CURRENT turn (item-3 redux). connectome-host runs every
|
|
3294
|
+
// agent as a trunk (it never exposes conversation forks), so without this
|
|
3295
|
+
// a reply falls back to the process-global most-recent-inbound locus and
|
|
3296
|
+
// a concurrent message in another channel hijacks it. Empty for
|
|
3297
|
+
// heartbeat / no-trigger turns → correct global fallback.
|
|
3298
|
+
activeChannelResolver: (agentName) => this.activeTriggerChannels.get(agentName),
|
|
2989
3299
|
// A text-only turn whose speech couldn't be delivered must not vanish
|
|
2990
3300
|
// silently: record a `[discord-send-failed]` marker in chronicle so the
|
|
2991
3301
|
// agent sees, on her next turn, that her reply never reached the human.
|
|
@@ -3262,15 +3572,21 @@ export class AgentFramework {
|
|
|
3262
3572
|
this.emitTrace({ type: 'tool:started', module: `mcpl:${serverId}`, tool: toolName, callId: call.id, input: call.input });
|
|
3263
3573
|
const startTime = Date.now();
|
|
3264
3574
|
const args = (call.input && typeof call.input === 'object') ? call.input : {};
|
|
3265
|
-
// Build state params for stateful tools (Step 8)
|
|
3575
|
+
// Build state params for stateful tools (Step 8).
|
|
3576
|
+
// Host-managed state is single-valued per server, so inject the host-managed
|
|
3577
|
+
// set's `state` for any call; otherwise fall back to a server-managed set's
|
|
3578
|
+
// opaque `checkpoint`. Never blindly pick "first stateful" — when a server
|
|
3579
|
+
// mixes host-managed (e.g. a feed) and server-managed (e.g. post/undo) sets,
|
|
3580
|
+
// that misattributes the feed's state.
|
|
3266
3581
|
let stateParams;
|
|
3267
3582
|
if (this.checkpointManager) {
|
|
3268
|
-
const
|
|
3269
|
-
if (
|
|
3270
|
-
|
|
3271
|
-
|
|
3272
|
-
|
|
3273
|
-
|
|
3583
|
+
const hostFs = this.checkpointManager.getHostManagedFeatureSet(serverId);
|
|
3584
|
+
if (hostFs) {
|
|
3585
|
+
stateParams = { state: this.checkpointManager.getCurrentState(serverId, hostFs) };
|
|
3586
|
+
}
|
|
3587
|
+
else {
|
|
3588
|
+
const fs = this.checkpointManager.getStatefulFeatureSet(serverId);
|
|
3589
|
+
if (fs) {
|
|
3274
3590
|
const cp = this.checkpointManager.getCurrentCheckpoint(serverId, fs);
|
|
3275
3591
|
if (cp)
|
|
3276
3592
|
stateParams = { checkpoint: cp };
|
|
@@ -3281,10 +3597,17 @@ export class AgentFramework {
|
|
|
3281
3597
|
.then((result) => {
|
|
3282
3598
|
const durationMs = Date.now() - startTime;
|
|
3283
3599
|
this.emitTrace({ type: 'tool:completed', module: `mcpl:${serverId}`, tool: toolName, callId: call.id, durationMs });
|
|
3284
|
-
// Record checkpoint from stateful tool response (Step 8)
|
|
3600
|
+
// Record checkpoint from stateful tool response (Step 8).
|
|
3601
|
+
// Attribute to the set the server tagged (result.state.featureSet) first;
|
|
3602
|
+
// else the host-managed set (host-managed checkpoints carry data/patch);
|
|
3603
|
+
// else the single stateful set. Avoids recording a feed checkpoint onto a
|
|
3604
|
+
// server-managed set just because it registered first.
|
|
3285
3605
|
if (result.state && this.checkpointManager) {
|
|
3286
|
-
const
|
|
3287
|
-
|
|
3606
|
+
const tagged = result.state.featureSet;
|
|
3607
|
+
const fs = tagged
|
|
3608
|
+
?? this.checkpointManager.getHostManagedFeatureSet(serverId)
|
|
3609
|
+
?? this.checkpointManager.getStatefulFeatureSet(serverId);
|
|
3610
|
+
if (fs && this.checkpointManager.isStateful(serverId, fs)) {
|
|
3288
3611
|
this.checkpointManager.recordCheckpoint(serverId, fs, result.state);
|
|
3289
3612
|
}
|
|
3290
3613
|
}
|
|
@@ -3398,6 +3721,84 @@ export class AgentFramework {
|
|
|
3398
3721
|
});
|
|
3399
3722
|
});
|
|
3400
3723
|
}
|
|
3724
|
+
/**
|
|
3725
|
+
* Handle the synthesized `wake_add_rule` / `wake_remove_rule` tools: mutate
|
|
3726
|
+
* the hot-reloaded gate.json at runtime. Validation lives in the EventGate
|
|
3727
|
+
* (same path as gate.json load), so an invalid rule is surfaced as a tool
|
|
3728
|
+
* error and nothing is written.
|
|
3729
|
+
*/
|
|
3730
|
+
dispatchWakeRuleToolCall(agentName, call) {
|
|
3731
|
+
this.emitTrace({ type: 'tool:started', module: 'gate', tool: call.name, callId: call.id, input: call.input });
|
|
3732
|
+
const finish = (result) => {
|
|
3733
|
+
this.emitTrace({
|
|
3734
|
+
type: result.isError ? 'tool:failed' : 'tool:completed',
|
|
3735
|
+
module: 'gate', tool: call.name, callId: call.id, durationMs: 0,
|
|
3736
|
+
...(result.isError ? { error: result.error } : {}),
|
|
3737
|
+
});
|
|
3738
|
+
this.pushEvent({ type: 'tool-result', callId: call.id, agentName, moduleName: 'gate', result });
|
|
3739
|
+
};
|
|
3740
|
+
try {
|
|
3741
|
+
if (call.name === 'wake_remove_rule') {
|
|
3742
|
+
const input = (call.input ?? {});
|
|
3743
|
+
if (typeof input.name !== 'string' || !input.name) {
|
|
3744
|
+
finish({ success: false, error: 'wake_remove_rule: `name` (string) is required', isError: true });
|
|
3745
|
+
return;
|
|
3746
|
+
}
|
|
3747
|
+
const removed = this.removeGatePolicy(input.name);
|
|
3748
|
+
finish({
|
|
3749
|
+
success: true,
|
|
3750
|
+
data: { removed, name: input.name, policies: this.getGatePolicyNames() },
|
|
3751
|
+
});
|
|
3752
|
+
return;
|
|
3753
|
+
}
|
|
3754
|
+
// wake_add_rule — assemble the canonical behavior from the typed fields
|
|
3755
|
+
// (exactly one), then let the gate's own validator do the authoritative
|
|
3756
|
+
// range/shape checks.
|
|
3757
|
+
const input = (call.input ?? {});
|
|
3758
|
+
const behaviorSources = [
|
|
3759
|
+
input.behavior !== undefined ? 'behavior' : null,
|
|
3760
|
+
input.debounceMs !== undefined ? 'debounceMs' : null,
|
|
3761
|
+
input.rateLimit !== undefined ? 'rateLimit' : null,
|
|
3762
|
+
input.passiveSample !== undefined ? 'passiveSample' : null,
|
|
3763
|
+
].filter((s) => s !== null);
|
|
3764
|
+
if (behaviorSources.length === 0) {
|
|
3765
|
+
finish({
|
|
3766
|
+
success: false,
|
|
3767
|
+
error: 'wake_add_rule: specify exactly one behavior — `behavior` ("always"/"defer"/"skip"), `debounceMs`, `rateLimit`, or `passiveSample`.',
|
|
3768
|
+
isError: true,
|
|
3769
|
+
});
|
|
3770
|
+
return;
|
|
3771
|
+
}
|
|
3772
|
+
if (behaviorSources.length > 1) {
|
|
3773
|
+
finish({
|
|
3774
|
+
success: false,
|
|
3775
|
+
error: `wake_add_rule: give only one behavior, got [${behaviorSources.join(', ')}].`,
|
|
3776
|
+
isError: true,
|
|
3777
|
+
});
|
|
3778
|
+
return;
|
|
3779
|
+
}
|
|
3780
|
+
const behavior = input.debounceMs !== undefined ? { debounce: input.debounceMs }
|
|
3781
|
+
: input.rateLimit !== undefined ? { rate_limit: input.rateLimit }
|
|
3782
|
+
: input.passiveSample !== undefined ? { passive_sample: input.passiveSample }
|
|
3783
|
+
: input.behavior;
|
|
3784
|
+
const position = input.position === 'prepend' ? 'prepend' : 'append';
|
|
3785
|
+
const rawPolicy = {
|
|
3786
|
+
name: input.name,
|
|
3787
|
+
match: input.match ?? {},
|
|
3788
|
+
behavior,
|
|
3789
|
+
...(input.resets !== undefined ? { resets: input.resets } : {}),
|
|
3790
|
+
};
|
|
3791
|
+
const policy = this.addGatePolicy(rawPolicy, { position });
|
|
3792
|
+
finish({
|
|
3793
|
+
success: true,
|
|
3794
|
+
data: { added: policy.name, policies: this.getGatePolicyNames() },
|
|
3795
|
+
});
|
|
3796
|
+
}
|
|
3797
|
+
catch (err) {
|
|
3798
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
3799
|
+
finish({ success: false, error: msg, isError: true });
|
|
3800
|
+
}
|
|
3801
|
+
}
|
|
3401
3802
|
/**
|
|
3402
3803
|
* Handle the synthesized `sleep` / `wake` tools. `sleep` arms the gate's
|
|
3403
3804
|
* suppression window, optionally announces in the sticky channel, and ends
|
|
@@ -3437,6 +3838,42 @@ export class AgentFramework {
|
|
|
3437
3838
|
// endTurn: the agent stops here and goes idle for the duration.
|
|
3438
3839
|
finish({ success: true, data: { sleepingFor: human, until }, endTurn: true });
|
|
3439
3840
|
}
|
|
3841
|
+
/** Aggregate the event-tag vocabulary: reserved chat:* core + each connected
|
|
3842
|
+
* server's declared tag ontology + gate.js status. */
|
|
3843
|
+
buildEventTagsResult() {
|
|
3844
|
+
const servers = {};
|
|
3845
|
+
for (const conn of this.mcplServerRegistry?.getAllServers() ?? []) {
|
|
3846
|
+
const declared = this.featureSetManager?.getDeclaredFeatureSets(conn.id) ?? {};
|
|
3847
|
+
const sets = {};
|
|
3848
|
+
for (const [name, decl] of Object.entries(declared)) {
|
|
3849
|
+
if (decl.tagOntology)
|
|
3850
|
+
sets[name] = decl.tagOntology;
|
|
3851
|
+
}
|
|
3852
|
+
if (Object.keys(sets).length > 0)
|
|
3853
|
+
servers[conn.id] = sets;
|
|
3854
|
+
}
|
|
3855
|
+
return {
|
|
3856
|
+
core: AgentFramework.CHAT_CORE_TAGS,
|
|
3857
|
+
servers,
|
|
3858
|
+
gateScript: this.eventGate?.getStatus().script ?? null,
|
|
3859
|
+
hint: 'Use these in gate.json policies (match.tagsAny / tagsAll / tagsNone) or ' +
|
|
3860
|
+
'in gate.js. Unknown/undeclared tags are tolerated (open ontologies).',
|
|
3861
|
+
};
|
|
3862
|
+
}
|
|
3863
|
+
dispatchEventTagsToolCall(agentName, call) {
|
|
3864
|
+
this.emitTrace({ type: 'tool:started', module: 'gate', tool: call.name, callId: call.id, input: call.input });
|
|
3865
|
+
let result;
|
|
3866
|
+
try {
|
|
3867
|
+
result = { success: true, data: this.buildEventTagsResult() };
|
|
3868
|
+
this.emitTrace({ type: 'tool:completed', module: 'gate', tool: call.name, callId: call.id, durationMs: 0 });
|
|
3869
|
+
}
|
|
3870
|
+
catch (error) {
|
|
3871
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
3872
|
+
this.emitTrace({ type: 'tool:failed', module: 'gate', tool: call.name, callId: call.id, error: err.message });
|
|
3873
|
+
result = { success: false, error: err.message, isError: true };
|
|
3874
|
+
}
|
|
3875
|
+
this.pushEvent({ type: 'tool-result', callId: call.id, agentName, moduleName: 'gate', result });
|
|
3876
|
+
}
|
|
3440
3877
|
dispatchGateToolCall(agentName, call) {
|
|
3441
3878
|
this.emitTrace({ type: 'tool:started', module: 'gate', tool: call.name, callId: call.id, input: call.input });
|
|
3442
3879
|
const startTime = Date.now();
|
|
@@ -3468,7 +3905,13 @@ export class AgentFramework {
|
|
|
3468
3905
|
const inferenceId = `${agent.name}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
3469
3906
|
return {
|
|
3470
3907
|
inferenceId,
|
|
3471
|
-
|
|
3908
|
+
// Conversation identity = the agent (a trunk agent IS its own conversation;
|
|
3909
|
+
// forks get their own agent). The turn's channel LOCUS — the "proper
|
|
3910
|
+
// conversation tracking" this once flagged as a TODO — is now tracked
|
|
3911
|
+
// per-agent in `activeTriggerChannels` and surfaced to the agent via
|
|
3912
|
+
// buildChannelContext (channels.defaultOutgoing) below, so the agent is
|
|
3913
|
+
// told the same channel its speech will route to (item-3 redux).
|
|
3914
|
+
conversationId: agent.name,
|
|
3472
3915
|
turnIndex: 0, // Simplified; needs per-conversation counter TODO
|
|
3473
3916
|
userMessage: null, // Could extract from trigger context
|
|
3474
3917
|
model: {
|
|
@@ -3477,7 +3920,7 @@ export class AgentFramework {
|
|
|
3477
3920
|
contextWindow: 200000,
|
|
3478
3921
|
capabilities: ['tools'],
|
|
3479
3922
|
},
|
|
3480
|
-
channels: this.channelRegistry?.buildChannelContext(),
|
|
3923
|
+
channels: this.channelRegistry?.buildChannelContext(agent.name),
|
|
3481
3924
|
};
|
|
3482
3925
|
}
|
|
3483
3926
|
}
|