@animalabs/connectome-host 0.3.7 → 0.3.9
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/.github/workflows/ci.yml +52 -0
- package/.github/workflows/publish.yml +2 -2
- package/bun.lock +7 -7
- package/package.json +6 -4
- package/src/framework-agent-config.ts +51 -0
- package/src/framework-strategy.ts +70 -0
- package/src/index.ts +68 -94
- package/src/logging-adapter.ts +33 -3
- package/src/mcpl-config.ts +1 -0
- package/src/modules/channel-mode-module.ts +14 -16
- package/src/modules/mcpl-admin-module.ts +0 -15
- package/src/modules/settings-module.ts +83 -59
- package/src/modules/subscription-gc-module.ts +17 -20
- package/src/modules/web-ui-module.ts +63 -10
- package/src/modules/web-ui-observers.ts +19 -6
- package/src/recipe.ts +53 -3
- package/src/state/agent-tree-reducer.ts +17 -3
- package/src/strategies/frontdesk-strategy.ts +5 -5
- package/test/agent-tree-reducer.test.ts +35 -3
- package/test/autobio-progress-snapshot.test.ts +27 -12
- package/test/framework-fkm-composition.test.ts +151 -0
- package/test/logging-adapter-refusal.test.ts +57 -0
- package/test/recipe-compression-fallback.test.ts +36 -0
- package/test/recipe-primary-summary-fallback-rejection.test.ts +24 -0
- package/test/recipe-think-policy.test.ts +39 -0
- package/test/subscription-gc-module.test.ts +7 -5
- package/test/web-ui-observers.test.ts +27 -2
- package/web/package-lock.json +425 -302
- package/web/bun.lock +0 -357
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches:
|
|
6
|
+
- main
|
|
7
|
+
pull_request:
|
|
8
|
+
|
|
9
|
+
permissions:
|
|
10
|
+
contents: read
|
|
11
|
+
|
|
12
|
+
jobs:
|
|
13
|
+
build-and-test:
|
|
14
|
+
name: Build & Test (${{ matrix.os }})
|
|
15
|
+
runs-on: ${{ matrix.os }}
|
|
16
|
+
strategy:
|
|
17
|
+
fail-fast: false
|
|
18
|
+
# Both platforms on purpose: a package-lock.json regenerated over an
|
|
19
|
+
# existing node_modules tree records only that machine's native binaries
|
|
20
|
+
# (esbuild/rollup/tailwind-oxide), so a lock made on macOS breaks Linux
|
|
21
|
+
# installs and vice versa. Each runner catches the lock the other's
|
|
22
|
+
# platform produced. macOS runs the install+build lock guard only;
|
|
23
|
+
# the full test suite runs on ubuntu.
|
|
24
|
+
matrix:
|
|
25
|
+
os: [ubuntu-latest, macos-latest]
|
|
26
|
+
|
|
27
|
+
steps:
|
|
28
|
+
- uses: actions/checkout@v6
|
|
29
|
+
|
|
30
|
+
- name: Setup Node.js
|
|
31
|
+
uses: actions/setup-node@v6
|
|
32
|
+
with:
|
|
33
|
+
node-version: 24
|
|
34
|
+
|
|
35
|
+
- name: Setup Bun
|
|
36
|
+
uses: oven-sh/setup-bun@v2
|
|
37
|
+
with:
|
|
38
|
+
bun-version: 1.3.14
|
|
39
|
+
|
|
40
|
+
- name: Install dependencies
|
|
41
|
+
run: npm install
|
|
42
|
+
|
|
43
|
+
# npm ci is the actual lockfile guard: `npm install` (npm >= 11)
|
|
44
|
+
# silently re-resolves platform packages missing from the lock, so a
|
|
45
|
+
# broken lock sails through it. npm ci installs the lock verbatim and
|
|
46
|
+
# the vite build then fails loudly on the missing rollup binary.
|
|
47
|
+
- name: Build web assets (strict lockfile)
|
|
48
|
+
run: npm run build:web:ci
|
|
49
|
+
|
|
50
|
+
- name: Test
|
|
51
|
+
if: matrix.os == 'ubuntu-latest'
|
|
52
|
+
run: npm test
|
|
@@ -31,7 +31,7 @@ jobs:
|
|
|
31
31
|
run: npm install
|
|
32
32
|
|
|
33
33
|
- name: Build web assets
|
|
34
|
-
run: npm run build:web
|
|
34
|
+
run: npm run build:web:ci
|
|
35
35
|
|
|
36
36
|
- name: Test
|
|
37
37
|
run: npm test
|
|
@@ -64,7 +64,7 @@ jobs:
|
|
|
64
64
|
run: npm install
|
|
65
65
|
|
|
66
66
|
- name: Build web assets
|
|
67
|
-
run: npm run build:web
|
|
67
|
+
run: npm run build:web:ci
|
|
68
68
|
|
|
69
69
|
- name: Publish
|
|
70
70
|
run: npm publish --access public --provenance
|
package/bun.lock
CHANGED
|
@@ -5,10 +5,10 @@
|
|
|
5
5
|
"": {
|
|
6
6
|
"name": "connectome-host",
|
|
7
7
|
"dependencies": {
|
|
8
|
-
"@animalabs/agent-framework": "^0.6.
|
|
8
|
+
"@animalabs/agent-framework": "^0.6.8",
|
|
9
9
|
"@animalabs/chronicle": "^0.2.0",
|
|
10
|
-
"@animalabs/context-manager": "^0.5.
|
|
11
|
-
"@animalabs/membrane": "^0.5.
|
|
10
|
+
"@animalabs/context-manager": "^0.5.13",
|
|
11
|
+
"@animalabs/membrane": "^0.5.73",
|
|
12
12
|
"@opentui/core": "^0.1.82",
|
|
13
13
|
},
|
|
14
14
|
"devDependencies": {
|
|
@@ -19,13 +19,13 @@
|
|
|
19
19
|
},
|
|
20
20
|
},
|
|
21
21
|
"packages": {
|
|
22
|
-
"@animalabs/agent-framework": ["@animalabs/agent-framework@0.6.
|
|
22
|
+
"@animalabs/agent-framework": ["@animalabs/agent-framework@0.6.8", "", { "dependencies": { "@animalabs/chronicle": "^0.2.2", "@animalabs/context-manager": "^0.5.13", "@animalabs/membrane": "^0.5.73", "chokidar": "^4.0.3", "discord.js": "^14.25.1", "ws": "^8.18.0" }, "bin": { "agent-framework-mcp": "dist/src/api/mcp-server.js", "agent-framework-recover": "dist/src/recovery/recover-cli.js" } }, "sha512-hLu+DI0Xj40zSRm+KmjEZO/uzizd2Gbaiih4IGNXJJZf0mcIxYDbOynEMpaLHtXGYMS8eYFmQK02ytdL9thtoA=="],
|
|
23
23
|
|
|
24
24
|
"@animalabs/chronicle": ["@animalabs/chronicle@0.2.1", "", { "optionalDependencies": { "@animalabs/chronicle-darwin-arm64": "0.2.1", "@animalabs/chronicle-darwin-x64": "0.2.1", "@animalabs/chronicle-linux-arm64-gnu": "0.2.1", "@animalabs/chronicle-linux-x64-gnu": "0.2.1", "@animalabs/chronicle-win32-x64-msvc": "0.2.1" } }, "sha512-rDFc069yfi7BQUusgsXBwBdqljUWLByXSYaTF40AP+tonBh0No3eF9VzKTSy1I8DYg/jAUHIKtVdKDGsVMVubw=="],
|
|
25
25
|
|
|
26
|
-
"@animalabs/context-manager": ["@animalabs/context-manager@0.5.
|
|
26
|
+
"@animalabs/context-manager": ["@animalabs/context-manager@0.5.13", "", { "dependencies": { "@animalabs/chronicle": "^0.2.6", "@animalabs/membrane": "^0.5.69" } }, "sha512-1NOsyCeiJoAa4MrBSGw1zbbs2XfvhRbkMfkFNCNFH9gSFTq/4tMwMu6lmZ082oNeB9FvptGrjCuZdVP8B9/UdA=="],
|
|
27
27
|
|
|
28
|
-
"@animalabs/membrane": ["@animalabs/membrane@0.5.
|
|
28
|
+
"@animalabs/membrane": ["@animalabs/membrane@0.5.73", "", { "dependencies": { "@anthropic-ai/sdk": "^0.52.0" } }, "sha512-w1rt/JVOD6dWwEO54tfR0yrLzwwD45cpYJ9hWJhHxs8k9FvW1PgIIV1KmNFNbpY8MR9HC5g7Lqgvhzs3pDst+g=="],
|
|
29
29
|
|
|
30
30
|
"@anthropic-ai/sdk": ["@anthropic-ai/sdk@0.52.0", "", { "bin": { "anthropic-ai-sdk": "bin/cli" } }, "sha512-d4c+fg+xy9e46c8+YnrrgIQR45CZlAi7PwdzIfDXDM6ACxEZli1/fxhURsq30ZpMZy6LvSkr41jGq5aF5TD7rQ=="],
|
|
31
31
|
|
|
@@ -277,7 +277,7 @@
|
|
|
277
277
|
|
|
278
278
|
"@animalabs/agent-framework/@animalabs/chronicle": ["@animalabs/chronicle@0.2.6", "", { "optionalDependencies": { "@animalabs/chronicle-darwin-arm64": "0.2.6", "@animalabs/chronicle-darwin-x64": "0.2.6", "@animalabs/chronicle-linux-arm64-gnu": "0.2.6", "@animalabs/chronicle-linux-x64-gnu": "0.2.6", "@animalabs/chronicle-win32-x64-msvc": "0.2.6" } }, "sha512-+gbt2GR4SP8MnKxeqkJZf6oZn339fLiBtk6GHyBD/gHQ4e4eFqBpTaTd5eKycKBws0xWGcOqyiJFH4IdBkweUA=="],
|
|
279
279
|
|
|
280
|
-
"@animalabs/context-manager/@animalabs/
|
|
280
|
+
"@animalabs/context-manager/@animalabs/chronicle": ["@animalabs/chronicle@0.2.6", "", { "optionalDependencies": { "@animalabs/chronicle-darwin-arm64": "0.2.6", "@animalabs/chronicle-darwin-x64": "0.2.6", "@animalabs/chronicle-linux-arm64-gnu": "0.2.6", "@animalabs/chronicle-linux-x64-gnu": "0.2.6", "@animalabs/chronicle-win32-x64-msvc": "0.2.6" } }, "sha512-+gbt2GR4SP8MnKxeqkJZf6oZn339fLiBtk6GHyBD/gHQ4e4eFqBpTaTd5eKycKBws0xWGcOqyiJFH4IdBkweUA=="],
|
|
281
281
|
|
|
282
282
|
"@discordjs/rest/@discordjs/collection": ["@discordjs/collection@2.1.1", "", {}, "sha512-LiSusze9Tc7qF03sLCujF5iZp7K+vRNEDBZ86FT9aQAv3vxMLihUvKvpsCWiQ2DJq1tVckopKm1rxomgNUc9hg=="],
|
|
283
283
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@animalabs/connectome-host",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.9",
|
|
4
4
|
"description": "General-purpose agent TUI host with recipe-based configuration",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"scripts": {
|
|
@@ -8,13 +8,15 @@
|
|
|
8
8
|
"dev": "bun --watch src/index.ts",
|
|
9
9
|
"test": "bun test",
|
|
10
10
|
"build:web": "npm --prefix web install && npm --prefix web run build",
|
|
11
|
+
"build:web:ci": "npm ci --prefix web && npm --prefix web run build",
|
|
12
|
+
"relock:web": "rm -rf web/node_modules web/package-lock.json && npm --prefix web install",
|
|
11
13
|
"postinstall": "test -d web && npm --prefix web install && npm --prefix web run build || true"
|
|
12
14
|
},
|
|
13
15
|
"dependencies": {
|
|
14
|
-
"@animalabs/agent-framework": "^0.6.
|
|
16
|
+
"@animalabs/agent-framework": "^0.6.8",
|
|
15
17
|
"@animalabs/chronicle": "^0.2.0",
|
|
16
|
-
"@animalabs/context-manager": "^0.5.
|
|
17
|
-
"@animalabs/membrane": "^0.5.
|
|
18
|
+
"@animalabs/context-manager": "^0.5.13",
|
|
19
|
+
"@animalabs/membrane": "^0.5.73",
|
|
18
20
|
"@opentui/core": "^0.1.82"
|
|
19
21
|
},
|
|
20
22
|
"devDependencies": {
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { AgentFramework } from '@animalabs/agent-framework';
|
|
2
|
+
import type { Recipe } from './recipe.js';
|
|
3
|
+
|
|
4
|
+
type AgentConfig = Parameters<typeof AgentFramework.create>[0]['agents'][number];
|
|
5
|
+
|
|
6
|
+
export type FrameworkAgentConfig = AgentConfig & {
|
|
7
|
+
// Forward recipe fields that newer Agent Framework releases understand
|
|
8
|
+
// while remaining structurally compatible with older installs.
|
|
9
|
+
refusalHandling?: Recipe['agent']['refusalHandling'];
|
|
10
|
+
sameRoundThinkTextPolicy?: 'public' | 'private';
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export function buildFrameworkAgentConfig(
|
|
14
|
+
recipe: Recipe,
|
|
15
|
+
agentName: string,
|
|
16
|
+
model: string,
|
|
17
|
+
strategy: FrameworkAgentConfig['strategy'],
|
|
18
|
+
): FrameworkAgentConfig {
|
|
19
|
+
return {
|
|
20
|
+
name: agentName,
|
|
21
|
+
model,
|
|
22
|
+
systemPrompt: recipe.agent.systemPrompt,
|
|
23
|
+
maxTokens: recipe.agent.maxTokens ?? 16384,
|
|
24
|
+
maxStreamTokens: recipe.agent.maxStreamTokens ?? 150000,
|
|
25
|
+
contextBudgetTokens: recipe.agent.contextBudgetTokens,
|
|
26
|
+
...(recipe.agent.cacheTtl && { cacheTtl: recipe.agent.cacheTtl }),
|
|
27
|
+
...(recipe.agent.provider === 'openai-responses' && {
|
|
28
|
+
providerParams: {
|
|
29
|
+
reasoning: {
|
|
30
|
+
effort: recipe.agent.responses?.reasoningEffort ?? 'high',
|
|
31
|
+
context: recipe.agent.responses?.reasoningContext ?? 'all_turns',
|
|
32
|
+
},
|
|
33
|
+
...(recipe.agent.responses?.serviceTier ? {
|
|
34
|
+
service_tier: recipe.agent.responses.serviceTier,
|
|
35
|
+
} : {}),
|
|
36
|
+
...(recipe.agent.responses?.compactThreshold ? {
|
|
37
|
+
context_management: [{
|
|
38
|
+
type: 'compaction',
|
|
39
|
+
compact_threshold: recipe.agent.responses.compactThreshold,
|
|
40
|
+
}],
|
|
41
|
+
} : {}),
|
|
42
|
+
},
|
|
43
|
+
}),
|
|
44
|
+
strategy,
|
|
45
|
+
...(recipe.agent.thinking && { thinking: recipe.agent.thinking }),
|
|
46
|
+
...(recipe.agent.refusalHandling && { refusalHandling: recipe.agent.refusalHandling }),
|
|
47
|
+
...(recipe.agent.sameRoundThinkTextPolicy !== undefined
|
|
48
|
+
? { sameRoundThinkTextPolicy: recipe.agent.sameRoundThinkTextPolicy }
|
|
49
|
+
: {}),
|
|
50
|
+
};
|
|
51
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import {
|
|
2
|
+
AutobiographicalStrategy,
|
|
3
|
+
PassthroughStrategy,
|
|
4
|
+
type ContextStrategy,
|
|
5
|
+
} from '@animalabs/agent-framework';
|
|
6
|
+
import type { Recipe, RecipeStrategy } from './recipe.js';
|
|
7
|
+
import { FrontdeskStrategy } from './strategies/frontdesk-strategy.js';
|
|
8
|
+
|
|
9
|
+
const PASSTHROUGH_KEYS: ReadonlyArray<keyof RecipeStrategy> = [
|
|
10
|
+
'enforceBudget',
|
|
11
|
+
'maxSpeculativeL1s',
|
|
12
|
+
'compressionRefusalCurveFallbacks',
|
|
13
|
+
'compressionContextBudgetTokens',
|
|
14
|
+
'positionedRecallPairs',
|
|
15
|
+
'recallHeaderTemplate',
|
|
16
|
+
'targetChunkTokens',
|
|
17
|
+
'mergeThreshold',
|
|
18
|
+
'summaryTargetTokens',
|
|
19
|
+
'l1BudgetTokens',
|
|
20
|
+
'l2BudgetTokens',
|
|
21
|
+
'l3BudgetTokens',
|
|
22
|
+
'toolResultMaxLastN',
|
|
23
|
+
'toolUseInputMaxTokens',
|
|
24
|
+
'adaptiveResolution',
|
|
25
|
+
'kvStableReachTokens',
|
|
26
|
+
'kvStableQualityGapRatio',
|
|
27
|
+
'compressionSlackRatio',
|
|
28
|
+
'overBudgetGraceRatio',
|
|
29
|
+
'foldingStrategy',
|
|
30
|
+
'speculativeProduction',
|
|
31
|
+
'l1HoldbackChunks',
|
|
32
|
+
'summaryParticipant',
|
|
33
|
+
'summarySystemPrompt',
|
|
34
|
+
'summaryUserPrompt',
|
|
35
|
+
'summaryContextLabel',
|
|
36
|
+
];
|
|
37
|
+
|
|
38
|
+
export function buildFrameworkStrategy(
|
|
39
|
+
recipe: Recipe,
|
|
40
|
+
model: string,
|
|
41
|
+
timeZone: string,
|
|
42
|
+
): ContextStrategy {
|
|
43
|
+
const strategyConfig = recipe.agent.strategy;
|
|
44
|
+
const strategyType = strategyConfig?.type ?? 'autobiographical';
|
|
45
|
+
const autobiographicalOpts: Record<string, unknown> = {
|
|
46
|
+
headWindowTokens: strategyConfig?.headWindowTokens ?? 4000,
|
|
47
|
+
recentWindowTokens: strategyConfig?.recentWindowTokens ?? 30000,
|
|
48
|
+
compressionModel: strategyConfig?.compressionModel ?? model,
|
|
49
|
+
autoTickOnNewMessage: true,
|
|
50
|
+
maxMessageTokens: strategyConfig?.maxMessageTokens ?? 10000,
|
|
51
|
+
...(strategyType === 'frontdesk' ? { timeZone } : {}),
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
for (const key of PASSTHROUGH_KEYS) {
|
|
55
|
+
const value = strategyConfig?.[key];
|
|
56
|
+
if (value !== undefined) autobiographicalOpts[key] = value;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Autobiographical agents default to adaptive resolution unless a recipe
|
|
60
|
+
// opts out; frontdesk keeps its historical hierarchical renderer default.
|
|
61
|
+
if (strategyType === 'autobiographical' && autobiographicalOpts.adaptiveResolution === undefined) {
|
|
62
|
+
autobiographicalOpts.adaptiveResolution = true;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return strategyType === 'passthrough'
|
|
66
|
+
? new PassthroughStrategy()
|
|
67
|
+
: strategyType === 'frontdesk'
|
|
68
|
+
? new FrontdeskStrategy(autobiographicalOpts)
|
|
69
|
+
: new AutobiographicalStrategy(autobiographicalOpts);
|
|
70
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -24,15 +24,14 @@ import {
|
|
|
24
24
|
import { LoggingAnthropicAdapter } from './logging-adapter.js';
|
|
25
25
|
import { CallLedger } from './call-ledger.js';
|
|
26
26
|
import { SettingsModule } from './modules/settings-module.js';
|
|
27
|
-
import { AgentFramework,
|
|
27
|
+
import { AgentFramework, WorkspaceModule, resolveTimeZone, type Module, type MountConfig } from '@animalabs/agent-framework';
|
|
28
28
|
import { resolve, join, basename } from 'node:path';
|
|
29
29
|
import { appendFile, mkdir, stat, rename } from 'node:fs/promises';
|
|
30
30
|
import { readFileSync, existsSync } from 'node:fs';
|
|
31
|
-
import { FrontdeskStrategy } from './strategies/frontdesk-strategy.js';
|
|
32
31
|
import { SubagentModule } from './modules/subagent-module.js';
|
|
33
32
|
import { LessonsModule } from './modules/lessons-module.js';
|
|
34
33
|
import { RetrievalModule } from './modules/retrieval-module.js';
|
|
35
|
-
import type { RecipeWorkspaceMount
|
|
34
|
+
import type { RecipeWorkspaceMount } from './recipe.js';
|
|
36
35
|
import { TuiModule } from './modules/tui-module.js';
|
|
37
36
|
import { TimeModule } from './modules/time-module.js';
|
|
38
37
|
import { FleetModule, type FleetModuleConfig } from './modules/fleet-module.js';
|
|
@@ -56,6 +55,8 @@ import {
|
|
|
56
55
|
parseRecipeArg,
|
|
57
56
|
} from './recipe.js';
|
|
58
57
|
import { createBranchState, resetBranchState, handleExport, type BranchState } from './commands.js';
|
|
58
|
+
import { buildFrameworkAgentConfig } from './framework-agent-config.js';
|
|
59
|
+
import { buildFrameworkStrategy } from './framework-strategy.js';
|
|
59
60
|
|
|
60
61
|
export type { AppContext };
|
|
61
62
|
|
|
@@ -400,102 +401,14 @@ async function createFramework(
|
|
|
400
401
|
// No server augmentation needed — gate is wired via FrameworkConfig.gate
|
|
401
402
|
|
|
402
403
|
// -- Build strategy --
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
// `Record<string, unknown>` cast on `strategyConfig`. Every field we
|
|
406
|
-
// forward is declared on `RecipeStrategy` (see recipe.ts); a typo in a
|
|
407
|
-
// recipe (e.g. `l1BudgetTokes`) now fails at recipe validation rather
|
|
408
|
-
// than silently being a no-op at strategy construction. AutobiographicalStrategy
|
|
409
|
-
// and FrontdeskStrategy share this option bag today; if strategy-specific
|
|
410
|
-
// fields are ever added, this should split into per-strategy types.
|
|
411
|
-
const strategyConfig = recipe.agent.strategy;
|
|
412
|
-
const strategyType = strategyConfig?.type ?? 'autobiographical';
|
|
413
|
-
const autobiographicalOpts: Record<string, unknown> = {
|
|
414
|
-
headWindowTokens: strategyConfig?.headWindowTokens ?? 4000,
|
|
415
|
-
recentWindowTokens: strategyConfig?.recentWindowTokens ?? 30000,
|
|
416
|
-
compressionModel: strategyConfig?.compressionModel ?? model,
|
|
417
|
-
autoTickOnNewMessage: true,
|
|
418
|
-
maxMessageTokens: strategyConfig?.maxMessageTokens ?? 10000,
|
|
419
|
-
...(strategyType === 'frontdesk' ? { timeZone } : {}),
|
|
420
|
-
};
|
|
421
|
-
// Forward optional tuning fields when set. The key list is typed
|
|
422
|
-
// against `RecipeStrategy`, so an unknown field name is a compile
|
|
423
|
-
// error here rather than a silent no-op at runtime.
|
|
424
|
-
const passthroughKeys: ReadonlyArray<keyof RecipeStrategy> = [
|
|
425
|
-
'enforceBudget',
|
|
426
|
-
'maxSpeculativeL1s',
|
|
427
|
-
'positionedRecallPairs',
|
|
428
|
-
'recallHeaderTemplate',
|
|
429
|
-
'targetChunkTokens',
|
|
430
|
-
'mergeThreshold',
|
|
431
|
-
'summaryTargetTokens',
|
|
432
|
-
'l1BudgetTokens',
|
|
433
|
-
'l2BudgetTokens',
|
|
434
|
-
'l3BudgetTokens',
|
|
435
|
-
'toolResultMaxLastN',
|
|
436
|
-
'toolUseInputMaxTokens',
|
|
437
|
-
'adaptiveResolution',
|
|
438
|
-
'kvStableReachTokens',
|
|
439
|
-
'kvStableQualityGapRatio',
|
|
440
|
-
'compressionSlackRatio',
|
|
441
|
-
'overBudgetGraceRatio',
|
|
442
|
-
'foldingStrategy',
|
|
443
|
-
'speculativeProduction',
|
|
444
|
-
'l1HoldbackChunks',
|
|
445
|
-
'summaryParticipant',
|
|
446
|
-
'summarySystemPrompt',
|
|
447
|
-
'summaryUserPrompt',
|
|
448
|
-
'summaryContextLabel',
|
|
449
|
-
];
|
|
450
|
-
for (const key of passthroughKeys) {
|
|
451
|
-
const v = strategyConfig?.[key];
|
|
452
|
-
if (v !== undefined) autobiographicalOpts[key] = v;
|
|
453
|
-
}
|
|
454
|
-
// Adaptive resolution (document-based gradual compression) is the intended
|
|
455
|
-
// default for autobiographical agents. Frontdesk keeps the hierarchical
|
|
456
|
-
// renderer (its salience-biased L1 selection); it can still opt in via the
|
|
457
|
-
// recipe. A recipe may set `adaptiveResolution: false` to opt back out.
|
|
458
|
-
if (strategyType === 'autobiographical' && autobiographicalOpts.adaptiveResolution === undefined) {
|
|
459
|
-
autobiographicalOpts.adaptiveResolution = true;
|
|
460
|
-
}
|
|
461
|
-
const strategy = strategyType === 'passthrough'
|
|
462
|
-
? new PassthroughStrategy()
|
|
463
|
-
: strategyType === 'frontdesk'
|
|
464
|
-
? new FrontdeskStrategy(autobiographicalOpts)
|
|
465
|
-
: new AutobiographicalStrategy(autobiographicalOpts);
|
|
404
|
+
const strategy = buildFrameworkStrategy(recipe, model, timeZone);
|
|
405
|
+
const agentConfig = buildFrameworkAgentConfig(recipe, agentName, model, strategy);
|
|
466
406
|
|
|
467
407
|
// -- Create framework --
|
|
468
408
|
const framework = await AgentFramework.create({
|
|
469
409
|
storePath,
|
|
470
410
|
membrane,
|
|
471
|
-
agents: [
|
|
472
|
-
{
|
|
473
|
-
name: agentName,
|
|
474
|
-
model,
|
|
475
|
-
systemPrompt: recipe.agent.systemPrompt,
|
|
476
|
-
maxTokens: recipe.agent.maxTokens ?? 16384,
|
|
477
|
-
maxStreamTokens: recipe.agent.maxStreamTokens ?? 150000,
|
|
478
|
-
contextBudgetTokens: recipe.agent.contextBudgetTokens,
|
|
479
|
-
...(recipe.agent.cacheTtl && { cacheTtl: recipe.agent.cacheTtl }),
|
|
480
|
-
...(recipe.agent.provider === 'openai-responses' && {
|
|
481
|
-
providerParams: {
|
|
482
|
-
reasoning: {
|
|
483
|
-
effort: recipe.agent.responses?.reasoningEffort ?? 'high',
|
|
484
|
-
context: recipe.agent.responses?.reasoningContext ?? 'all_turns',
|
|
485
|
-
},
|
|
486
|
-
...(recipe.agent.responses?.compactThreshold ? {
|
|
487
|
-
context_management: [{
|
|
488
|
-
type: 'compaction',
|
|
489
|
-
compact_threshold: recipe.agent.responses.compactThreshold,
|
|
490
|
-
}],
|
|
491
|
-
} : {}),
|
|
492
|
-
},
|
|
493
|
-
}),
|
|
494
|
-
strategy,
|
|
495
|
-
...(recipe.agent.thinking && { thinking: recipe.agent.thinking }),
|
|
496
|
-
...(recipe.agent.refusalHandling && { refusalHandling: recipe.agent.refusalHandling }),
|
|
497
|
-
},
|
|
498
|
-
],
|
|
411
|
+
agents: [agentConfig],
|
|
499
412
|
modules: moduleInstances,
|
|
500
413
|
mcplServers: finalServers,
|
|
501
414
|
gate: gateOptions,
|
|
@@ -503,6 +416,45 @@ async function createFramework(
|
|
|
503
416
|
});
|
|
504
417
|
|
|
505
418
|
// Wire post-creation hooks
|
|
419
|
+
// Compression-quarantine klaxon → the framework's ops-alert channel
|
|
420
|
+
// (failures.log + ops:alert trace + CONNECTOME_OPS_WEBHOOK). The strategy
|
|
421
|
+
// re-fires this every alarm interval for as long as ANY chunk is
|
|
422
|
+
// quarantined: quarantined spans stay raw, the fold floor creeps, and the
|
|
423
|
+
// picker eventually cannot fit the window — a guaranteed future outage
|
|
424
|
+
// that must never be a silent state.
|
|
425
|
+
// Duck-typed on both sides so version skew in either dep degrades to a
|
|
426
|
+
// no-op (the strategy's own stderr klaxon still fires) instead of a crash.
|
|
427
|
+
{
|
|
428
|
+
const alarmCapable = strategy as unknown as {
|
|
429
|
+
setQuarantineAlarmHandler?: (fn: (status: { count: number; keys: string[] }) => void) => void;
|
|
430
|
+
};
|
|
431
|
+
const notify = (framework as unknown as {
|
|
432
|
+
notifyOps?: (kind: string, agent: string, message: string, data?: Record<string, unknown>) => void;
|
|
433
|
+
}).notifyOps?.bind(framework);
|
|
434
|
+
if (alarmCapable.setQuarantineAlarmHandler && notify) {
|
|
435
|
+
alarmCapable.setQuarantineAlarmHandler((status) => {
|
|
436
|
+
if (status.count === 0) {
|
|
437
|
+
// All-clear travels the same channel the alarm did, under a
|
|
438
|
+
// DISTINCT kind: the alarm kind's 15-min ops cooldown must never
|
|
439
|
+
// swallow the stand-down (silence after an alarm is ambiguous).
|
|
440
|
+
notify(
|
|
441
|
+
'compression-quarantine-clear',
|
|
442
|
+
agentName,
|
|
443
|
+
'compression quarantine EMPTY — all debt paid; alarm stands down.',
|
|
444
|
+
{ count: 0 },
|
|
445
|
+
);
|
|
446
|
+
return;
|
|
447
|
+
}
|
|
448
|
+
notify(
|
|
449
|
+
'compression-quarantine',
|
|
450
|
+
agentName,
|
|
451
|
+
`${status.count} chunk(s) in compression quarantine — spans stay raw and WILL eventually exhaust the context budget. Operator action required (inspect refusing content; branch, pin, or clear).`,
|
|
452
|
+
{ count: status.count, keys: status.keys },
|
|
453
|
+
);
|
|
454
|
+
});
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
|
|
506
458
|
if (subagentModule) {
|
|
507
459
|
subagentModule.setFramework(framework);
|
|
508
460
|
}
|
|
@@ -905,6 +857,28 @@ async function main() {
|
|
|
905
857
|
},
|
|
906
858
|
};
|
|
907
859
|
|
|
860
|
+
// Off-path refusal dragnet → ops alerts (observability M3): refusals on
|
|
861
|
+
// non-streamed calls (compression/summarizer drains, maintenance) never
|
|
862
|
+
// reach the framework's own noteRefusal — the 2026-07-15 mythos cascade
|
|
863
|
+
// started exactly there, silently. Surface them through the same
|
|
864
|
+
// opsAlert pipeline (failures.log + ops:alert trace + throttled webhook).
|
|
865
|
+
// Reads app.framework (not the closure) so session switches stay wired;
|
|
866
|
+
// feature-detects notifyOpsAlert for older framework versions.
|
|
867
|
+
if (adapter instanceof LoggingAnthropicAdapter) {
|
|
868
|
+
adapter.onRefusal = (info) => {
|
|
869
|
+
const fw = app.framework as unknown as {
|
|
870
|
+
notifyOpsAlert?: (kind: string, agent: string, msg: string, data?: Record<string, unknown>) => void;
|
|
871
|
+
};
|
|
872
|
+
fw.notifyOpsAlert?.(
|
|
873
|
+
'refusal-offpath',
|
|
874
|
+
app.agentName,
|
|
875
|
+
`off-path refusal (category=${info.category ?? 'unknown'}) on a ${info.messages}-message ` +
|
|
876
|
+
`complete() call (~${Math.round(info.inputTokens / 1000)}k tok) — likely compression/summarizer`,
|
|
877
|
+
{ ...info },
|
|
878
|
+
);
|
|
879
|
+
};
|
|
880
|
+
}
|
|
881
|
+
|
|
908
882
|
framework.start();
|
|
909
883
|
setupSynesthete(app);
|
|
910
884
|
setupMcplStderrLog(app, storePath);
|
package/src/logging-adapter.ts
CHANGED
|
@@ -30,8 +30,8 @@ import { appendFileSync } from 'node:fs';
|
|
|
30
30
|
import { summarizeCacheControls, type ProviderCallRecord } from './call-ledger.js';
|
|
31
31
|
|
|
32
32
|
/** Live read of the current reasoning setting. The host wires this to
|
|
33
|
-
* `SettingsModule.getReasoning()` so toggles via the `
|
|
34
|
-
*
|
|
33
|
+
* `SettingsModule.getReasoning()` so toggles via the `agent_settings` tool's
|
|
34
|
+
* reasoning_enabled field take effect on the next call without restart. */
|
|
35
35
|
export type ReasoningGetter = () => { enabled: boolean; budgetTokens: number };
|
|
36
36
|
export type ProviderCallObserver = (record: ProviderCallRecord) => void;
|
|
37
37
|
|
|
@@ -95,7 +95,7 @@ export class LoggingAnthropicAdapter extends AnthropicAdapter {
|
|
|
95
95
|
// form with: `"thinking.type.enabled" is not supported for this model.
|
|
96
96
|
// Use "thinking.type.adaptive"`. Under adaptive the model sizes its own
|
|
97
97
|
// thinking, so `budgetTokens` is no longer sent (it still shows in
|
|
98
|
-
//
|
|
98
|
+
// agent_settings as an informational hint).
|
|
99
99
|
//
|
|
100
100
|
// membrane's `ProviderRequest` doesn't type a top-level `thinking` field
|
|
101
101
|
// (membrane is 0.5.68 — agent-framework is the package that went 0.6.0;
|
|
@@ -131,6 +131,24 @@ export class LoggingAnthropicAdapter extends AnthropicAdapter {
|
|
|
131
131
|
};
|
|
132
132
|
}
|
|
133
133
|
|
|
134
|
+
/**
|
|
135
|
+
* Off-path refusal dragnet (observability M3). The main inference driver
|
|
136
|
+
* instruments its own refusals (agent-framework noteRefusal), but that
|
|
137
|
+
* covers ONLY streamed agent turns. Every other model call in the process
|
|
138
|
+
* — compression/summarizer drains, maintenance — flows through this
|
|
139
|
+
* adapter's complete() and previously refused in silence; the 2026-07-15
|
|
140
|
+
* mythos cascade STARTED there and alerted nowhere. Fired for
|
|
141
|
+
* kind==='complete' only: agent turns stream, so this never double-reports
|
|
142
|
+
* a refusal the main driver already escalated.
|
|
143
|
+
*/
|
|
144
|
+
onRefusal?: (info: {
|
|
145
|
+
kind: 'complete' | 'stream';
|
|
146
|
+
category?: string;
|
|
147
|
+
model: string;
|
|
148
|
+
messages: number;
|
|
149
|
+
inputTokens: number;
|
|
150
|
+
}) => void;
|
|
151
|
+
|
|
134
152
|
private observeCall(
|
|
135
153
|
kind: 'complete' | 'stream',
|
|
136
154
|
timestamp: string,
|
|
@@ -140,6 +158,18 @@ export class LoggingAnthropicAdapter extends AnthropicAdapter {
|
|
|
140
158
|
response?: ProviderResponse,
|
|
141
159
|
error?: unknown,
|
|
142
160
|
): void {
|
|
161
|
+
const raw0 = (response as { raw?: { stop_reason?: string; stop_details?: { category?: string } } } | undefined)?.raw;
|
|
162
|
+
if (kind === 'complete' && raw0?.stop_reason === 'refusal' && this.onRefusal) {
|
|
163
|
+
try {
|
|
164
|
+
this.onRefusal({
|
|
165
|
+
kind,
|
|
166
|
+
category: raw0.stop_details?.category,
|
|
167
|
+
model: request.model,
|
|
168
|
+
messages: request.messages.length,
|
|
169
|
+
inputTokens: response?.usage.inputTokens ?? 0,
|
|
170
|
+
});
|
|
171
|
+
} catch { /* observers never affect provider traffic */ }
|
|
172
|
+
}
|
|
143
173
|
if (!this.onCall) return;
|
|
144
174
|
const cache = summarizeCacheControls(rawRequest);
|
|
145
175
|
const raw = (response as { raw?: Record<string, unknown> } | undefined)?.raw;
|
package/src/mcpl-config.ts
CHANGED
|
@@ -25,6 +25,7 @@ export interface ServerFileEntry {
|
|
|
25
25
|
disabledFeatureSets?: string[];
|
|
26
26
|
enabledTools?: string[];
|
|
27
27
|
disabledTools?: string[];
|
|
28
|
+
/** @deprecated One-time migration input for legacy installations. */
|
|
28
29
|
channelSubscription?: 'auto' | 'manual' | string[];
|
|
29
30
|
}
|
|
30
31
|
|
|
@@ -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,
|