@animalabs/connectome-host 0.3.5 → 0.3.8
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/.env.example +6 -0
- package/.github/workflows/ci.yml +52 -0
- package/.github/workflows/publish.yml +2 -2
- package/README.md +5 -0
- package/bun.lock +14 -6
- package/docs/AGENT-ONBOARDING.md +6 -1
- package/package.json +6 -4
- package/scripts/import-codex-rollout.ts +288 -0
- package/src/call-ledger.ts +371 -0
- package/src/call-pricing.ts +119 -0
- package/src/framework-agent-config.ts +51 -0
- package/src/framework-strategy.ts +70 -0
- package/src/index.ts +130 -105
- package/src/logging-adapter.ts +105 -12
- package/src/mcpl-config.ts +1 -0
- package/src/modules/channel-mode-module.ts +14 -16
- package/src/modules/fleet-module.ts +17 -7
- package/src/modules/mcpl-admin-module.ts +6 -15
- package/src/modules/settings-module.ts +83 -59
- package/src/modules/subscription-gc-module.ts +17 -20
- package/src/modules/time-module.ts +15 -9
- package/src/modules/web-ui-module.ts +88 -10
- package/src/modules/web-ui-observers.ts +35 -9
- package/src/recipe.ts +133 -6
- package/src/state/agent-tree-reducer.ts +17 -3
- package/src/strategies/frontdesk-strategy.ts +15 -9
- package/src/web/protocol.ts +89 -0
- package/test/agent-tree-reducer.test.ts +35 -3
- package/test/autobio-progress-snapshot.test.ts +27 -12
- package/test/call-ledger.test.ts +91 -0
- package/test/call-pricing.test.ts +69 -0
- package/test/framework-fkm-composition.test.ts +160 -0
- package/test/frontdesk-strategy.test.ts +10 -0
- package/test/import-codex-rollout.test.ts +36 -0
- package/test/logging-adapter-refusal.test.ts +57 -0
- package/test/logging-adapter.test.ts +58 -1
- package/test/recipe-cache-ttl.test.ts +7 -3
- package/test/recipe-compression-fallback.test.ts +36 -0
- package/test/recipe-primary-summary-fallback.test.ts +40 -0
- package/test/recipe-provider.test.ts +36 -0
- package/test/recipe-think-policy.test.ts +39 -0
- package/test/recipe-timezone.test.ts +20 -0
- package/test/subscription-gc-module.test.ts +7 -5
- package/test/time-module.test.ts +13 -0
- package/test/web-ui-observers.test.ts +70 -2
- package/web/package-lock.json +425 -302
- package/web/src/App.tsx +9 -0
- package/web/src/ObserverGate.tsx +21 -11
- package/web/src/Usage.tsx +116 -2
- package/web/src/wire.ts +5 -2
- package/web/bun.lock +0 -357
|
@@ -7,22 +7,22 @@
|
|
|
7
7
|
* have to remember (and keep in sync) all three:
|
|
8
8
|
*
|
|
9
9
|
* debounced (wake on every message, batched):
|
|
10
|
-
* 1.
|
|
10
|
+
* 1. open the channel (`channel_open`)
|
|
11
11
|
* 2. upsert a per-channel gate policy that DEBOUNCES the ambient tag
|
|
12
12
|
* (framework.addGatePolicy → gate.json, hot-reloaded) so a burst wakes the
|
|
13
13
|
* agent once after it settles
|
|
14
14
|
* 3. pin subscription-gc to "off" for the channel, else a chatty channel
|
|
15
|
-
* auto-
|
|
15
|
+
* auto-closes itself at the idle-char limit and silently leaves
|
|
16
16
|
* debounced mode
|
|
17
17
|
*
|
|
18
18
|
* mentions (revert to mentions/DMs only):
|
|
19
19
|
* 1. remove the per-channel debounce gate policy
|
|
20
|
-
* 2.
|
|
20
|
+
* 2. close the channel
|
|
21
21
|
* 3. restore subscription-gc to its default limit
|
|
22
22
|
*
|
|
23
23
|
* The SAME `channelId` string is used for all three subsystems — it's the id the
|
|
24
24
|
* gate sees on an incoming event (`GateEventInfo.channelId`) and the id
|
|
25
|
-
* subscription-gc counts/
|
|
25
|
+
* subscription-gc counts/closes, so they line up by construction (this is
|
|
26
26
|
* exactly how SubscriptionGcModule already operates).
|
|
27
27
|
*
|
|
28
28
|
* Steps are best-effort with per-step reporting rather than a hard transaction:
|
|
@@ -45,7 +45,7 @@ import type { AgentFramework } from '@animalabs/agent-framework';
|
|
|
45
45
|
export interface ChannelModeConfig {
|
|
46
46
|
/** MCPL server id that owns subscriptions (default 'discord'). */
|
|
47
47
|
serverId?: string;
|
|
48
|
-
/**
|
|
48
|
+
/** @deprecated Lifecycle tools are host-generic; retained for config compatibility. */
|
|
49
49
|
toolPrefix?: string;
|
|
50
50
|
/** subscription-gc module name, for pinning the idle limit (default 'subscription-gc'). */
|
|
51
51
|
gcModuleName?: string;
|
|
@@ -69,13 +69,11 @@ export class ChannelModeModule implements Module {
|
|
|
69
69
|
private callSeq = 0;
|
|
70
70
|
|
|
71
71
|
private readonly serverId: string;
|
|
72
|
-
private readonly toolPrefix: string;
|
|
73
72
|
private readonly gcModuleName: string;
|
|
74
73
|
private readonly defaultDebounceMs: number;
|
|
75
74
|
|
|
76
75
|
constructor(config: ChannelModeConfig = {}) {
|
|
77
76
|
this.serverId = config.serverId ?? 'discord';
|
|
78
|
-
this.toolPrefix = config.toolPrefix ?? `mcpl--${this.serverId}`;
|
|
79
77
|
this.gcModuleName = config.gcModuleName ?? 'subscription-gc';
|
|
80
78
|
this.defaultDebounceMs =
|
|
81
79
|
typeof config.defaultDebounceMs === 'number' && config.defaultDebounceMs > 0
|
|
@@ -103,12 +101,12 @@ export class ChannelModeModule implements Module {
|
|
|
103
101
|
name: 'set_channel_mode',
|
|
104
102
|
description:
|
|
105
103
|
'Set how a channel gets your attention, in one step. ' +
|
|
106
|
-
'`mode: "debounced"` = wake on EVERY message but batched: it
|
|
104
|
+
'`mode: "debounced"` = wake on EVERY message but batched: it opens ' +
|
|
107
105
|
'to the channel, adds a gate rule that debounces its ambient traffic ' +
|
|
108
106
|
'(one wake after the burst settles), and pins the channel so it is never ' +
|
|
109
|
-
'auto-
|
|
110
|
-
'
|
|
111
|
-
'the default auto-
|
|
107
|
+
'auto-closed for being chatty. `mode: "mentions"` reverts to ' +
|
|
108
|
+
'direct-address-only: it removes that gate rule, closes, and restores ' +
|
|
109
|
+
'the default auto-close limit. `debounceMs` (100–300000, default ' +
|
|
112
110
|
`${this.defaultDebounceMs}) sets the quiet window for debounced mode.`,
|
|
113
111
|
inputSchema: {
|
|
114
112
|
type: 'object',
|
|
@@ -182,7 +180,7 @@ export class ChannelModeModule implements Module {
|
|
|
182
180
|
const steps: StepResult[] = [];
|
|
183
181
|
|
|
184
182
|
// 1. Subscribe to ambient traffic.
|
|
185
|
-
steps.push(await this.callToolStep('
|
|
183
|
+
steps.push(await this.callToolStep('open', 'channel_open', { channelId, serverId: this.serverId }));
|
|
186
184
|
|
|
187
185
|
// 2. Upsert the per-channel ambient debounce policy (prepend so it beats a
|
|
188
186
|
// broad ambient defer/debounce — first match wins). Validation lives in
|
|
@@ -201,7 +199,7 @@ export class ChannelModeModule implements Module {
|
|
|
201
199
|
steps.push({ step: 'gate-policy', ok: false, detail: errMsg(err) });
|
|
202
200
|
}
|
|
203
201
|
|
|
204
|
-
// 3. Pin subscription-gc off so a chatty channel doesn't auto-
|
|
202
|
+
// 3. Pin subscription-gc off so a chatty channel doesn't auto-close
|
|
205
203
|
// itself back out of debounced mode.
|
|
206
204
|
steps.push(
|
|
207
205
|
await this.callToolStep('gc-off', `${this.gcModuleName}--set_channel_idle_limit`, {
|
|
@@ -224,10 +222,10 @@ export class ChannelModeModule implements Module {
|
|
|
224
222
|
steps.push({ step: 'gate-policy', ok: false, detail: errMsg(err) });
|
|
225
223
|
}
|
|
226
224
|
|
|
227
|
-
// 2.
|
|
228
|
-
steps.push(await this.callToolStep('
|
|
225
|
+
// 2. Close ambient traffic through the generic host lifecycle.
|
|
226
|
+
steps.push(await this.callToolStep('close', 'channel_close', { channelId, serverId: this.serverId }));
|
|
229
227
|
|
|
230
|
-
// 3. Restore the default auto-
|
|
228
|
+
// 3. Restore the default auto-close limit.
|
|
231
229
|
steps.push(
|
|
232
230
|
await this.callToolStep('gc-default', `${this.gcModuleName}--set_channel_idle_limit`, {
|
|
233
231
|
channelId,
|
|
@@ -29,6 +29,7 @@ import type {
|
|
|
29
29
|
ToolCall,
|
|
30
30
|
ToolResult,
|
|
31
31
|
} from '@animalabs/agent-framework';
|
|
32
|
+
import { formatZonedDateTime, resolveTimeZone } from '@animalabs/agent-framework';
|
|
32
33
|
import { spawn as spawnProcess, type ChildProcess } from 'node:child_process';
|
|
33
34
|
import { connect as netConnect, type Socket } from 'node:net';
|
|
34
35
|
import { existsSync, mkdirSync, unlinkSync, openSync, closeSync, appendFileSync, realpathSync } from 'node:fs';
|
|
@@ -81,6 +82,8 @@ interface FleetEventSubscription {
|
|
|
81
82
|
// ---------------------------------------------------------------------------
|
|
82
83
|
|
|
83
84
|
export interface FleetModuleConfig {
|
|
85
|
+
/** IANA zone for wall-clock timestamps returned by fleet tools. */
|
|
86
|
+
timeZone?: string;
|
|
84
87
|
/** Default subscription sent to children at handshake (default: ['*']). */
|
|
85
88
|
defaultSubscription?: string[];
|
|
86
89
|
/** Max events to retain per child for peek (default: 500). */
|
|
@@ -255,6 +258,7 @@ export class FleetModule implements Module {
|
|
|
255
258
|
sigtermEscalationMs: config.sigtermEscalationMs ?? 5_000,
|
|
256
259
|
childIndexPath: config.childIndexPath ?? process.argv[1] ?? '',
|
|
257
260
|
childRuntimePath: config.childRuntimePath ?? process.execPath,
|
|
261
|
+
timeZone: resolveTimeZone(config.timeZone),
|
|
258
262
|
};
|
|
259
263
|
|
|
260
264
|
this.autoStartChildren = config.autoStart ?? [];
|
|
@@ -1014,7 +1018,12 @@ export class FleetModule implements Module {
|
|
|
1014
1018
|
{
|
|
1015
1019
|
detached: true,
|
|
1016
1020
|
stdio: startupFd !== null ? ['ignore', startupFd, startupFd] : 'ignore',
|
|
1017
|
-
env: {
|
|
1021
|
+
env: {
|
|
1022
|
+
...process.env,
|
|
1023
|
+
...input.env,
|
|
1024
|
+
AGENT_TIMEZONE: this.config.timeZone,
|
|
1025
|
+
DATA_DIR: dataDir,
|
|
1026
|
+
},
|
|
1018
1027
|
},
|
|
1019
1028
|
);
|
|
1020
1029
|
proc.unref();
|
|
@@ -1095,9 +1104,9 @@ export class FleetModule implements Module {
|
|
|
1095
1104
|
name: c.name,
|
|
1096
1105
|
status: c.status,
|
|
1097
1106
|
pid: c.pid,
|
|
1098
|
-
startedAt: c.startedAt,
|
|
1099
|
-
exitedAt: c.exitedAt,
|
|
1100
|
-
lastEventAt: c.lastEventAt,
|
|
1107
|
+
startedAt: formatZonedDateTime(c.startedAt, this.config.timeZone),
|
|
1108
|
+
exitedAt: c.exitedAt === null ? null : formatZonedDateTime(c.exitedAt, this.config.timeZone),
|
|
1109
|
+
lastEventAt: c.lastEventAt === null ? null : formatZonedDateTime(c.lastEventAt, this.config.timeZone),
|
|
1101
1110
|
eventCount: c.events.length,
|
|
1102
1111
|
}));
|
|
1103
1112
|
return { success: true, data: list };
|
|
@@ -1120,9 +1129,10 @@ export class FleetModule implements Module {
|
|
|
1120
1129
|
socketPath: c.socketPath,
|
|
1121
1130
|
pid: c.pid,
|
|
1122
1131
|
status: c.status,
|
|
1123
|
-
startedAt: c.startedAt,
|
|
1124
|
-
exitedAt: c.exitedAt,
|
|
1125
|
-
lastEventAt: c.lastEventAt,
|
|
1132
|
+
startedAt: formatZonedDateTime(c.startedAt, this.config.timeZone),
|
|
1133
|
+
exitedAt: c.exitedAt === null ? null : formatZonedDateTime(c.exitedAt, this.config.timeZone),
|
|
1134
|
+
lastEventAt: c.lastEventAt === null ? null : formatZonedDateTime(c.lastEventAt, this.config.timeZone),
|
|
1135
|
+
timeZone: this.config.timeZone,
|
|
1126
1136
|
exitCode: c.exitCode,
|
|
1127
1137
|
exitReason: c.exitReason,
|
|
1128
1138
|
eventCount: c.events.length,
|
|
@@ -33,6 +33,7 @@ import type {
|
|
|
33
33
|
AgentFramework,
|
|
34
34
|
McplServerConfig,
|
|
35
35
|
} from '@animalabs/agent-framework';
|
|
36
|
+
import { resolveTimeZone } from '@animalabs/agent-framework';
|
|
36
37
|
import {
|
|
37
38
|
DEFAULT_CONFIG_PATH,
|
|
38
39
|
DEFAULT_AGENT_OVERLAY_PATH,
|
|
@@ -44,6 +45,8 @@ import {
|
|
|
44
45
|
} from '../mcpl-config.js';
|
|
45
46
|
|
|
46
47
|
export interface McplAdminModuleConfig {
|
|
48
|
+
/** IANA zone propagated to newly deployed stdio servers. */
|
|
49
|
+
timeZone?: string;
|
|
47
50
|
/** Path to the agent overlay file. Default: `mcpl-servers.agent.json` in cwd. */
|
|
48
51
|
overlayPath?: string;
|
|
49
52
|
/** Path to the human-owned server config file (read-only here). */
|
|
@@ -64,10 +67,12 @@ export class McplAdminModule implements Module {
|
|
|
64
67
|
private framework: AgentFramework | null = null;
|
|
65
68
|
private overlayPath: string;
|
|
66
69
|
private configPath: string;
|
|
70
|
+
private timeZone: string;
|
|
67
71
|
|
|
68
72
|
constructor(config?: McplAdminModuleConfig) {
|
|
69
73
|
this.overlayPath = config?.overlayPath ?? DEFAULT_AGENT_OVERLAY_PATH;
|
|
70
74
|
this.configPath = config?.configPath ?? DEFAULT_CONFIG_PATH;
|
|
75
|
+
this.timeZone = resolveTimeZone(config?.timeZone);
|
|
71
76
|
}
|
|
72
77
|
|
|
73
78
|
/** Post-creation wiring (called from index.ts, mirrors ActivityModule.setFramework). */
|
|
@@ -109,16 +114,6 @@ export class McplAdminModule implements Module {
|
|
|
109
114
|
token: { type: 'string', description: 'Bearer token for WebSocket auth.' },
|
|
110
115
|
toolPrefix: { type: 'string', description: 'Tool namespace prefix. Default: mcpl--<id>.' },
|
|
111
116
|
reconnect: { type: 'boolean', description: 'Auto-reconnect on transport failure (default false). Note: does NOT respawn a crashed child — use mcpl_restart for that.' },
|
|
112
|
-
channelSubscription: {
|
|
113
|
-
type: 'string',
|
|
114
|
-
enum: ['auto', 'manual'],
|
|
115
|
-
description: "Channel auto-open policy: 'auto' (default) opens every channel the server registers; 'manual' opens none. For an allow-list, use channelAllowlist instead.",
|
|
116
|
-
},
|
|
117
|
-
channelAllowlist: {
|
|
118
|
-
type: 'array',
|
|
119
|
-
items: { type: 'string' },
|
|
120
|
-
description: 'Allow-list of channel ids to auto-open (overrides channelSubscription).',
|
|
121
|
-
},
|
|
122
117
|
enabledFeatureSets: { type: 'array', items: { type: 'string' } },
|
|
123
118
|
disabledFeatureSets: { type: 'array', items: { type: 'string' } },
|
|
124
119
|
enabledTools: { type: 'array', items: { type: 'string' }, description: 'Tool allow-list (bare names, * wildcard).' },
|
|
@@ -258,11 +253,6 @@ export class McplAdminModule implements Module {
|
|
|
258
253
|
if (typeof input.token === 'string') entry.token = input.token;
|
|
259
254
|
if (typeof input.toolPrefix === 'string') entry.toolPrefix = input.toolPrefix;
|
|
260
255
|
if (typeof input.reconnect === 'boolean') entry.reconnect = input.reconnect;
|
|
261
|
-
if (Array.isArray(input.channelAllowlist)) {
|
|
262
|
-
entry.channelSubscription = input.channelAllowlist.map(String);
|
|
263
|
-
} else if (input.channelSubscription === 'auto' || input.channelSubscription === 'manual') {
|
|
264
|
-
entry.channelSubscription = input.channelSubscription;
|
|
265
|
-
}
|
|
266
256
|
if (Array.isArray(input.enabledFeatureSets)) entry.enabledFeatureSets = input.enabledFeatureSets.map(String);
|
|
267
257
|
if (Array.isArray(input.disabledFeatureSets)) entry.disabledFeatureSets = input.disabledFeatureSets.map(String);
|
|
268
258
|
if (Array.isArray(input.enabledTools)) entry.enabledTools = input.enabledTools.map(String);
|
|
@@ -275,6 +265,7 @@ export class McplAdminModule implements Module {
|
|
|
275
265
|
saveAgentOverlay(this.overlayPath, overlay);
|
|
276
266
|
|
|
277
267
|
const config = resolveOverlayEntry(id, entry, this.overlayPath) as unknown as McplServerConfig;
|
|
268
|
+
config.env = { ...(config.env ?? {}), AGENT_TIMEZONE: this.timeZone };
|
|
278
269
|
|
|
279
270
|
const alreadyLoaded = framework.listMcplServers().some(s => s.id === id);
|
|
280
271
|
try {
|
|
@@ -3,10 +3,11 @@
|
|
|
3
3
|
* itself. State persists to chronicle via ModuleContext (`setState`/`getState`)
|
|
4
4
|
* so changes survive restarts.
|
|
5
5
|
*
|
|
6
|
-
* First domain: **reasoning** (Anthropic extended thinking)
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
6
|
+
* First domain: **reasoning** (Anthropic extended thinking), surfaced to the
|
|
7
|
+
* agent as `agent_settings` fields (reasoning_enabled /
|
|
8
|
+
* reasoning_budget_tokens) via the framework's settings-extension hook —
|
|
9
|
+
* NOT as standalone tools (the former reasoning_status/enable/disable trio
|
|
10
|
+
* was tool bloat for one boolean + number).
|
|
10
11
|
*
|
|
11
12
|
* The host's adapter wrapper (LoggingAnthropicAdapter) reads `getReasoning()`
|
|
12
13
|
* on each call and injects `thinking: {type:'enabled', budget_tokens: N}` into
|
|
@@ -69,75 +70,98 @@ export class SettingsModule implements Module {
|
|
|
69
70
|
return { ...this.state.reasoning };
|
|
70
71
|
}
|
|
71
72
|
|
|
73
|
+
/** No standalone tools — reasoning controls live inside the framework's
|
|
74
|
+
* `agent_settings` tool via getAgentSettingsExtension() below. The three
|
|
75
|
+
* former reasoning_* tools were pure tool bloat for one boolean + number. */
|
|
72
76
|
getTools(): ToolDefinition[] {
|
|
73
|
-
return [
|
|
74
|
-
{
|
|
75
|
-
name: 'reasoning_status',
|
|
76
|
-
description:
|
|
77
|
-
'Show whether extended thinking (reasoning) is currently enabled and the token budget.',
|
|
78
|
-
inputSchema: { type: 'object', properties: {} },
|
|
79
|
-
},
|
|
80
|
-
{
|
|
81
|
-
name: 'reasoning_enable',
|
|
82
|
-
description:
|
|
83
|
-
'Enable extended thinking (reasoning) on your subsequent inference calls. ' +
|
|
84
|
-
'Optional budgetTokens in tokens (default: current value, initially 8192; min 1024).',
|
|
85
|
-
inputSchema: {
|
|
86
|
-
type: 'object',
|
|
87
|
-
properties: {
|
|
88
|
-
budgetTokens: {
|
|
89
|
-
type: 'number',
|
|
90
|
-
description: 'Token budget for thinking blocks (min 1024).',
|
|
91
|
-
},
|
|
92
|
-
},
|
|
93
|
-
},
|
|
94
|
-
},
|
|
95
|
-
{
|
|
96
|
-
name: 'reasoning_disable',
|
|
97
|
-
description:
|
|
98
|
-
'Disable extended thinking (reasoning). Subsequent inference calls will not request thinking blocks.',
|
|
99
|
-
inputSchema: { type: 'object', properties: {} },
|
|
100
|
-
},
|
|
101
|
-
];
|
|
77
|
+
return [];
|
|
102
78
|
}
|
|
103
79
|
|
|
104
80
|
async handleToolCall(call: ToolCall): Promise<ToolResult> {
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
81
|
+
return {
|
|
82
|
+
success: false,
|
|
83
|
+
error:
|
|
84
|
+
`Unknown tool: ${call.name}. Reasoning controls moved into agent_settings ` +
|
|
85
|
+
`(fields reasoning_enabled / reasoning_budget_tokens).`,
|
|
86
|
+
isError: true,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Declare reasoning as an agent_settings extension: the framework merges
|
|
92
|
+
* these fields into the agent_settings tool and routes get/update/reset for
|
|
93
|
+
* them back here. Framework versions predating the hook simply never call
|
|
94
|
+
* this — in that case reasoning is temporarily not agent-tunable (the
|
|
95
|
+
* adapter still honors persisted state).
|
|
96
|
+
*/
|
|
97
|
+
getAgentSettingsExtension(): {
|
|
98
|
+
properties: Record<string, unknown>;
|
|
99
|
+
keys: string[];
|
|
100
|
+
get(agentName: string): Record<string, unknown>;
|
|
101
|
+
update(agentName: string, patch: Record<string, unknown>): Record<string, unknown>;
|
|
102
|
+
reset(agentName: string, keys?: string[]): Record<string, unknown>;
|
|
103
|
+
} {
|
|
104
|
+
return {
|
|
105
|
+
properties: {
|
|
106
|
+
reasoning_enabled: {
|
|
107
|
+
type: 'boolean',
|
|
108
|
+
description:
|
|
109
|
+
'Extended thinking (reasoning) on subsequent inference calls.',
|
|
110
|
+
},
|
|
111
|
+
reasoning_budget_tokens: {
|
|
112
|
+
type: 'number',
|
|
113
|
+
description: 'Token budget for thinking blocks (min 1024).',
|
|
114
|
+
},
|
|
115
|
+
},
|
|
116
|
+
keys: ['reasoning_enabled', 'reasoning_budget_tokens'],
|
|
117
|
+
get: () => this.reasoningSettingsView(),
|
|
118
|
+
update: (_agentName, patch) => {
|
|
119
|
+
const next = { ...this.state.reasoning };
|
|
120
|
+
if (patch.reasoning_enabled !== undefined) {
|
|
121
|
+
if (typeof patch.reasoning_enabled !== 'boolean') {
|
|
122
|
+
throw new Error('reasoning_enabled must be a boolean');
|
|
123
|
+
}
|
|
124
|
+
next.enabled = patch.reasoning_enabled;
|
|
125
|
+
}
|
|
126
|
+
if (patch.reasoning_budget_tokens !== undefined) {
|
|
127
|
+
const budget = Number(patch.reasoning_budget_tokens);
|
|
128
|
+
if (!Number.isFinite(budget)) {
|
|
129
|
+
throw new Error('reasoning_budget_tokens must be a number');
|
|
130
|
+
}
|
|
131
|
+
next.budgetTokens = Math.max(1024, Math.round(budget));
|
|
132
|
+
}
|
|
133
|
+
this.state.reasoning = next;
|
|
115
134
|
this.ctx?.setState(this.state);
|
|
116
|
-
return
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
|
|
135
|
+
return this.reasoningSettingsView();
|
|
136
|
+
},
|
|
137
|
+
reset: (_agentName, keys) => {
|
|
138
|
+
const all = !keys || keys.length === 0;
|
|
139
|
+
if (all || keys?.includes('reasoning_enabled')) {
|
|
140
|
+
this.state.reasoning.enabled = DEFAULTS.reasoning.enabled;
|
|
141
|
+
}
|
|
142
|
+
if (all || keys?.includes('reasoning_budget_tokens')) {
|
|
143
|
+
this.state.reasoning.budgetTokens = DEFAULTS.reasoning.budgetTokens;
|
|
144
|
+
}
|
|
120
145
|
this.ctx?.setState(this.state);
|
|
121
|
-
return
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
146
|
+
return this.reasoningSettingsView();
|
|
147
|
+
},
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** The extension's wire view of reasoning state (flat agent_settings keys). */
|
|
152
|
+
private reasoningSettingsView(): Record<string, unknown> {
|
|
153
|
+
return {
|
|
154
|
+
reasoning_enabled: this.state.reasoning.enabled,
|
|
155
|
+
reasoning_budget_tokens: this.state.reasoning.budgetTokens,
|
|
156
|
+
};
|
|
125
157
|
}
|
|
126
158
|
|
|
127
159
|
async onProcess(_event: ProcessEvent, _state: ProcessState): Promise<EventResponse> {
|
|
128
160
|
return {};
|
|
129
161
|
}
|
|
130
162
|
|
|
131
|
-
private reasoningStatusText(): string {
|
|
132
|
-
const r = this.state.reasoning;
|
|
133
|
-
return `reasoning=${r.enabled ? 'ENABLED' : 'disabled'}, budgetTokens=${r.budgetTokens}`;
|
|
134
|
-
}
|
|
135
163
|
}
|
|
136
164
|
|
|
137
165
|
function clone<T>(x: T): T {
|
|
138
166
|
return JSON.parse(JSON.stringify(x)) as T;
|
|
139
167
|
}
|
|
140
|
-
|
|
141
|
-
function ok(message: string): ToolResult {
|
|
142
|
-
return { success: true, data: { message }, isError: false };
|
|
143
|
-
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* SubscriptionGcModule — auto-
|
|
2
|
+
* SubscriptionGcModule — auto-close noisy ordinary channel traffic.
|
|
3
3
|
*
|
|
4
4
|
* Problem: a subscribed channel delivers ambient (non-mention, non-DM) messages
|
|
5
5
|
* into the agent's context whether or not the agent ever acts on them. A busy
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
* the agent saw every subscribed channel in that context (compressed if
|
|
13
13
|
* large; nothing is dropped), so the slate is clean.
|
|
14
14
|
* - The instant a channel's since-last-activation count crosses its limit, it
|
|
15
|
-
* is auto-
|
|
15
|
+
* is auto-closed via the host's generic `channel_close` tool. This
|
|
16
16
|
* bounds any one channel's context pollution to ~limit characters.
|
|
17
17
|
*
|
|
18
18
|
* Everything is durable across restarts: counters + per-channel overrides
|
|
@@ -36,15 +36,15 @@ import type {
|
|
|
36
36
|
} from '@animalabs/agent-framework';
|
|
37
37
|
|
|
38
38
|
export interface SubscriptionGcConfig {
|
|
39
|
-
/** Default ambient-character budget per channel before auto-
|
|
39
|
+
/** Default ambient-character budget per channel before auto-close. */
|
|
40
40
|
defaultLimitChars?: number;
|
|
41
41
|
/** MCPL server id that owns subscriptions (default 'discord'). */
|
|
42
42
|
serverId?: string;
|
|
43
|
-
/**
|
|
43
|
+
/** @deprecated Lifecycle tools are host-generic; retained for config compatibility. */
|
|
44
44
|
toolPrefix?: string;
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
-
/** `'off'` pins a channel (never auto-
|
|
47
|
+
/** `'off'` pins a channel (never auto-close); a number overrides the
|
|
48
48
|
* default limit; absence falls back to the default. */
|
|
49
49
|
type LimitOverride = number | 'off';
|
|
50
50
|
|
|
@@ -66,7 +66,6 @@ export class SubscriptionGcModule implements Module {
|
|
|
66
66
|
|
|
67
67
|
private readonly defaultLimitChars: number;
|
|
68
68
|
private readonly serverId: string;
|
|
69
|
-
private readonly toolPrefix: string;
|
|
70
69
|
|
|
71
70
|
private state: GcState = { overrides: {}, counters: {} };
|
|
72
71
|
|
|
@@ -76,7 +75,6 @@ export class SubscriptionGcModule implements Module {
|
|
|
76
75
|
? Math.round(config.defaultLimitChars)
|
|
77
76
|
: 20000;
|
|
78
77
|
this.serverId = config.serverId ?? 'discord';
|
|
79
|
-
this.toolPrefix = config.toolPrefix ?? `mcpl--${this.serverId}`;
|
|
80
78
|
}
|
|
81
79
|
|
|
82
80
|
async start(ctx: ModuleContext): Promise<void> {
|
|
@@ -121,9 +119,9 @@ export class SubscriptionGcModule implements Module {
|
|
|
121
119
|
description:
|
|
122
120
|
'Set how many characters of ambient (non-mention, non-DM) traffic a ' +
|
|
123
121
|
'subscribed channel may emit between your activations before it is ' +
|
|
124
|
-
'auto-
|
|
122
|
+
'auto-closed. `limit` is a number of characters, "default" to ' +
|
|
125
123
|
`use the global default (${this.defaultLimitChars}), or "off" to ` +
|
|
126
|
-
'never auto-
|
|
124
|
+
'never auto-close this channel.',
|
|
127
125
|
inputSchema: {
|
|
128
126
|
type: 'object',
|
|
129
127
|
properties: {
|
|
@@ -139,7 +137,7 @@ export class SubscriptionGcModule implements Module {
|
|
|
139
137
|
{
|
|
140
138
|
name: 'list_channel_idle_limits',
|
|
141
139
|
description:
|
|
142
|
-
'Show the auto-
|
|
140
|
+
'Show the auto-close configuration: the global default limit, ' +
|
|
143
141
|
'per-channel overrides, and the current since-last-activation ambient ' +
|
|
144
142
|
'character counts per channel.',
|
|
145
143
|
inputSchema: { type: 'object', properties: {} },
|
|
@@ -169,7 +167,7 @@ export class SubscriptionGcModule implements Module {
|
|
|
169
167
|
desc = `default (${this.defaultLimitChars})`;
|
|
170
168
|
} else if (limit === 'off') {
|
|
171
169
|
this.state.overrides[channelId] = 'off';
|
|
172
|
-
desc = 'off (never auto-
|
|
170
|
+
desc = 'off (never auto-close)';
|
|
173
171
|
} else if (Number.isFinite(numeric) && numeric > 0) {
|
|
174
172
|
this.state.overrides[channelId] = Math.round(numeric);
|
|
175
173
|
desc = `${Math.round(numeric)} characters`;
|
|
@@ -207,14 +205,14 @@ export class SubscriptionGcModule implements Module {
|
|
|
207
205
|
const next = (this.state.counters[channelId] ?? 0) + chars;
|
|
208
206
|
|
|
209
207
|
if (limit !== Infinity && next > limit) {
|
|
210
|
-
// Cross the threshold →
|
|
208
|
+
// Cross the threshold → close and clear the counter.
|
|
211
209
|
delete this.state.counters[channelId];
|
|
212
210
|
this.persistNow();
|
|
213
211
|
const result = await this.ctx
|
|
214
212
|
?.callTool({
|
|
215
213
|
id: `gc-unsub-${this.callSeq++}`,
|
|
216
|
-
name:
|
|
217
|
-
input: { channelId },
|
|
214
|
+
name: 'channel_close',
|
|
215
|
+
input: { channelId, serverId: this.serverId },
|
|
218
216
|
})
|
|
219
217
|
.catch((err: unknown) => ({
|
|
220
218
|
success: false,
|
|
@@ -231,18 +229,17 @@ export class SubscriptionGcModule implements Module {
|
|
|
231
229
|
{
|
|
232
230
|
type: 'text',
|
|
233
231
|
text:
|
|
234
|
-
`[subscription-gc] Auto-
|
|
232
|
+
`[subscription-gc] Auto-closed channel ${channelId}: it emitted ` +
|
|
235
233
|
`over ${limit} characters of ambient traffic since your last activation without ` +
|
|
236
|
-
`engagement.
|
|
237
|
-
|
|
238
|
-
`set_channel_idle_limit, or check what you're missing with ${this.toolPrefix}--channel_missed.`,
|
|
234
|
+
`engagement. Direct addresses there still reach you. Reopen with channel_open, ` +
|
|
235
|
+
`or raise/disable its limit with set_channel_idle_limit.`,
|
|
239
236
|
},
|
|
240
237
|
],
|
|
241
238
|
},
|
|
242
239
|
],
|
|
243
240
|
};
|
|
244
241
|
}
|
|
245
|
-
//
|
|
242
|
+
// Close failed — keep the channel counted so we retry on the next
|
|
246
243
|
// ambient message rather than silently giving up.
|
|
247
244
|
this.state.counters[channelId] = next;
|
|
248
245
|
this.persistNow();
|
|
@@ -270,7 +267,7 @@ export class SubscriptionGcModule implements Module {
|
|
|
270
267
|
if (event.serverId !== this.serverId) return null;
|
|
271
268
|
const origin = (event.origin ?? {}) as Record<string, unknown>;
|
|
272
269
|
if (origin.isMention || origin.isDM) return null;
|
|
273
|
-
const channelId = origin.channelId;
|
|
270
|
+
const channelId = origin.mcplChannelId ?? origin.channelId;
|
|
274
271
|
if (typeof channelId !== 'string' || channelId.length === 0) return null;
|
|
275
272
|
return { channelId, chars: textLen(event.content) };
|
|
276
273
|
}
|
|
@@ -15,21 +15,23 @@ import type {
|
|
|
15
15
|
ToolCall,
|
|
16
16
|
ToolResult,
|
|
17
17
|
} from '@animalabs/agent-framework';
|
|
18
|
+
import { formatZonedDateTime, resolveTimeZone } from '@animalabs/agent-framework';
|
|
18
19
|
|
|
19
20
|
interface TimeState {
|
|
20
21
|
sessionStartAnnounced?: boolean;
|
|
21
22
|
}
|
|
22
23
|
|
|
23
|
-
function formatNow(date: Date = new Date()): {
|
|
24
|
+
export function formatNow(date: Date = new Date(), configuredTimeZone?: string): {
|
|
24
25
|
iso: string;
|
|
25
26
|
local: string;
|
|
26
27
|
timezone: string;
|
|
27
28
|
unixMs: number;
|
|
28
29
|
} {
|
|
29
|
-
const timezone =
|
|
30
|
+
const timezone = resolveTimeZone(configuredTimeZone);
|
|
31
|
+
const local = formatZonedDateTime(date, timezone);
|
|
30
32
|
return {
|
|
31
|
-
iso:
|
|
32
|
-
local
|
|
33
|
+
iso: local.slice(0, local.indexOf(' [')),
|
|
34
|
+
local,
|
|
33
35
|
timezone,
|
|
34
36
|
unixMs: date.getTime(),
|
|
35
37
|
};
|
|
@@ -39,6 +41,11 @@ export class TimeModule implements Module {
|
|
|
39
41
|
readonly name = 'time';
|
|
40
42
|
|
|
41
43
|
private ctx: ModuleContext | null = null;
|
|
44
|
+
private readonly timeZone: string;
|
|
45
|
+
|
|
46
|
+
constructor(timeZone?: string) {
|
|
47
|
+
this.timeZone = resolveTimeZone(timeZone);
|
|
48
|
+
}
|
|
42
49
|
|
|
43
50
|
async start(ctx: ModuleContext): Promise<void> {
|
|
44
51
|
this.ctx = ctx;
|
|
@@ -46,10 +53,9 @@ export class TimeModule implements Module {
|
|
|
46
53
|
const state = ctx.getState<TimeState>() ?? {};
|
|
47
54
|
if (state.sessionStartAnnounced) return;
|
|
48
55
|
|
|
49
|
-
const now = formatNow();
|
|
56
|
+
const now = formatNow(new Date(), this.timeZone);
|
|
50
57
|
const text =
|
|
51
|
-
`The time at the start of this session is
|
|
52
|
-
`(local: ${now.local}, timezone: ${now.timezone}).`;
|
|
58
|
+
`The local time at the start of this session is ${now.local}.`;
|
|
53
59
|
|
|
54
60
|
ctx.addMessage('user', [{ type: 'text', text }]);
|
|
55
61
|
ctx.setState<TimeState>({ ...state, sessionStartAnnounced: true });
|
|
@@ -67,7 +73,7 @@ export class TimeModule implements Module {
|
|
|
67
73
|
return [
|
|
68
74
|
{
|
|
69
75
|
name: 'now',
|
|
70
|
-
description: 'Return the current wall-clock time
|
|
76
|
+
description: 'Return the current wall-clock time in the agent-configured timezone, plus unix milliseconds.',
|
|
71
77
|
inputSchema: {
|
|
72
78
|
type: 'object',
|
|
73
79
|
properties: {},
|
|
@@ -78,7 +84,7 @@ export class TimeModule implements Module {
|
|
|
78
84
|
|
|
79
85
|
async handleToolCall(call: ToolCall): Promise<ToolResult> {
|
|
80
86
|
if (call.name === 'now') {
|
|
81
|
-
return { success: true, data: formatNow() };
|
|
87
|
+
return { success: true, data: formatNow(new Date(), this.timeZone) };
|
|
82
88
|
}
|
|
83
89
|
return { success: false, isError: true, error: `Unknown tool: ${call.name}` };
|
|
84
90
|
}
|