@sns-myagent/cli 0.2.0 → 0.3.1
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/CHANGELOG.md +12 -1
- package/README.md +7 -8
- package/bin/.gitkeep +0 -0
- package/bin/snscoder.js +7 -96
- package/dist/cli.js +22026 -0
- package/package.json +4 -3
- package/scripts/apply-pi-natives-patch.js +82 -56
- package/scripts/fetch-binary.mjs +106 -217
- package/scripts/smoke-test.sh +92 -0
- package/src/adapters/telegram/bot.ts +41 -1
- package/src/adapters/telegram/bridge.ts +186 -0
- package/src/adapters/telegram/handler.ts +138 -13
- package/src/adapters/telegram/index.ts +12 -2
- package/src/agents/__tests__/config.test.ts +79 -0
- package/src/agents/__tests__/resilience.test.ts +119 -0
- package/src/agents/__tests__/strategies.test.ts +83 -0
- package/src/agents/config.ts +367 -0
- package/src/agents/ensemble.ts +230 -0
- package/src/agents/resilience.ts +332 -0
- package/src/agents/strategies/best-of-n.ts +108 -0
- package/src/agents/strategies/consensus.ts +104 -0
- package/src/agents/strategies/critic.ts +131 -0
- package/src/agents/strategies/index.ts +12 -0
- package/src/agents/strategies/types.ts +68 -0
- package/src/async/__tests__/task-runner.test.ts +162 -0
- package/src/async/__tests__/task-store.test.ts +146 -0
- package/src/async/index.ts +28 -1
- package/src/async/notifier.ts +133 -0
- package/src/async/task-runner.ts +175 -0
- package/src/async/task-store.ts +170 -0
- package/src/async/types.ts +70 -0
- package/src/cli/entry.ts +3 -1
- package/src/cli/index.ts +74 -55
- package/src/config/index.ts +1 -1
- package/src/debug/index.ts +1 -1
- package/src/internal-urls/docs-index.generated.txt +0 -2
- package/src/modes/components/welcome.ts +13 -6
- package/src/modes/controllers/event-controller.ts +1 -1
- package/src/modes/setup-wizard/scenes/splash.ts +1 -1
- package/src/session/agent-session.ts +1 -1
- package/src/slash-commands/builtin-registry.ts +63 -0
- package/src/slash-commands/helpers/task.ts +181 -0
- package/src/tbm/__tests__/tbm.test.ts +660 -0
- package/src/tbm/comm-modes.ts +165 -0
- package/src/tbm/config.ts +136 -0
- package/src/tbm/context-delta.ts +146 -0
- package/src/tbm/context-pyramid.ts +202 -0
- package/src/tbm/dashboard.ts +131 -0
- package/src/tbm/index.ts +247 -0
- package/src/tbm/lazy-skills.ts +182 -0
- package/src/tbm/response-cache.ts +220 -0
- package/src/tbm/tombstone.ts +189 -0
- package/src/tbm/tool-compress.ts +230 -0
- package/src/tools/ask.ts +1 -1
- package/src/tui/chat-blocks.ts +205 -0
- package/src/tui/chat-ui.ts +270 -0
- package/src/tui/code-cell.ts +90 -1
- package/src/tui/command-palette.ts +189 -0
- package/src/tui/index.ts +4 -0
- package/src/tui/splash.ts +130 -0
- package/src/ui/banner.ts +69 -29
- package/src/ui/colors.ts +24 -0
- package/src/ui/error-display.ts +130 -0
- package/src/ui/gradient.ts +104 -0
- package/src/ui/index.ts +15 -0
- package/src/ui/memory-toast.ts +102 -0
- package/src/ui/status-bar.ts +36 -30
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Communication Modes — controls response verbosity.
|
|
3
|
+
*
|
|
4
|
+
* | Mode | Example | Tokens | Use Case |
|
|
5
|
+
* |------|---------|--------|----------|
|
|
6
|
+
* | Caveman | `Bug auth. Fix: token_exp < not <=.` | ~20 | Debug, quick ops |
|
|
7
|
+
* | Normal | `Found bug in auth middleware. Token expiry check uses wrong operator.` | ~60 | Daily work |
|
|
8
|
+
* | Verbose | Full explanation with context and alternatives... | ~150 | Learning, docs |
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
export type CommMode = "caveman" | "normal" | "verbose";
|
|
12
|
+
|
|
13
|
+
export interface CommModeConfig {
|
|
14
|
+
/** Mode label. */
|
|
15
|
+
name: CommMode;
|
|
16
|
+
/** Approximate target tokens per response. */
|
|
17
|
+
targetTokens: number;
|
|
18
|
+
/** System prompt addition to guide response style. */
|
|
19
|
+
systemDirective: string;
|
|
20
|
+
/** Human-readable description. */
|
|
21
|
+
description: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Mode definitions. */
|
|
25
|
+
export const COMM_MODES: ReadonlyMap<CommMode, CommModeConfig> = new Map([
|
|
26
|
+
[
|
|
27
|
+
"caveman",
|
|
28
|
+
{
|
|
29
|
+
name: "caveman",
|
|
30
|
+
targetTokens: 20,
|
|
31
|
+
description: "Ultra-terse. Fragments only. No filler. Tech terms exact.",
|
|
32
|
+
systemDirective: `Respond in caveman style:
|
|
33
|
+
- Drop articles (a/an/the), filler words, pleasantries
|
|
34
|
+
- Fragments OK. Short synonyms only
|
|
35
|
+
- Pattern: [thing] [action] [reason]
|
|
36
|
+
- Code blocks, paths, commands, errors: keep exact
|
|
37
|
+
- Security warnings: write normal, resume terse after
|
|
38
|
+
- Example: "Bug in auth middleware. Token expiry check uses < not <=. Fix:"`,
|
|
39
|
+
},
|
|
40
|
+
],
|
|
41
|
+
[
|
|
42
|
+
"normal",
|
|
43
|
+
{
|
|
44
|
+
name: "normal",
|
|
45
|
+
targetTokens: 60,
|
|
46
|
+
description: "Clear and concise. Standard professional tone.",
|
|
47
|
+
systemDirective: `Respond clearly and concisely:
|
|
48
|
+
- Complete sentences but no filler
|
|
49
|
+
- Be direct — state the finding/answer first, then brief context if needed
|
|
50
|
+
- Code blocks, paths, errors: keep exact
|
|
51
|
+
- No pleasantries or hedging`,
|
|
52
|
+
},
|
|
53
|
+
],
|
|
54
|
+
[
|
|
55
|
+
"verbose",
|
|
56
|
+
{
|
|
57
|
+
name: "verbose",
|
|
58
|
+
targetTokens: 150,
|
|
59
|
+
description: "Full explanation with context, alternatives, and reasoning.",
|
|
60
|
+
systemDirective: `Respond with full detail:
|
|
61
|
+
- Explain the "why" behind every recommendation
|
|
62
|
+
- Include alternatives and trade-offs
|
|
63
|
+
- Provide examples where helpful
|
|
64
|
+
- Structure with headers/lists for readability
|
|
65
|
+
- Good for learning, documentation, complex decisions`,
|
|
66
|
+
},
|
|
67
|
+
],
|
|
68
|
+
]);
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Auto-detect communication mode from message characteristics.
|
|
72
|
+
*/
|
|
73
|
+
export function autoDetectMode(message: string): CommMode {
|
|
74
|
+
const lower = message.toLowerCase();
|
|
75
|
+
const len = message.length;
|
|
76
|
+
|
|
77
|
+
// Caveman triggers: very short, command-like, debug context
|
|
78
|
+
if (len < 30) return "caveman";
|
|
79
|
+
if (len < 80 && !lower.includes("explain") && !lower.includes("why") && !lower.includes("how")) {
|
|
80
|
+
return "caveman";
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Verbose triggers: learning/doc context
|
|
84
|
+
const verboseKeywords = ["explain", "why", "how does", "teach", "learn", "documentation", "comprehensive", "detailed", "trade-off"];
|
|
85
|
+
if (verboseKeywords.some((kw) => lower.includes(kw))) return "verbose";
|
|
86
|
+
|
|
87
|
+
// Default: normal
|
|
88
|
+
return "normal";
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export class CommunicationModeManager {
|
|
92
|
+
#currentMode: CommMode | "auto";
|
|
93
|
+
#effectiveMode: CommMode;
|
|
94
|
+
#usage: Map<CommMode, number> = new Map([
|
|
95
|
+
["caveman", 0],
|
|
96
|
+
["normal", 0],
|
|
97
|
+
["verbose", 0],
|
|
98
|
+
]);
|
|
99
|
+
|
|
100
|
+
constructor(initialMode: CommMode | "auto" = "auto") {
|
|
101
|
+
this.#currentMode = initialMode;
|
|
102
|
+
this.#effectiveMode = initialMode === "auto" ? "normal" : initialMode;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** Get the current mode setting (may be "auto"). */
|
|
106
|
+
get setting(): CommMode | "auto" {
|
|
107
|
+
return this.#currentMode;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** Get the effective mode (never "auto" — resolved to concrete mode). */
|
|
111
|
+
get effective(): CommMode {
|
|
112
|
+
return this.#effectiveMode;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** Get the system directive for the current effective mode. */
|
|
116
|
+
get directive(): string {
|
|
117
|
+
return COMM_MODES.get(this.#effectiveMode)?.systemDirective ?? "";
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** Get the target token count for the current effective mode. */
|
|
121
|
+
get targetTokens(): number {
|
|
122
|
+
return COMM_MODES.get(this.#effectiveMode)?.targetTokens ?? 60;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Resolve the mode for a new message.
|
|
127
|
+
* If mode is "auto", detects from message content.
|
|
128
|
+
* Returns the directive to inject into system prompt.
|
|
129
|
+
*/
|
|
130
|
+
resolveForMessage(message: string): { mode: CommMode; directive: string } {
|
|
131
|
+
if (this.#currentMode === "auto") {
|
|
132
|
+
this.#effectiveMode = autoDetectMode(message);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
this.#usage.set(this.#effectiveMode, (this.#usage.get(this.#effectiveMode) ?? 0) + 1);
|
|
136
|
+
|
|
137
|
+
return {
|
|
138
|
+
mode: this.#effectiveMode,
|
|
139
|
+
directive: this.directive,
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** Set mode explicitly (from /mode command). */
|
|
144
|
+
setMode(mode: CommMode | "auto"): void {
|
|
145
|
+
this.#currentMode = mode;
|
|
146
|
+
if (mode !== "auto") {
|
|
147
|
+
this.#effectiveMode = mode;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** Get usage stats. */
|
|
152
|
+
get usage(): ReadonlyMap<CommMode, number> {
|
|
153
|
+
return this.#usage;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** Reset state. */
|
|
157
|
+
reset(): void {
|
|
158
|
+
this.#effectiveMode = this.#currentMode === "auto" ? "normal" : this.#currentMode;
|
|
159
|
+
this.#usage = new Map([
|
|
160
|
+
["caveman", 0],
|
|
161
|
+
["normal", 0],
|
|
162
|
+
["verbose", 0],
|
|
163
|
+
]);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TBM Configuration — all Token Budget Manager settings.
|
|
3
|
+
*
|
|
4
|
+
* Loaded from .sns-myagent/config.yaml under the `tbm:` key.
|
|
5
|
+
* Every subsystem reads its own section; the manager checks `tbm.enabled` first.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export interface TbmConfig {
|
|
9
|
+
/** Master switch. All TBM subsystems are inactive when false. */
|
|
10
|
+
enabled: boolean;
|
|
11
|
+
|
|
12
|
+
/** Context Delta Caching — send only changed context each turn. */
|
|
13
|
+
context_delta: {
|
|
14
|
+
enabled: boolean;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
/** Multi-Resolution Context Pyramid — load context in escalating levels. */
|
|
18
|
+
pyramid: {
|
|
19
|
+
enabled: boolean;
|
|
20
|
+
/** Starting pyramid level (0-4). Default 1. */
|
|
21
|
+
start_level: number;
|
|
22
|
+
/** Max level to auto-escalate to. Default 4. */
|
|
23
|
+
max_level: number;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
/** Lazy Skill Loading — inject skill names only, load on-demand. */
|
|
27
|
+
lazy_skills: {
|
|
28
|
+
enabled: boolean;
|
|
29
|
+
/** Max tokens to spend on skill names in prompt. Default 200. */
|
|
30
|
+
name_budget: number;
|
|
31
|
+
/** Max full skills to load per turn. Default 3. */
|
|
32
|
+
max_per_turn: number;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
/** Tool Output Auto-Compress — truncate/summarize tool output. */
|
|
36
|
+
compress: {
|
|
37
|
+
enabled: boolean;
|
|
38
|
+
/** Per-tool token budgets. */
|
|
39
|
+
budgets: {
|
|
40
|
+
terminal: number;
|
|
41
|
+
read_file: number;
|
|
42
|
+
web_extract: number;
|
|
43
|
+
search_files: number;
|
|
44
|
+
default: number;
|
|
45
|
+
};
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
/** Communication Mode — controls response verbosity. */
|
|
49
|
+
comm_mode: "auto" | "caveman" | "normal" | "verbose";
|
|
50
|
+
|
|
51
|
+
/** Conversation Tombstoning — compress old messages. */
|
|
52
|
+
tombstone: {
|
|
53
|
+
enabled: boolean;
|
|
54
|
+
/** Tombstone messages older than N turns. Default 20. */
|
|
55
|
+
after_turns: number;
|
|
56
|
+
/** Keep at least N recent messages untouched. Default 5. */
|
|
57
|
+
keep_recent: number;
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
/** Response Cache — exact and semantic match. */
|
|
61
|
+
response_cache: {
|
|
62
|
+
enabled: boolean;
|
|
63
|
+
/** Cache TTL in seconds. Default 3600. */
|
|
64
|
+
ttl_seconds: number;
|
|
65
|
+
/** Max cached responses. Default 100. */
|
|
66
|
+
max_entries: number;
|
|
67
|
+
/** Semantic similarity threshold (0-1). Default 0.95. */
|
|
68
|
+
similarity_threshold: number;
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Default TBM configuration — everything enabled with sensible defaults. */
|
|
73
|
+
export const DEFAULT_TBM_CONFIG: TbmConfig = {
|
|
74
|
+
enabled: true,
|
|
75
|
+
context_delta: { enabled: true },
|
|
76
|
+
pyramid: {
|
|
77
|
+
enabled: true,
|
|
78
|
+
start_level: 1,
|
|
79
|
+
max_level: 4,
|
|
80
|
+
},
|
|
81
|
+
lazy_skills: {
|
|
82
|
+
enabled: true,
|
|
83
|
+
name_budget: 200,
|
|
84
|
+
max_per_turn: 3,
|
|
85
|
+
},
|
|
86
|
+
compress: {
|
|
87
|
+
enabled: true,
|
|
88
|
+
budgets: {
|
|
89
|
+
terminal: 500,
|
|
90
|
+
read_file: 800,
|
|
91
|
+
web_extract: 1000,
|
|
92
|
+
search_files: 300,
|
|
93
|
+
default: 500,
|
|
94
|
+
},
|
|
95
|
+
},
|
|
96
|
+
comm_mode: "auto",
|
|
97
|
+
tombstone: {
|
|
98
|
+
enabled: true,
|
|
99
|
+
after_turns: 20,
|
|
100
|
+
keep_recent: 5,
|
|
101
|
+
},
|
|
102
|
+
response_cache: {
|
|
103
|
+
enabled: true,
|
|
104
|
+
ttl_seconds: 3600,
|
|
105
|
+
max_entries: 100,
|
|
106
|
+
similarity_threshold: 0.95,
|
|
107
|
+
},
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Merge user config over defaults. Unknown keys are ignored.
|
|
112
|
+
* Deep-merges nested objects so partial overrides work.
|
|
113
|
+
*/
|
|
114
|
+
export function resolveTbmConfig(overrides?: Partial<TbmConfig>): TbmConfig {
|
|
115
|
+
if (!overrides) return { ...DEFAULT_TBM_CONFIG };
|
|
116
|
+
return deepMerge(DEFAULT_TBM_CONFIG as unknown as Record<string, unknown>, overrides as unknown as Record<string, unknown>) as unknown as TbmConfig;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function deepMerge(base: Record<string, unknown>, over: Record<string, unknown>): Record<string, unknown> {
|
|
120
|
+
const result: Record<string, unknown> = { ...base };
|
|
121
|
+
for (const key of Object.keys(over)) {
|
|
122
|
+
if (
|
|
123
|
+
over[key] !== null &&
|
|
124
|
+
typeof over[key] === "object" &&
|
|
125
|
+
!Array.isArray(over[key]) &&
|
|
126
|
+
typeof base[key] === "object" &&
|
|
127
|
+
base[key] !== null &&
|
|
128
|
+
!Array.isArray(base[key])
|
|
129
|
+
) {
|
|
130
|
+
result[key] = deepMerge(base[key] as Record<string, unknown>, over[key] as Record<string, unknown>);
|
|
131
|
+
} else if (over[key] !== undefined) {
|
|
132
|
+
result[key] = over[key];
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
return result;
|
|
136
|
+
}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Context Delta Caching — send only changed context each turn.
|
|
3
|
+
*
|
|
4
|
+
* Static prefix (system prompt, tools, identity) is cached at provider level.
|
|
5
|
+
* Dynamic suffix (recent messages, tool output) is sent as delta.
|
|
6
|
+
*
|
|
7
|
+
* Target: 60-80% input token savings after turn 1.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { createHash } from "node:crypto";
|
|
11
|
+
|
|
12
|
+
export interface ContextDeltaStats {
|
|
13
|
+
/** Total turns processed. */
|
|
14
|
+
turns: number;
|
|
15
|
+
/** Turns where cache hit (static prefix unchanged). */
|
|
16
|
+
cache_hits: number;
|
|
17
|
+
/** Estimated tokens saved by not resending static prefix. */
|
|
18
|
+
tokens_saved: number;
|
|
19
|
+
/** Total input tokens consumed. */
|
|
20
|
+
total_input_tokens: number;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface ContextSegment {
|
|
24
|
+
/** Segment type — static rarely changes, dynamic changes every turn. */
|
|
25
|
+
kind: "static" | "dynamic";
|
|
26
|
+
/** Content hash for change detection. */
|
|
27
|
+
hash: string;
|
|
28
|
+
/** Token count estimate (chars / 4). */
|
|
29
|
+
tokens: number;
|
|
30
|
+
/** The actual content. */
|
|
31
|
+
content: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Hash content for change detection. Uses SHA-256 truncated to 16 hex chars.
|
|
36
|
+
*/
|
|
37
|
+
export function hashContent(content: string): string {
|
|
38
|
+
return createHash("sha256").update(content).digest("hex").slice(0, 16);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Estimate token count from character count. Rule of thumb: ~4 chars per token.
|
|
43
|
+
*/
|
|
44
|
+
export function estimateTokens(text: string): number {
|
|
45
|
+
return Math.ceil(text.length / 4);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Split full context into static and dynamic segments.
|
|
50
|
+
*
|
|
51
|
+
* Static: system prompt + tool definitions + identity.
|
|
52
|
+
* Dynamic: conversation messages + tool output.
|
|
53
|
+
*/
|
|
54
|
+
export function splitContext(fullContext: string): { staticPart: string; dynamicPart: string } {
|
|
55
|
+
// Look for the conversation boundary marker
|
|
56
|
+
const markers = ["<conversation>", "---\n\nUser:", "## Conversation", "Human:", "Assistant:"];
|
|
57
|
+
let splitIdx = -1;
|
|
58
|
+
|
|
59
|
+
for (const marker of markers) {
|
|
60
|
+
const idx = fullContext.indexOf(marker);
|
|
61
|
+
if (idx > 0) {
|
|
62
|
+
splitIdx = idx;
|
|
63
|
+
break;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Fallback: split at 60% mark (static usually larger)
|
|
68
|
+
if (splitIdx < 0) {
|
|
69
|
+
splitIdx = Math.floor(fullContext.length * 0.6);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return {
|
|
73
|
+
staticPart: fullContext.slice(0, splitIdx),
|
|
74
|
+
dynamicPart: fullContext.slice(splitIdx),
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export class ContextDeltaCache {
|
|
79
|
+
#cachedStaticHash: string | null = null;
|
|
80
|
+
#stats: ContextDeltaStats = {
|
|
81
|
+
turns: 0,
|
|
82
|
+
cache_hits: 0,
|
|
83
|
+
tokens_saved: 0,
|
|
84
|
+
total_input_tokens: 0,
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Process a new turn's context. Returns delta info:
|
|
89
|
+
* - `staticChanged`: whether the static prefix needs resending
|
|
90
|
+
* - `deltaContent`: only the dynamic part (if static unchanged)
|
|
91
|
+
* - `fullContent`: full content (if static changed)
|
|
92
|
+
* - `tokensSaved`: estimated tokens saved this turn
|
|
93
|
+
*/
|
|
94
|
+
processTurn(fullContext: string): {
|
|
95
|
+
staticChanged: boolean;
|
|
96
|
+
content: string;
|
|
97
|
+
tokensSaved: number;
|
|
98
|
+
} {
|
|
99
|
+
this.#stats.turns++;
|
|
100
|
+
|
|
101
|
+
const { staticPart, dynamicPart } = splitContext(fullContext);
|
|
102
|
+
const staticHash = hashContent(staticPart);
|
|
103
|
+
const dynamicTokens = estimateTokens(dynamicPart);
|
|
104
|
+
const staticTokens = estimateTokens(staticPart);
|
|
105
|
+
|
|
106
|
+
this.#stats.total_input_tokens += dynamicTokens;
|
|
107
|
+
|
|
108
|
+
if (this.#cachedStaticHash === staticHash) {
|
|
109
|
+
// Cache HIT — static prefix unchanged, send only delta
|
|
110
|
+
this.#stats.cache_hits++;
|
|
111
|
+
this.#stats.tokens_saved += staticTokens;
|
|
112
|
+
return {
|
|
113
|
+
staticChanged: false,
|
|
114
|
+
content: dynamicPart,
|
|
115
|
+
tokensSaved: staticTokens,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Cache MISS — static prefix changed, resend everything
|
|
120
|
+
this.#cachedStaticHash = staticHash;
|
|
121
|
+
this.#stats.total_input_tokens += staticTokens;
|
|
122
|
+
|
|
123
|
+
return {
|
|
124
|
+
staticChanged: true,
|
|
125
|
+
content: fullContext,
|
|
126
|
+
tokensSaved: 0,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** Get current stats snapshot. */
|
|
131
|
+
get stats(): Readonly<ContextDeltaStats> {
|
|
132
|
+
return { ...this.#stats };
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** Cache hit rate (0-1). */
|
|
136
|
+
get hitRate(): number {
|
|
137
|
+
if (this.#stats.turns === 0) return 0;
|
|
138
|
+
return this.#stats.cache_hits / this.#stats.turns;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** Reset cache state (e.g. on session reset). */
|
|
142
|
+
reset(): void {
|
|
143
|
+
this.#cachedStaticHash = null;
|
|
144
|
+
this.#stats = { turns: 0, cache_hits: 0, tokens_saved: 0, total_input_tokens: 0 };
|
|
145
|
+
}
|
|
146
|
+
}
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Multi-Resolution Context Pyramid — load context in escalating levels.
|
|
3
|
+
*
|
|
4
|
+
* Each level adds more context. Auto-escalates when response quality drops.
|
|
5
|
+
*
|
|
6
|
+
* Level 0: Identity only (~100 tokens) — Simple Q&A
|
|
7
|
+
* Level 1: + Last 3 messages (~500 tokens) — Continuation
|
|
8
|
+
* Level 2: + Relevant memories (~1,000 tokens) — Contextual tasks
|
|
9
|
+
* Level 3: + Relevant skills (~2,000 tokens) — Complex tasks
|
|
10
|
+
* Level 4: + Full history (~5,000 tokens) — Deep research
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
export type PyramidLevel = 0 | 1 | 2 | 3 | 4;
|
|
14
|
+
|
|
15
|
+
export interface PyramidLevelConfig {
|
|
16
|
+
level: PyramidLevel;
|
|
17
|
+
/** Human-readable label. */
|
|
18
|
+
label: string;
|
|
19
|
+
/** Approximate token budget for this level. */
|
|
20
|
+
tokenBudget: number;
|
|
21
|
+
/** What content is included at this level. */
|
|
22
|
+
includes: string[];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Pyramid level definitions. */
|
|
26
|
+
export const PYRAMID_LEVELS: ReadonlyMap<PyramidLevel, PyramidLevelConfig> = new Map([
|
|
27
|
+
[0, { level: 0, label: "Identity", tokenBudget: 100, includes: ["identity"] }],
|
|
28
|
+
[1, { level: 1, label: "Continuation", tokenBudget: 500, includes: ["identity", "last_3_messages"] }],
|
|
29
|
+
[2, { level: 2, label: "Contextual", tokenBudget: 1000, includes: ["identity", "last_3_messages", "relevant_memories"] }],
|
|
30
|
+
[3, { level: 3, label: "Complex", tokenBudget: 2000, includes: ["identity", "last_3_messages", "relevant_memories", "relevant_skills"] }],
|
|
31
|
+
[4, { level: 4, label: "Deep Research", tokenBudget: 5000, includes: ["identity", "last_3_messages", "relevant_memories", "relevant_skills", "full_history"] }],
|
|
32
|
+
]);
|
|
33
|
+
|
|
34
|
+
export interface PyramidStats {
|
|
35
|
+
/** Current active level. */
|
|
36
|
+
currentLevel: PyramidLevel;
|
|
37
|
+
/** Total escalations this session. */
|
|
38
|
+
escalations: number;
|
|
39
|
+
/** Total de-escalations this session. */
|
|
40
|
+
deescalations: number;
|
|
41
|
+
/** Tokens consumed at each level. */
|
|
42
|
+
levelTokens: Record<PyramidLevel, number>;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Analyze task complexity to determine initial pyramid level.
|
|
47
|
+
* Returns 0-4 based on heuristics.
|
|
48
|
+
*/
|
|
49
|
+
export function analyzeTaskComplexity(message: string): PyramidLevel {
|
|
50
|
+
const lower = message.toLowerCase();
|
|
51
|
+
const len = message.length;
|
|
52
|
+
|
|
53
|
+
// Simple Q&A triggers
|
|
54
|
+
if (len < 50 && !lower.includes("?") && !lower.includes("how") && !lower.includes("why")) {
|
|
55
|
+
return 0;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Complex task indicators
|
|
59
|
+
const complexKeywords = ["implement", "debug", "refactor", "architecture", "design", "explain in detail", "compare", "analyze"];
|
|
60
|
+
const hasComplexKeyword = complexKeywords.some((kw) => lower.includes(kw));
|
|
61
|
+
|
|
62
|
+
// Research indicators
|
|
63
|
+
const researchKeywords = ["research", "investigate", "comprehensive", "all options", "trade-offs", "deep dive"];
|
|
64
|
+
const hasResearchKeyword = researchKeywords.some((kw) => lower.includes(kw));
|
|
65
|
+
|
|
66
|
+
if (hasResearchKeyword && len > 200) return 4;
|
|
67
|
+
if (hasComplexKeyword && len > 100) return 3;
|
|
68
|
+
if (len > 50 || lower.includes("?")) return 2;
|
|
69
|
+
return 1;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export class ContextPyramid {
|
|
73
|
+
#currentLevel: PyramidLevel;
|
|
74
|
+
#maxLevel: PyramidLevel;
|
|
75
|
+
#stats: PyramidStats;
|
|
76
|
+
#recentQuality: number[] = []; // last N response quality scores (0-1)
|
|
77
|
+
|
|
78
|
+
constructor(startLevel: PyramidLevel = 1, maxLevel: PyramidLevel = 4) {
|
|
79
|
+
this.#currentLevel = startLevel;
|
|
80
|
+
this.#maxLevel = maxLevel;
|
|
81
|
+
this.#stats = {
|
|
82
|
+
currentLevel: startLevel,
|
|
83
|
+
escalations: 0,
|
|
84
|
+
deescalations: 0,
|
|
85
|
+
levelTokens: { 0: 0, 1: 0, 2: 0, 3: 0, 4: 0 },
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** Get the current pyramid level config. */
|
|
90
|
+
get current(): PyramidLevelConfig {
|
|
91
|
+
return PYRAMID_LEVELS.get(this.#currentLevel)!;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Get current level number. */
|
|
95
|
+
get level(): PyramidLevel {
|
|
96
|
+
return this.#currentLevel;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Get stats snapshot. */
|
|
100
|
+
get stats(): Readonly<PyramidStats> {
|
|
101
|
+
return { ...this.#stats };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Determine context level for a new message.
|
|
106
|
+
* Auto-escalates based on task complexity if level was set to auto.
|
|
107
|
+
*/
|
|
108
|
+
resolveLevel(message: string): PyramidLevel {
|
|
109
|
+
const suggested = analyzeTaskComplexity(message);
|
|
110
|
+
return Math.min(suggested, this.#maxLevel) as PyramidLevel;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Set level explicitly (from /mode command or auto-detect).
|
|
115
|
+
*/
|
|
116
|
+
setLevel(level: PyramidLevel): void {
|
|
117
|
+
if (level < 0 || level > 4) return;
|
|
118
|
+
if (level > this.#maxLevel) level = this.#maxLevel;
|
|
119
|
+
|
|
120
|
+
if (level > this.#currentLevel) {
|
|
121
|
+
this.#stats.escalations++;
|
|
122
|
+
} else if (level < this.#currentLevel) {
|
|
123
|
+
this.#stats.deescalations++;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
this.#currentLevel = level;
|
|
127
|
+
this.#stats.currentLevel = level;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Report response quality to inform escalation decisions.
|
|
132
|
+
* Quality: 0 = garbage, 1 = perfect.
|
|
133
|
+
*/
|
|
134
|
+
reportQuality(quality: number): void {
|
|
135
|
+
this.#recentQuality.push(Math.max(0, Math.min(1, quality)));
|
|
136
|
+
if (this.#recentQuality.length > 5) {
|
|
137
|
+
this.#recentQuality.shift();
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// If average quality < 0.5 and not at max, escalate
|
|
141
|
+
const avg = this.#recentQuality.reduce((a, b) => a + b, 0) / this.#recentQuality.length;
|
|
142
|
+
if (avg < 0.5 && this.#currentLevel < this.#maxLevel) {
|
|
143
|
+
this.setLevel((this.#currentLevel + 1) as PyramidLevel);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Record token usage at the current level.
|
|
149
|
+
*/
|
|
150
|
+
recordUsage(tokens: number): void {
|
|
151
|
+
this.#stats.levelTokens[this.#currentLevel] += tokens;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Filter context content based on current pyramid level.
|
|
156
|
+
* Returns only the content appropriate for the level.
|
|
157
|
+
*/
|
|
158
|
+
filterContext(context: {
|
|
159
|
+
identity?: string;
|
|
160
|
+
recentMessages?: string[];
|
|
161
|
+
memories?: string[];
|
|
162
|
+
skills?: string[];
|
|
163
|
+
fullHistory?: string;
|
|
164
|
+
}): string {
|
|
165
|
+
const parts: string[] = [];
|
|
166
|
+
const levelConfig = PYRAMID_LEVELS.get(this.#currentLevel)!;
|
|
167
|
+
|
|
168
|
+
if (levelConfig.includes.includes("identity") && context.identity) {
|
|
169
|
+
parts.push(context.identity);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (levelConfig.includes.includes("last_3_messages") && context.recentMessages) {
|
|
173
|
+
parts.push(context.recentMessages.slice(-3).join("\n"));
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (levelConfig.includes.includes("relevant_memories") && context.memories) {
|
|
177
|
+
parts.push(context.memories.join("\n"));
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (levelConfig.includes.includes("relevant_skills") && context.skills) {
|
|
181
|
+
parts.push(context.skills.join("\n"));
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (levelConfig.includes.includes("full_history") && context.fullHistory) {
|
|
185
|
+
parts.push(context.fullHistory);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
return parts.join("\n\n");
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/** Reset pyramid state. */
|
|
192
|
+
reset(): void {
|
|
193
|
+
this.#currentLevel = 1;
|
|
194
|
+
this.#recentQuality = [];
|
|
195
|
+
this.#stats = {
|
|
196
|
+
currentLevel: 1,
|
|
197
|
+
escalations: 0,
|
|
198
|
+
deescalations: 0,
|
|
199
|
+
levelTokens: { 0: 0, 1: 0, 2: 0, 3: 0, 4: 0 },
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
}
|