@animalabs/agent-framework 0.6.5 → 0.6.6
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/agent.d.ts +18 -1
- package/dist/src/agent.d.ts.map +1 -1
- package/dist/src/agent.js +201 -2
- package/dist/src/agent.js.map +1 -1
- package/dist/src/framework.d.ts +15 -1
- package/dist/src/framework.d.ts.map +1 -1
- package/dist/src/framework.js +423 -86
- package/dist/src/framework.js.map +1 -1
- package/dist/src/gate/event-gate.d.ts.map +1 -1
- package/dist/src/gate/event-gate.js +27 -4
- package/dist/src/gate/event-gate.js.map +1 -1
- package/dist/src/mcpl/channel-registry.d.ts +34 -15
- package/dist/src/mcpl/channel-registry.d.ts.map +1 -1
- package/dist/src/mcpl/channel-registry.js +299 -67
- package/dist/src/mcpl/channel-registry.js.map +1 -1
- package/dist/src/mcpl/push-handler.d.ts +1 -0
- 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 +3 -1
- package/dist/src/mcpl/server-connection.d.ts.map +1 -1
- package/dist/src/mcpl/server-connection.js +4 -0
- package/dist/src/mcpl/server-connection.js.map +1 -1
- package/dist/src/mcpl/types.d.ts +43 -12
- package/dist/src/mcpl/types.d.ts.map +1 -1
- package/dist/src/mcpl/types.js +1 -0
- package/dist/src/mcpl/types.js.map +1 -1
- package/dist/src/types/agent.d.ts +15 -0
- package/dist/src/types/agent.d.ts.map +1 -1
- package/dist/src/types/events.d.ts +1 -0
- package/dist/src/types/events.d.ts.map +1 -1
- package/dist/src/types/trace.d.ts +2 -0
- package/dist/src/types/trace.d.ts.map +1 -1
- package/dist/test/agent-runtime-settings.test.d.ts +2 -0
- package/dist/test/agent-runtime-settings.test.d.ts.map +1 -0
- package/dist/test/agent-runtime-settings.test.js +120 -0
- package/dist/test/agent-runtime-settings.test.js.map +1 -0
- package/dist/test/channel-registry-routing.test.js +5 -4
- package/dist/test/channel-registry-routing.test.js.map +1 -1
- package/dist/test/conversation-fork-injection-scoping.test.js +12 -1
- package/dist/test/conversation-fork-injection-scoping.test.js.map +1 -1
- package/dist/test/event-gate.test.js +21 -1
- package/dist/test/event-gate.test.js.map +1 -1
- package/dist/test/helpers/mock-membrane.d.ts +13 -1
- package/dist/test/helpers/mock-membrane.d.ts.map +1 -1
- package/dist/test/helpers/mock-membrane.js +8 -1
- package/dist/test/helpers/mock-membrane.js.map +1 -1
- package/dist/test/maintenance-timer.test.js +3 -1
- package/dist/test/maintenance-timer.test.js.map +1 -1
- package/dist/test/mcpl-gate-roundtrip.test.js +138 -6
- package/dist/test/mcpl-gate-roundtrip.test.js.map +1 -1
- package/dist/test/present-while-acting.test.d.ts +18 -0
- package/dist/test/present-while-acting.test.d.ts.map +1 -0
- package/dist/test/present-while-acting.test.js +267 -0
- package/dist/test/present-while-acting.test.js.map +1 -0
- package/dist/test/trunk-channel-routing.test.js +38 -0
- package/dist/test/trunk-channel-routing.test.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +3 -3
- package/src/agent.ts +222 -3
- package/src/framework.ts +468 -92
- package/src/gate/event-gate.ts +38 -4
- package/src/mcpl/channel-registry.ts +372 -76
- package/src/mcpl/push-handler.ts +2 -0
- package/src/mcpl/server-connection.ts +10 -0
- package/src/mcpl/types.ts +49 -12
- package/src/types/agent.ts +18 -0
- package/src/types/events.ts +1 -1
- package/src/types/trace.ts +2 -0
package/src/framework.ts
CHANGED
|
@@ -44,6 +44,9 @@ import type {
|
|
|
44
44
|
ContextMaintenanceRun,
|
|
45
45
|
ContextMaintenanceAgentRun,
|
|
46
46
|
ContextMaintenanceSnapshot,
|
|
47
|
+
AgentRuntimeSettingsPatch,
|
|
48
|
+
AgentRuntimeSettingsOverrides,
|
|
49
|
+
AgentRuntimeSettingsSnapshot,
|
|
47
50
|
} from './types/index.js';
|
|
48
51
|
import { ProcessQueueImpl } from './queue.js';
|
|
49
52
|
import { Agent } from './agent.js';
|
|
@@ -59,6 +62,30 @@ import { ConversationRouter } from './mcpl/conversation-router.js';
|
|
|
59
62
|
import { safeSlice } from './safe-slice.js';
|
|
60
63
|
import { toolResultDataToHistoryString } from './tool-result-history.js';
|
|
61
64
|
import { splitProseSegments } from './prose-segments.js';
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Tools whose presence suppresses auto-routing of the surrounding prose,
|
|
68
|
+
* for two distinct reasons:
|
|
69
|
+
* - `skip_reply` — the agent EXPLICITLY chose not to reply (the deliberate
|
|
70
|
+
* "stay silent" signal).
|
|
71
|
+
* - explicit delivery tools (channel_publish / *send_message /
|
|
72
|
+
* *reply_message / *send_dm) — already sent the message, so routing the
|
|
73
|
+
* prose again would double-post.
|
|
74
|
+
* `think` is deliberately NOT here: it is silent *reasoning*, but prose
|
|
75
|
+
* written around a think is the agent's actual voice and must be delivered.
|
|
76
|
+
*
|
|
77
|
+
* Scope: mid-turn rounds apply this PER ROUND (a round's prose is suppressed
|
|
78
|
+
* only by a silencing tool in that same round — live delivery can't be
|
|
79
|
+
* retracted by a later round's skip_reply); the turn's TRAILING prose keeps
|
|
80
|
+
* the historical turn-wide rule (any silencing tool anywhere in the turn
|
|
81
|
+
* suppresses it), preserving the "send_message then 'sent it.' postscript"
|
|
82
|
+
* double-post guard.
|
|
83
|
+
*/
|
|
84
|
+
const SILENCING_TOOLS = new Set([
|
|
85
|
+
'skip_reply', 'channel_publish', 'send_message', 'reply_message', 'send_dm',
|
|
86
|
+
]);
|
|
87
|
+
/** Strip the `server--` MCPL prefix from a tool name. */
|
|
88
|
+
const bareToolName = (n: string): string => n.split('--').pop()!;
|
|
62
89
|
import { CheckpointManager } from './mcpl/checkpoint-manager.js';
|
|
63
90
|
import { isToolAllowed } from './mcpl/tool-policy.js';
|
|
64
91
|
import { EventGate } from './gate/event-gate.js';
|
|
@@ -292,6 +319,13 @@ export class AgentFramework {
|
|
|
292
319
|
private processLoggingPersist: boolean;
|
|
293
320
|
private processLoggingBroadcast: boolean;
|
|
294
321
|
private activeStreams: Map<string, Promise<void>> = new Map();
|
|
322
|
+
|
|
323
|
+
/** Per-agent output locus pinned for the CURRENT logical turn (see
|
|
324
|
+
* resolveTurnLocus in driveStream). Lives here — not in driveStream
|
|
325
|
+
* locals — so the pin survives a context-budget stream restart, which
|
|
326
|
+
* continues the same logical turn in a fresh driveStream. Cleared at the
|
|
327
|
+
* start of every non-restart turn. */
|
|
328
|
+
private turnLocusPins: Map<string, string> = new Map();
|
|
295
329
|
private pendingAssistantBlocks: Map<string, ContentBlock[]> = new Map();
|
|
296
330
|
/** Streams the FRAMEWORK cancelled for non-terminal reasons, keyed
|
|
297
331
|
* `${agentName}:${streamId}`: an endTurn tool result or a context-budget
|
|
@@ -989,10 +1023,49 @@ export class AgentFramework {
|
|
|
989
1023
|
AgentFramework.EVENT_TAGS_TOOL,
|
|
990
1024
|
]
|
|
991
1025
|
: [];
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
1026
|
+
return [
|
|
1027
|
+
...moduleTools,
|
|
1028
|
+
...this.mcplTools,
|
|
1029
|
+
...channelTools,
|
|
1030
|
+
...gateTools,
|
|
1031
|
+
AgentFramework.AGENT_SETTINGS_TOOL,
|
|
1032
|
+
];
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
getAgentRuntimeSettings(agentName: string): AgentRuntimeSettingsSnapshot {
|
|
1036
|
+
const agent = this.agents.get(agentName);
|
|
1037
|
+
if (!agent) throw new Error(`Unknown agent: ${agentName}`);
|
|
1038
|
+
return agent.getRuntimeSettings();
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
updateAgentRuntimeSettings(
|
|
1042
|
+
agentName: string,
|
|
1043
|
+
patch: AgentRuntimeSettingsPatch,
|
|
1044
|
+
): AgentRuntimeSettingsSnapshot {
|
|
1045
|
+
const agent = this.agents.get(agentName);
|
|
1046
|
+
if (!agent) throw new Error(`Unknown agent: ${agentName}`);
|
|
1047
|
+
const result = agent.updateRuntimeSettings(patch);
|
|
1048
|
+
this.persistAgentRuntimeSettings(agentName, agent.getRuntimeSettingsOverrides());
|
|
1049
|
+
return result;
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
resetAgentRuntimeSettings(
|
|
1053
|
+
agentName: string,
|
|
1054
|
+
keys?: Array<keyof AgentRuntimeSettingsPatch>,
|
|
1055
|
+
): AgentRuntimeSettingsSnapshot {
|
|
1056
|
+
const agent = this.agents.get(agentName);
|
|
1057
|
+
if (!agent) throw new Error(`Unknown agent: ${agentName}`);
|
|
1058
|
+
const result = agent.resetRuntimeSettings(keys);
|
|
1059
|
+
this.persistAgentRuntimeSettings(agentName, agent.getRuntimeSettingsOverrides());
|
|
1060
|
+
return result;
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
cancelAgentRuntimeSettingsTransition(agentName: string): AgentRuntimeSettingsSnapshot {
|
|
1064
|
+
const agent = this.agents.get(agentName);
|
|
1065
|
+
if (!agent) throw new Error(`Unknown agent: ${agentName}`);
|
|
1066
|
+
const result = agent.cancelRuntimeSettingsTransition();
|
|
1067
|
+
this.persistAgentRuntimeSettings(agentName, agent.getRuntimeSettingsOverrides());
|
|
1068
|
+
return result;
|
|
996
1069
|
}
|
|
997
1070
|
|
|
998
1071
|
/** Counts-only context-maintenance diagnostics for authenticated debug UIs. */
|
|
@@ -1194,6 +1267,33 @@ export class AgentFramework {
|
|
|
1194
1267
|
}
|
|
1195
1268
|
}
|
|
1196
1269
|
|
|
1270
|
+
private readAgentRuntimeSettings(agentName: string): AgentRuntimeSettingsOverrides | null {
|
|
1271
|
+
try {
|
|
1272
|
+
const data = this.store.getStateJson(FRAMEWORK_STATE_ID) as {
|
|
1273
|
+
agentRuntimeSettings?: Record<string, AgentRuntimeSettingsOverrides>;
|
|
1274
|
+
} | null;
|
|
1275
|
+
const stored = data?.agentRuntimeSettings?.[agentName];
|
|
1276
|
+
return stored ? { ...stored } : null;
|
|
1277
|
+
} catch {
|
|
1278
|
+
return null;
|
|
1279
|
+
}
|
|
1280
|
+
}
|
|
1281
|
+
|
|
1282
|
+
private persistAgentRuntimeSettings(
|
|
1283
|
+
agentName: string,
|
|
1284
|
+
overrides: AgentRuntimeSettingsOverrides,
|
|
1285
|
+
): void {
|
|
1286
|
+
const data = this.store.getStateJson(FRAMEWORK_STATE_ID);
|
|
1287
|
+
const state = (data && typeof data === 'object' ? data : {}) as Record<string, unknown>;
|
|
1288
|
+
const all = {
|
|
1289
|
+
...((state.agentRuntimeSettings as Record<string, AgentRuntimeSettingsOverrides> | undefined) ?? {}),
|
|
1290
|
+
};
|
|
1291
|
+
if (Object.keys(overrides).length === 0) delete all[agentName];
|
|
1292
|
+
else all[agentName] = { ...overrides };
|
|
1293
|
+
state.agentRuntimeSettings = all;
|
|
1294
|
+
this.store.setStateJson(FRAMEWORK_STATE_ID, state);
|
|
1295
|
+
}
|
|
1296
|
+
|
|
1197
1297
|
/**
|
|
1198
1298
|
* Create an ephemeral agent that is NOT registered in the main event loop.
|
|
1199
1299
|
*
|
|
@@ -1533,6 +1633,31 @@ export class AgentFramework {
|
|
|
1533
1633
|
inputSchema: { type: 'object' },
|
|
1534
1634
|
};
|
|
1535
1635
|
|
|
1636
|
+
private static readonly AGENT_SETTINGS_TOOL: import('./types/index.js').ToolDefinition = {
|
|
1637
|
+
name: 'agent_settings',
|
|
1638
|
+
description:
|
|
1639
|
+
'Read or change your hot runtime settings. This intentionally exposes only ' +
|
|
1640
|
+
'context budget, recent raw tail size, and transition pace; model, prompts, ' +
|
|
1641
|
+
'folding strategy, and other restart-bound configuration are not mutable here. ' +
|
|
1642
|
+
'Lower context budgets converge gradually under the transition pace before ' +
|
|
1643
|
+
'becoming the hard live limit; increases take effect immediately.',
|
|
1644
|
+
inputSchema: {
|
|
1645
|
+
type: 'object',
|
|
1646
|
+
properties: {
|
|
1647
|
+
action: { type: 'string', enum: ['get', 'update', 'reset', 'cancel'] },
|
|
1648
|
+
context_budget_tokens: { type: 'number', description: 'Total input context budget, including the reserved response allowance.' },
|
|
1649
|
+
tail_tokens: { type: 'number', description: 'Recent raw context retained verbatim.' },
|
|
1650
|
+
transition_pace_tokens: { type: 'number', description: 'Maximum ordinary KV re-read/perturbation per compile while converging.' },
|
|
1651
|
+
settings: {
|
|
1652
|
+
type: 'array',
|
|
1653
|
+
items: { type: 'string', enum: ['context_budget_tokens', 'tail_tokens', 'transition_pace_tokens'] },
|
|
1654
|
+
description: 'For reset: settings to restore to recipe values. Omit to reset all.',
|
|
1655
|
+
},
|
|
1656
|
+
},
|
|
1657
|
+
required: ['action'],
|
|
1658
|
+
},
|
|
1659
|
+
};
|
|
1660
|
+
|
|
1536
1661
|
/** Reserved chat:* core vocabulary (MCPL RFC-001 §4) — short descriptions so
|
|
1537
1662
|
* the agent can author rules without reading the spec. */
|
|
1538
1663
|
private static readonly CHAT_CORE_TAGS: Record<string, string> = {
|
|
@@ -2601,6 +2726,8 @@ export class AgentFramework {
|
|
|
2601
2726
|
});
|
|
2602
2727
|
|
|
2603
2728
|
const agent = new Agent(config, contextManager, this.membrane);
|
|
2729
|
+
const restoredSettings = this.readAgentRuntimeSettings(config.name);
|
|
2730
|
+
if (restoredSettings) agent.restoreRuntimeSettings(restoredSettings);
|
|
2604
2731
|
this.agents.set(config.name, agent);
|
|
2605
2732
|
this.agentConfigs.set(config.name, config);
|
|
2606
2733
|
|
|
@@ -2708,16 +2835,51 @@ export class AgentFramework {
|
|
|
2708
2835
|
}));
|
|
2709
2836
|
agent.getContextManager().addMessage('user', toolResultContent);
|
|
2710
2837
|
|
|
2711
|
-
// Flush any messages that were deferred while
|
|
2712
|
-
// Route to the PRIMARY agent — deferred messages are
|
|
2713
|
-
// (
|
|
2838
|
+
// Flush any messages that were deferred while this turn was in
|
|
2839
|
+
// flight. Route to the PRIMARY agent — deferred messages are
|
|
2840
|
+
// framework-level (module messages, push events, subagent return
|
|
2841
|
+
// notifications) meant for the main conversation.
|
|
2842
|
+
//
|
|
2843
|
+
// Hear-while-acting: messages flushed into THIS agent's own window
|
|
2844
|
+
// are also collected for mid-turn injection — passed to the membrane
|
|
2845
|
+
// with the tool results below, so the NEXT inference round of the
|
|
2846
|
+
// live turn sees them instead of the agent staying deaf until the
|
|
2847
|
+
// turn ends. The window stores them here, right after the
|
|
2848
|
+
// tool_result message, which is exactly where the membrane appends
|
|
2849
|
+
// them in the live conversation — same participant, same content
|
|
2850
|
+
// blocks, same metadata, and compile() emits recent messages
|
|
2851
|
+
// verbatim, so the next turn's compiled prefix byte-matches the
|
|
2852
|
+
// live request (prompt-cache safe).
|
|
2853
|
+
//
|
|
2854
|
+
// The queue is drained ONLY at the target agent's own boundary:
|
|
2855
|
+
// draining at another agent's boundary would store the messages
|
|
2856
|
+
// mid-turn in the target's window without injecting them into the
|
|
2857
|
+
// target's live stream (deaf agent + window/live divergence). Left
|
|
2858
|
+
// queued, they flush at the target's own next boundary — with
|
|
2859
|
+
// injection — or in driveStream's finally when its turn ends.
|
|
2860
|
+
const midTurnInjections: Array<{ participant: string; content: ContentBlock[]; metadata?: MessageMetadata }> = [];
|
|
2714
2861
|
if (this.deferredMessages.length > 0) {
|
|
2715
|
-
const
|
|
2716
|
-
|
|
2717
|
-
|
|
2718
|
-
|
|
2719
|
-
|
|
2720
|
-
|
|
2862
|
+
const target = (this.primaryAgentName ? this.agents.get(this.primaryAgentName) : undefined) ?? agent;
|
|
2863
|
+
if (target === agent) {
|
|
2864
|
+
const deferred = this.deferredMessages.splice(0);
|
|
2865
|
+
for (const msg of deferred) {
|
|
2866
|
+
target.getContextManager().addMessage(msg.participant, msg.content, msg.metadata);
|
|
2867
|
+
// Injection guards: tool blocks would corrupt the tool-cycle
|
|
2868
|
+
// structure the membrane enforces, and a message named as the
|
|
2869
|
+
// agent itself would render as an ASSISTANT turn on the wire
|
|
2870
|
+
// (an unintended prefill the model would continue). Such
|
|
2871
|
+
// messages stay window-only — visible next turn.
|
|
2872
|
+
const hasToolBlocks = msg.content.some(
|
|
2873
|
+
(b) => b.type === 'tool_use' || b.type === 'tool_result'
|
|
2874
|
+
);
|
|
2875
|
+
if (!hasToolBlocks && msg.participant !== agent.name) {
|
|
2876
|
+
midTurnInjections.push({
|
|
2877
|
+
participant: msg.participant,
|
|
2878
|
+
content: msg.content,
|
|
2879
|
+
...(msg.metadata ? { metadata: msg.metadata } : {}),
|
|
2880
|
+
});
|
|
2881
|
+
}
|
|
2882
|
+
}
|
|
2721
2883
|
}
|
|
2722
2884
|
}
|
|
2723
2885
|
|
|
@@ -2769,13 +2931,23 @@ export class AgentFramework {
|
|
|
2769
2931
|
timestamp: Date.now(),
|
|
2770
2932
|
});
|
|
2771
2933
|
} else if (currentState.stream) {
|
|
2772
|
-
// Streaming path: convert results and resume the stream
|
|
2934
|
+
// Streaming path: convert results and resume the stream.
|
|
2935
|
+
// Mid-turn messages collected above ride along as injected user
|
|
2936
|
+
// messages (membrane ≥0.5.72) — appended after the tool_result
|
|
2937
|
+
// envelope so the next round of THIS turn hears them.
|
|
2773
2938
|
const membraneResults = currentState.toolResults.map(tc =>
|
|
2774
2939
|
this.toMembraneToolResult(tc.id, tc.result, maxChars)
|
|
2775
2940
|
);
|
|
2776
|
-
currentState.stream.provideToolResults(
|
|
2941
|
+
currentState.stream.provideToolResults(
|
|
2942
|
+
membraneResults,
|
|
2943
|
+
midTurnInjections.length > 0 ? { injectedMessages: midTurnInjections } : undefined,
|
|
2944
|
+
);
|
|
2777
2945
|
agent.setStreaming(currentState.stream);
|
|
2778
|
-
this.emitTrace({
|
|
2946
|
+
this.emitTrace({
|
|
2947
|
+
type: 'inference:stream_resumed',
|
|
2948
|
+
agentName: agent.name,
|
|
2949
|
+
...(midTurnInjections.length > 0 ? { injectedMessages: midTurnInjections.length } : {}),
|
|
2950
|
+
});
|
|
2779
2951
|
} else {
|
|
2780
2952
|
// Non-streaming fallback: schedule re-inference
|
|
2781
2953
|
this.pendingRequests.push({
|
|
@@ -3229,6 +3401,15 @@ export class AgentFramework {
|
|
|
3229
3401
|
* Convert an MCPL push event to a context message.
|
|
3230
3402
|
*/
|
|
3231
3403
|
private handleMcplPushEvent(event: McplPushEvent): void {
|
|
3404
|
+
const triggerChannel = this.derivePushEventChannel(event.origin);
|
|
3405
|
+
if (triggerChannel && this.channelRegistry) {
|
|
3406
|
+
this.channelRegistry.ensureChannelRegistered(
|
|
3407
|
+
event.serverId,
|
|
3408
|
+
triggerChannel.channelId,
|
|
3409
|
+
triggerChannel.label,
|
|
3410
|
+
);
|
|
3411
|
+
}
|
|
3412
|
+
|
|
3232
3413
|
const metadata: Record<string, unknown> = {
|
|
3233
3414
|
...event.origin,
|
|
3234
3415
|
serverId: event.serverId,
|
|
@@ -3237,23 +3418,39 @@ export class AgentFramework {
|
|
|
3237
3418
|
triggered: event.triggerInference ?? false,
|
|
3238
3419
|
};
|
|
3239
3420
|
|
|
3240
|
-
const
|
|
3241
|
-
|
|
3242
|
-
|
|
3243
|
-
|
|
3244
|
-
|
|
3245
|
-
|
|
3246
|
-
|
|
3247
|
-
|
|
3248
|
-
|
|
3249
|
-
|
|
3250
|
-
|
|
3251
|
-
|
|
3252
|
-
|
|
3253
|
-
|
|
3254
|
-
|
|
3421
|
+
const content = [...event.content];
|
|
3422
|
+
const addressedWhileClosed = triggerChannel &&
|
|
3423
|
+
event.tags?.includes('chat:addressed') &&
|
|
3424
|
+
this.channelRegistry &&
|
|
3425
|
+
!this.channelRegistry.isChannelOpen(triggerChannel.channelId);
|
|
3426
|
+
if (addressedWhileClosed) {
|
|
3427
|
+
const descriptor = this.channelRegistry!.getDescriptor(triggerChannel.channelId);
|
|
3428
|
+
const messageId = typeof event.origin?.messageId === 'string'
|
|
3429
|
+
? event.origin.messageId
|
|
3430
|
+
: event.eventId;
|
|
3431
|
+
const maxBackscroll = descriptor?.capabilities?.history?.maxMessages ?? 0;
|
|
3432
|
+
content.push({
|
|
3433
|
+
type: 'text',
|
|
3434
|
+
text:
|
|
3435
|
+
`\n[Channel invitation] You were addressed in closed channel ` +
|
|
3436
|
+
`"${triggerChannel.label ?? triggerChannel.channelId}" (${triggerChannel.channelId}, server ${event.serverId}). ` +
|
|
3437
|
+
`You received this one message without subscribing. ` +
|
|
3438
|
+
(maxBackscroll > 0
|
|
3439
|
+
? `To subscribe, call channel_open with channelId, serverId "${event.serverId}", backscroll (0-${maxBackscroll}), and beforeMessageId "${messageId}". `
|
|
3440
|
+
: `To subscribe, call channel_open with this channelId and serverId "${event.serverId}". `) +
|
|
3441
|
+
`You may reply once without subscribing. To remain closed, call channel_decline ` +
|
|
3442
|
+
`with channelId, serverId "${event.serverId}", and messageId "${messageId}"; ` +
|
|
3443
|
+
`optionally set acknowledge to a surface value such as 👀.`,
|
|
3444
|
+
});
|
|
3445
|
+
metadata.channelInvitation = true;
|
|
3446
|
+
metadata.channelOpen = false;
|
|
3447
|
+
metadata.channelId = triggerChannel.channelId;
|
|
3448
|
+
metadata.invitationMessageId = messageId;
|
|
3255
3449
|
}
|
|
3256
3450
|
|
|
3451
|
+
const id = this.addMessage('user', content, metadata);
|
|
3452
|
+
this.emitTrace({ type: 'message:added', messageId: id, source: 'mcpl:push-event' });
|
|
3453
|
+
|
|
3257
3454
|
if (event.triggerInference) {
|
|
3258
3455
|
// Default broadcast excludes conversation forks (channel-driven).
|
|
3259
3456
|
const targetAgents = event.targetAgents
|
|
@@ -3522,11 +3719,67 @@ export class AgentFramework {
|
|
|
3522
3719
|
const myStreamId = agent.streamId;
|
|
3523
3720
|
let hadToolCalls = false;
|
|
3524
3721
|
|
|
3722
|
+
// ---- Present-while-acting turn state ---------------------------------
|
|
3723
|
+
// Output locus for THIS turn: resolved lazily at the first routed prose
|
|
3724
|
+
// segment and pinned for the rest of the turn (mid-turn rounds AND the
|
|
3725
|
+
// trailing prose at 'complete'), so one turn's voice stays in one channel
|
|
3726
|
+
// even when a queued inbound from another channel overwrites the
|
|
3727
|
+
// per-agent triggering channel between segments (PR #32). The pin lives
|
|
3728
|
+
// in `turnLocusPins` so it survives a context-budget stream restart. A
|
|
3729
|
+
// null resolution is NOT pinned — a turn whose locus is unresolvable
|
|
3730
|
+
// early (e.g. heartbeat-triggered) may still resolve a channel by the
|
|
3731
|
+
// time later prose routes.
|
|
3732
|
+
if (trigger?.reason !== 'context_budget_restart') {
|
|
3733
|
+
this.turnLocusPins.delete(agent.name);
|
|
3734
|
+
}
|
|
3735
|
+
const resolveTurnLocus = (): string | null => {
|
|
3736
|
+
let locus = this.turnLocusPins.get(agent.name) ?? null;
|
|
3737
|
+
if (locus === null && this.channelRegistry) {
|
|
3738
|
+
locus = this.channelRegistry.resolveLocus(agent.name);
|
|
3739
|
+
if (locus !== null) this.turnLocusPins.set(agent.name, locus);
|
|
3740
|
+
}
|
|
3741
|
+
return locus;
|
|
3742
|
+
};
|
|
3743
|
+
|
|
3744
|
+
// Ordered delivery chain for live-routed prose. Links are enqueued
|
|
3745
|
+
// WITHOUT awaiting in the stream-event loop — an awaited network post
|
|
3746
|
+
// here would stall consumption of the next round's events by
|
|
3747
|
+
// segments × RTT on every prose-bearing round. The 'complete' case
|
|
3748
|
+
// awaits the chain before routing trailing prose, so in-channel ordering
|
|
3749
|
+
// is preserved end-to-end. Each link catches its own error: one failed
|
|
3750
|
+
// post must not silence the rest of the turn.
|
|
3751
|
+
let turnSpeechChain: Promise<void> = Promise.resolve();
|
|
3752
|
+
const enqueueSpeech = (text: string, locus: string | null): void => {
|
|
3753
|
+
turnSpeechChain = turnSpeechChain
|
|
3754
|
+
.then(async () => {
|
|
3755
|
+
await this.channelRegistry!.routeSpeech(agent.name, text, locus);
|
|
3756
|
+
})
|
|
3757
|
+
.catch((err) => console.error('mid-turn speech routing failed:', err));
|
|
3758
|
+
};
|
|
3759
|
+
|
|
3760
|
+
// Sticky silencing: once any round uses a SILENCING_TOOLS member,
|
|
3761
|
+
// prose from that round ONWARD is suppressed — skip_reply means "stay
|
|
3762
|
+
// silent", an explicit send means "I deliver my own words". Earlier
|
|
3763
|
+
// rounds' prose was already delivered live and cannot be retracted.
|
|
3764
|
+
let turnSilenced = false;
|
|
3765
|
+
|
|
3766
|
+
// Live routing is only trusted when the membrane provides verbatim
|
|
3767
|
+
// round-scoped blocks (roundContent, native tool mode, membrane ≥0.5.64).
|
|
3768
|
+
// The fallback `preamble` is CUMULATIVE in XML mode (assistant prefill +
|
|
3769
|
+
// all earlier rounds' prose and raw tool/thinking XML) — live-routing it
|
|
3770
|
+
// would repost the transcript and leak thinking every round. When no
|
|
3771
|
+
// round was live-routed, the 'complete' case falls back to the
|
|
3772
|
+
// historical whole-turn routing so fallback-mode prose is still
|
|
3773
|
+
// delivered exactly once, at turn end.
|
|
3774
|
+
let liveProseRouting = false;
|
|
3775
|
+
|
|
3525
3776
|
// Typing indicator: show "<agent> is typing…" in the channel she's
|
|
3526
3777
|
// responding to, for the whole duration of this turn. Started here (paired
|
|
3527
3778
|
// with the finally below, so it can never leak) and refreshed on a 7s
|
|
3528
3779
|
// interval by the ChannelRegistry until stopped on any exit path.
|
|
3529
|
-
const typingChannel = this.channelRegistry
|
|
3780
|
+
const typingChannel = this.channelRegistry
|
|
3781
|
+
? trigger?.channelId ?? this.channelRegistry.getDefaultPublishChannel()
|
|
3782
|
+
: null;
|
|
3530
3783
|
if (typingChannel) this.channelRegistry!.startTyping(typingChannel);
|
|
3531
3784
|
|
|
3532
3785
|
try {
|
|
@@ -3599,6 +3852,43 @@ export class AgentFramework {
|
|
|
3599
3852
|
for (const call of event.calls) {
|
|
3600
3853
|
this.dispatchToolCall(agent.name, call);
|
|
3601
3854
|
}
|
|
3855
|
+
|
|
3856
|
+
// Speak-while-acting: route THIS round's prose to the locus NOW
|
|
3857
|
+
// instead of batching every segment to the end of the turn. Long
|
|
3858
|
+
// tool-using turns (robot control, live sessions) otherwise dump
|
|
3859
|
+
// the whole narration in one blob when the turn finally settles —
|
|
3860
|
+
// and, worse, make "talking" and "acting" feel mutually exclusive
|
|
3861
|
+
// to the agent. Mid-turn prose is speech (delivered live, in
|
|
3862
|
+
// order, via the turn's speech chain — never awaited here); the
|
|
3863
|
+
// `think` tool remains the private channel. Silencing is STICKY
|
|
3864
|
+
// from the round it occurs (see SILENCING_TOOLS). Only rounds
|
|
3865
|
+
// with verbatim roundContent are live-routed (see liveProseRouting
|
|
3866
|
+
// note above — the fallback preamble is cumulative in XML mode).
|
|
3867
|
+
if (this.channelRegistry) {
|
|
3868
|
+
const roundToolNames = event.calls.map((c) => c.name);
|
|
3869
|
+
if (roundToolNames.some((n) => SILENCING_TOOLS.has(bareToolName(n)))) {
|
|
3870
|
+
turnSilenced = true;
|
|
3871
|
+
}
|
|
3872
|
+
if (roundContent && roundContent.length > 0) {
|
|
3873
|
+
liveProseRouting = true;
|
|
3874
|
+
const roundSegments = splitProseSegments(assistantBlocks);
|
|
3875
|
+
if (roundSegments.length > 0) {
|
|
3876
|
+
if (turnSilenced) {
|
|
3877
|
+
console.error(
|
|
3878
|
+
`[routing] ${agent.name}: mid-turn round [${roundToolNames.join(', ')}] -> prose NOT routed (turn silenced)`,
|
|
3879
|
+
);
|
|
3880
|
+
} else {
|
|
3881
|
+
const locus = resolveTurnLocus();
|
|
3882
|
+
console.error(
|
|
3883
|
+
`[routing] ${agent.name}: mid-turn round [${roundToolNames.join(', ')}] -> routing ${roundSegments.length} prose segment(s) live -> ${locus ?? '(default)'}`,
|
|
3884
|
+
);
|
|
3885
|
+
for (const seg of roundSegments) {
|
|
3886
|
+
enqueueSpeech(seg, locus);
|
|
3887
|
+
}
|
|
3888
|
+
}
|
|
3889
|
+
}
|
|
3890
|
+
}
|
|
3891
|
+
}
|
|
3602
3892
|
// Stream's async iterator blocks on next() until provideToolResults() is called
|
|
3603
3893
|
break;
|
|
3604
3894
|
}
|
|
@@ -3703,10 +3993,13 @@ export class AgentFramework {
|
|
|
3703
3993
|
}
|
|
3704
3994
|
}
|
|
3705
3995
|
|
|
3706
|
-
// Separate speech from thoughts
|
|
3707
|
-
//
|
|
3708
|
-
//
|
|
3709
|
-
//
|
|
3996
|
+
// Separate speech from thoughts — for MODULE dispatch only
|
|
3997
|
+
// (dispatchSpeech / TUI rendering below). This split does NOT
|
|
3998
|
+
// decide what reaches the channel: mid-turn prose is routed live
|
|
3999
|
+
// per round (see the 'tool-calls' case) and trailing prose is
|
|
4000
|
+
// routed after this block. On a tool-using turn the module-level
|
|
4001
|
+
// convention remains "text is thoughts"; on a text-only turn all
|
|
4002
|
+
// text is speech.
|
|
3710
4003
|
const isTextBlock = (block: ContentBlock): block is ContentBlock & { type: 'text' } =>
|
|
3711
4004
|
block.type === 'text';
|
|
3712
4005
|
const allText = response.content.filter(isTextBlock);
|
|
@@ -3906,70 +4199,54 @@ export class AgentFramework {
|
|
|
3906
4199
|
}
|
|
3907
4200
|
}
|
|
3908
4201
|
} else if (this.channelRegistry && hadToolCalls && allText.length > 0) {
|
|
3909
|
-
// Tool-call turn that also produced prose.
|
|
3910
|
-
//
|
|
3911
|
-
//
|
|
3912
|
-
//
|
|
3913
|
-
//
|
|
3914
|
-
//
|
|
3915
|
-
// turn
|
|
3916
|
-
//
|
|
3917
|
-
// reaches the channel.
|
|
3918
|
-
//
|
|
3919
|
-
//
|
|
3920
|
-
//
|
|
3921
|
-
//
|
|
3922
|
-
//
|
|
3923
|
-
// *reply_message / *send_dm) — already sent the message, so
|
|
3924
|
-
// routing it again would double-post.
|
|
3925
|
-
// `think` is deliberately NOT here: it is silent *reasoning*, but
|
|
3926
|
-
// prose written after a think is the agent's actual reply and must
|
|
3927
|
-
// be delivered. (A think-only turn has no trailing prose and stays
|
|
3928
|
-
// silent via the `!t` check below.)
|
|
3929
|
-
const SILENCING = new Set([
|
|
3930
|
-
'skip_reply', 'channel_publish', 'send_message', 'reply_message', 'send_dm',
|
|
3931
|
-
]);
|
|
3932
|
-
const bare = (n: string) => (n.includes('--') ? n.split('--').pop()! : n);
|
|
4202
|
+
// Tool-call turn that also produced prose. When live routing was
|
|
4203
|
+
// active (native roundContent), each mid-turn round's prose was
|
|
4204
|
+
// already delivered when that round yielded — only the TRAILING
|
|
4205
|
+
// prose (blocks after the last tool round) remains here. On the
|
|
4206
|
+
// fallback path (XML tool mode / older membrane: no roundContent,
|
|
4207
|
+
// so nothing was live-routed) the historical behavior applies:
|
|
4208
|
+
// route the whole turn's parsed segments, once, now. The global
|
|
4209
|
+
// speech/thoughts split is left untouched (module/TUI rendering
|
|
4210
|
+
// unaffected) — this only governs what reaches the channel.
|
|
4211
|
+
//
|
|
4212
|
+
// Silencing here is the sticky flag ∪ turn-wide scan: the scan
|
|
4213
|
+
// (over the whole turn's tool_use blocks) covers the fallback
|
|
4214
|
+
// path and the "send_message then a 'sent it.' postscript"
|
|
4215
|
+
// double-post guard; the sticky flag is its live-rounds twin.
|
|
3933
4216
|
const toolNames = response.content
|
|
3934
4217
|
.filter((b) => b.type === 'tool_use')
|
|
3935
4218
|
.map((b) => (b as unknown as { name?: string }).name)
|
|
3936
4219
|
.filter((n): n is string => typeof n === 'string');
|
|
3937
|
-
const silenced = toolNames.some((n) =>
|
|
3938
|
-
|
|
3939
|
-
|
|
3940
|
-
|
|
3941
|
-
//
|
|
3942
|
-
//
|
|
3943
|
-
//
|
|
3944
|
-
|
|
3945
|
-
// ("msgA → [tool] → msgB → [tool] → msgC") is then delivered as
|
|
3946
|
-
// separate messages IN ORDER instead of being collapsed into one
|
|
3947
|
-
// trailing post (item 4). The silencing rule stays turn-wide: an
|
|
3948
|
-
// explicit send / skip suppresses ALL auto-routed prose this turn,
|
|
3949
|
-
// preserving the double-post guard.
|
|
3950
|
-
const segments = splitProseSegments(response.content);
|
|
4220
|
+
const silenced = turnSilenced || toolNames.some((n) => SILENCING_TOOLS.has(bareToolName(n)));
|
|
4221
|
+
|
|
4222
|
+
const segments = splitProseSegments(liveProseRouting ? terminalContent : response.content);
|
|
4223
|
+
|
|
4224
|
+
// Preserve in-channel ordering: everything enqueued live must
|
|
4225
|
+
// land before the trailing prose. Awaited even when silenced —
|
|
4226
|
+
// the chain may still be flushing earlier rounds' posts.
|
|
4227
|
+
await turnSpeechChain;
|
|
3951
4228
|
|
|
3952
4229
|
if (silenced || segments.length === 0) {
|
|
3953
4230
|
console.error(
|
|
3954
|
-
`[routing] ${agent.name}: tool-call turn [${toolNames.join(', ') || 'none'}] -> NOT routed ` +
|
|
3955
|
-
`(${silenced ? 'silencing tool / explicit send' : 'no prose'})`,
|
|
4231
|
+
`[routing] ${agent.name}: tool-call turn [${toolNames.join(', ') || 'none'}] -> trailing prose NOT routed ` +
|
|
4232
|
+
`(${silenced ? 'silencing tool / explicit send' : 'no trailing prose'})`,
|
|
3956
4233
|
);
|
|
3957
4234
|
} else {
|
|
3958
|
-
//
|
|
3959
|
-
//
|
|
3960
|
-
//
|
|
3961
|
-
//
|
|
3962
|
-
//
|
|
3963
|
-
//
|
|
3964
|
-
//
|
|
3965
|
-
const
|
|
4235
|
+
// Reuse the locus pinned at the turn's first live-routed
|
|
4236
|
+
// segment (or resolve it now for a turn whose only prose is
|
|
4237
|
+
// trailing). This dispatch runs AFTER the agent is idle (see
|
|
4238
|
+
// PR #32 note below), so a queued inbound from another channel
|
|
4239
|
+
// could otherwise overwrite the per-agent triggering channel
|
|
4240
|
+
// between segments — and a turn that narrated live into one
|
|
4241
|
+
// channel must not land its postscript in another.
|
|
4242
|
+
const locus = resolveTurnLocus();
|
|
3966
4243
|
console.error(
|
|
3967
|
-
`[routing] ${agent.name}: tool-call turn [${toolNames.join(', ')}] -> routing ${segments.length} prose segment(s)
|
|
4244
|
+
`[routing] ${agent.name}: tool-call turn [${toolNames.join(', ')}] -> routing ${segments.length} ${liveProseRouting ? 'trailing ' : ''}prose segment(s) -> ${locus ?? '(default)'}`,
|
|
3968
4245
|
);
|
|
3969
4246
|
// Deliver sequentially (await each) so the segments land in order.
|
|
3970
4247
|
for (const seg of segments) {
|
|
3971
4248
|
try {
|
|
3972
|
-
await this.channelRegistry.routeSpeech(agent.name, seg,
|
|
4249
|
+
await this.channelRegistry.routeSpeech(agent.name, seg, locus);
|
|
3973
4250
|
} catch (err) {
|
|
3974
4251
|
console.error('speech routing failed:', err);
|
|
3975
4252
|
}
|
|
@@ -4393,6 +4670,12 @@ export class AgentFramework {
|
|
|
4393
4670
|
return;
|
|
4394
4671
|
}
|
|
4395
4672
|
|
|
4673
|
+
// Route the agent's typed, allowlisted hot-settings surface.
|
|
4674
|
+
if (enrichedCall.name === 'agent_settings') {
|
|
4675
|
+
this.dispatchAgentSettingsToolCall(agentName, enrichedCall);
|
|
4676
|
+
return;
|
|
4677
|
+
}
|
|
4678
|
+
|
|
4396
4679
|
// Route gate_status tool
|
|
4397
4680
|
if (enrichedCall.name === 'gate_status' && this.eventGate) {
|
|
4398
4681
|
this.dispatchGateToolCall(agentName, enrichedCall);
|
|
@@ -4511,12 +4794,19 @@ export class AgentFramework {
|
|
|
4511
4794
|
throw new Error('No agents configured');
|
|
4512
4795
|
}
|
|
4513
4796
|
|
|
4514
|
-
//
|
|
4515
|
-
//
|
|
4797
|
+
// Defer non-tool_result messages while a tool cycle is mid-flight
|
|
4798
|
+
// (pendingAssistantBlocks: preserves tool_use → tool_result adjacency)
|
|
4799
|
+
// OR while the target agent has a live stream at all. A message stored
|
|
4800
|
+
// mid-stream lands BEFORE the turn's assistant blocks in the window even
|
|
4801
|
+
// though the live conversation never saw it — so the next compile
|
|
4802
|
+
// diverges from the live prefix (prompt-cache bust) and the message
|
|
4803
|
+
// misses mid-turn injection. Deferred messages flush at the agent's next
|
|
4804
|
+
// tool boundary (where they are ALSO injected into the live stream —
|
|
4805
|
+
// hear-while-acting) or in driveStream's finally when the turn ends.
|
|
4516
4806
|
const hasToolResult = content.some(b => b.type === 'tool_result');
|
|
4517
|
-
if (!hasToolResult && this.pendingAssistantBlocks.size > 0) {
|
|
4807
|
+
if (!hasToolResult && (this.pendingAssistantBlocks.size > 0 || this.activeStreams.has(agent.name))) {
|
|
4518
4808
|
this.deferredMessages.push({ participant, content, metadata });
|
|
4519
|
-
return '' as MessageId; // Deferred —
|
|
4809
|
+
return '' as MessageId; // Deferred — flushed at the next boundary
|
|
4520
4810
|
}
|
|
4521
4811
|
|
|
4522
4812
|
return agent.getContextManager().addMessage(participant, content, metadata);
|
|
@@ -5004,6 +5294,7 @@ export class AgentFramework {
|
|
|
5004
5294
|
(event) => this.pushEvent(event),
|
|
5005
5295
|
(event) => this.emitTrace(event as { type: TraceEvent['type']; [key: string]: unknown }),
|
|
5006
5296
|
{
|
|
5297
|
+
store: this.store,
|
|
5007
5298
|
sendTypingFn: (serverId, channelId, metadata, op) => {
|
|
5008
5299
|
const server = this.mcplServerRegistry!.getServer(serverId);
|
|
5009
5300
|
if (server) {
|
|
@@ -5108,8 +5399,11 @@ export class AgentFramework {
|
|
|
5108
5399
|
|
|
5109
5400
|
// Record per-server channel subscription policy before the server
|
|
5110
5401
|
// registers channels — handleRegister fires during the handshake.
|
|
5111
|
-
if (
|
|
5112
|
-
this.channelRegistry.setSubscriptionPolicy(
|
|
5402
|
+
if (this.channelRegistry) {
|
|
5403
|
+
this.channelRegistry.setSubscriptionPolicy(
|
|
5404
|
+
config.id,
|
|
5405
|
+
config.channelSubscription ?? 'manual',
|
|
5406
|
+
);
|
|
5113
5407
|
}
|
|
5114
5408
|
|
|
5115
5409
|
const connection = await this.mcplServerRegistry.addServer(config, this.mcplHostCapabilities);
|
|
@@ -5934,6 +6228,88 @@ export class AgentFramework {
|
|
|
5934
6228
|
});
|
|
5935
6229
|
}
|
|
5936
6230
|
|
|
6231
|
+
private dispatchAgentSettingsToolCall(agentName: string, call: ToolCall): void {
|
|
6232
|
+
this.emitTrace({
|
|
6233
|
+
type: 'tool:started',
|
|
6234
|
+
module: 'agent',
|
|
6235
|
+
tool: call.name,
|
|
6236
|
+
callId: call.id,
|
|
6237
|
+
input: call.input,
|
|
6238
|
+
});
|
|
6239
|
+
let result: ToolResult;
|
|
6240
|
+
try {
|
|
6241
|
+
const input = (call.input ?? {}) as {
|
|
6242
|
+
action?: unknown;
|
|
6243
|
+
context_budget_tokens?: unknown;
|
|
6244
|
+
tail_tokens?: unknown;
|
|
6245
|
+
transition_pace_tokens?: unknown;
|
|
6246
|
+
settings?: unknown;
|
|
6247
|
+
};
|
|
6248
|
+
switch (input.action) {
|
|
6249
|
+
case 'get':
|
|
6250
|
+
result = { success: true, data: this.getAgentRuntimeSettings(agentName) };
|
|
6251
|
+
break;
|
|
6252
|
+
case 'cancel':
|
|
6253
|
+
result = { success: true, data: this.cancelAgentRuntimeSettingsTransition(agentName) };
|
|
6254
|
+
break;
|
|
6255
|
+
case 'update': {
|
|
6256
|
+
const patch: AgentRuntimeSettingsPatch = {};
|
|
6257
|
+
if (input.context_budget_tokens !== undefined) {
|
|
6258
|
+
patch.contextBudgetTokens = Number(input.context_budget_tokens);
|
|
6259
|
+
}
|
|
6260
|
+
if (input.tail_tokens !== undefined) patch.tailTokens = Number(input.tail_tokens);
|
|
6261
|
+
if (input.transition_pace_tokens !== undefined) {
|
|
6262
|
+
patch.transitionPaceTokens = Number(input.transition_pace_tokens);
|
|
6263
|
+
}
|
|
6264
|
+
result = { success: true, data: this.updateAgentRuntimeSettings(agentName, patch) };
|
|
6265
|
+
break;
|
|
6266
|
+
}
|
|
6267
|
+
case 'reset': {
|
|
6268
|
+
let keys: Array<keyof AgentRuntimeSettingsPatch> | undefined;
|
|
6269
|
+
if (input.settings !== undefined) {
|
|
6270
|
+
if (!Array.isArray(input.settings)) throw new Error('reset `settings` must be an array');
|
|
6271
|
+
const names: Record<string, keyof AgentRuntimeSettingsPatch> = {
|
|
6272
|
+
context_budget_tokens: 'contextBudgetTokens',
|
|
6273
|
+
tail_tokens: 'tailTokens',
|
|
6274
|
+
transition_pace_tokens: 'transitionPaceTokens',
|
|
6275
|
+
};
|
|
6276
|
+
keys = input.settings.map((name) => {
|
|
6277
|
+
if (typeof name !== 'string' || !names[name]) {
|
|
6278
|
+
throw new Error(`Unknown reset setting: ${String(name)}`);
|
|
6279
|
+
}
|
|
6280
|
+
return names[name];
|
|
6281
|
+
});
|
|
6282
|
+
}
|
|
6283
|
+
result = { success: true, data: this.resetAgentRuntimeSettings(agentName, keys) };
|
|
6284
|
+
break;
|
|
6285
|
+
}
|
|
6286
|
+
default:
|
|
6287
|
+
throw new Error('agent_settings: action must be get, update, reset, or cancel');
|
|
6288
|
+
}
|
|
6289
|
+
} catch (error) {
|
|
6290
|
+
result = {
|
|
6291
|
+
success: false,
|
|
6292
|
+
error: error instanceof Error ? error.message : String(error),
|
|
6293
|
+
isError: true,
|
|
6294
|
+
};
|
|
6295
|
+
}
|
|
6296
|
+
this.emitTrace({
|
|
6297
|
+
type: result.isError ? 'tool:failed' : 'tool:completed',
|
|
6298
|
+
module: 'agent',
|
|
6299
|
+
tool: call.name,
|
|
6300
|
+
callId: call.id,
|
|
6301
|
+
durationMs: 0,
|
|
6302
|
+
...(result.isError ? { error: result.error } : {}),
|
|
6303
|
+
});
|
|
6304
|
+
this.pushEvent({
|
|
6305
|
+
type: 'tool-result',
|
|
6306
|
+
callId: call.id,
|
|
6307
|
+
agentName,
|
|
6308
|
+
moduleName: 'agent',
|
|
6309
|
+
result,
|
|
6310
|
+
});
|
|
6311
|
+
}
|
|
6312
|
+
|
|
5937
6313
|
/** Aggregate the event-tag vocabulary: reserved chat:* core + each connected
|
|
5938
6314
|
* server's declared tag ontology + gate.js status. */
|
|
5939
6315
|
private buildEventTagsResult(): Record<string, unknown> {
|