@datename/fastcode-termux 0.1.6 → 0.1.8-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +4 -0
- package/README.md +67 -16
- package/dist/agent/approval-json.d.ts +43 -0
- package/dist/agent/approval-json.js +69 -0
- package/dist/agent/approval-native.d.ts +3 -0
- package/dist/agent/approval-native.js +165 -0
- package/dist/agent/approval-store.d.ts +16 -0
- package/dist/agent/approval-store.js +21 -0
- package/dist/agent/approval.d.ts +83 -0
- package/dist/agent/approval.js +42 -0
- package/dist/agent/compaction.d.ts +41 -0
- package/dist/agent/compaction.js +270 -0
- package/dist/agent/context.d.ts +29 -0
- package/dist/agent/context.js +62 -0
- package/dist/agent/guards.d.ts +24 -16
- package/dist/agent/guards.js +1167 -18
- package/dist/agent/ignore.d.ts +11 -0
- package/dist/agent/ignore.js +129 -0
- package/dist/agent/policy/defaults.d.ts +16 -0
- package/dist/agent/policy/defaults.js +17 -0
- package/dist/agent/policy/egress.d.ts +32 -0
- package/dist/agent/policy/egress.js +280 -0
- package/dist/agent/process-tree.d.ts +22 -0
- package/dist/agent/process-tree.js +102 -0
- package/dist/agent/protocol-mode.d.ts +6 -0
- package/dist/agent/protocol-mode.js +51 -0
- package/dist/agent/protocol.d.ts +20 -1
- package/dist/agent/protocol.js +169 -39
- package/dist/agent/registry.d.ts +19 -0
- package/dist/agent/registry.js +247 -0
- package/dist/agent/runner.d.ts +127 -0
- package/dist/agent/runner.js +391 -0
- package/dist/agent/shell-ast.d.ts +11 -0
- package/dist/agent/shell-ast.js +204 -0
- package/dist/agent/shell-session.d.ts +51 -0
- package/dist/agent/shell-session.js +611 -0
- package/dist/agent/stream.d.ts +29 -0
- package/dist/agent/stream.js +103 -0
- package/dist/agent/tool-schemas.d.ts +2 -0
- package/dist/agent/tool-schemas.js +82 -0
- package/dist/agent/tools/append_file.d.ts +2 -0
- package/dist/agent/tools/append_file.js +28 -0
- package/dist/agent/tools/apply_patch.d.ts +10 -0
- package/dist/agent/tools/apply_patch.js +92 -0
- package/dist/agent/tools/copy_file.d.ts +2 -0
- package/dist/agent/tools/copy_file.js +53 -0
- package/dist/agent/tools/delete_file.d.ts +2 -0
- package/dist/agent/tools/delete_file.js +43 -0
- package/dist/agent/tools/grep.d.ts +2 -0
- package/dist/agent/tools/grep.js +54 -0
- package/dist/agent/tools/index.d.ts +2 -0
- package/dist/agent/tools/index.js +32 -0
- package/dist/agent/tools/list_files.d.ts +2 -0
- package/dist/agent/tools/list_files.js +17 -0
- package/dist/agent/tools/make_dir.d.ts +2 -0
- package/dist/agent/tools/make_dir.js +26 -0
- package/dist/agent/tools/move_path.d.ts +9 -0
- package/dist/agent/tools/move_path.js +73 -0
- package/dist/agent/tools/paths.d.ts +7 -0
- package/dist/agent/tools/paths.js +84 -0
- package/dist/agent/tools/pwd.d.ts +2 -0
- package/dist/agent/tools/pwd.js +10 -0
- package/dist/agent/tools/read_file.d.ts +2 -0
- package/dist/agent/tools/read_file.js +81 -0
- package/dist/agent/tools/read_many.d.ts +2 -0
- package/dist/agent/tools/read_many.js +43 -0
- package/dist/agent/tools/readers.d.ts +58 -0
- package/dist/agent/tools/readers.js +426 -0
- package/dist/agent/tools/replace_in_file.d.ts +2 -0
- package/dist/agent/tools/replace_in_file.js +63 -0
- package/dist/agent/tools/shell.d.ts +2 -0
- package/dist/agent/tools/shell.js +48 -0
- package/dist/agent/tools/stat_path.d.ts +2 -0
- package/dist/agent/tools/stat_path.js +31 -0
- package/dist/agent/tools/types.d.ts +65 -0
- package/dist/agent/tools/types.js +1 -0
- package/dist/agent/tools/validate.d.ts +19 -0
- package/dist/agent/tools/validate.js +92 -0
- package/dist/agent/tools/write_file.d.ts +2 -0
- package/dist/agent/tools/write_file.js +28 -0
- package/dist/agent/tools.d.ts +3 -1
- package/dist/agent/tools.js +2 -377
- package/dist/app.d.ts +2 -1
- package/dist/app.js +846 -173
- package/dist/cli-main.js +363 -0
- package/dist/cli.js +444 -333
- package/dist/commands/about.js +1 -1
- package/dist/commands/attach.d.ts +9 -0
- package/dist/commands/attach.js +15 -0
- package/dist/commands/bg.d.ts +7 -0
- package/dist/commands/bg.js +9 -0
- package/dist/commands/cancel.d.ts +10 -0
- package/dist/commands/cancel.js +10 -0
- package/dist/commands/clear.d.ts +1 -0
- package/dist/commands/clear.js +1 -1
- package/dist/commands/compact.d.ts +22 -0
- package/dist/commands/compact.js +34 -0
- package/dist/commands/context.d.ts +7 -0
- package/dist/commands/context.js +25 -0
- package/dist/commands/edit.d.ts +10 -0
- package/dist/commands/edit.js +36 -0
- package/dist/commands/effort.d.ts +25 -0
- package/dist/commands/effort.js +48 -0
- package/dist/commands/expand.d.ts +10 -0
- package/dist/commands/expand.js +8 -0
- package/dist/commands/index.js +24 -0
- package/dist/commands/jobs.d.ts +7 -0
- package/dist/commands/jobs.js +17 -0
- package/dist/commands/kill.d.ts +7 -0
- package/dist/commands/kill.js +13 -0
- package/dist/commands/login.d.ts +6 -4
- package/dist/commands/login.js +24 -22
- package/dist/commands/logout.js +8 -16
- package/dist/commands/model.d.ts +1 -0
- package/dist/commands/model.js +10 -22
- package/dist/commands/provider.d.ts +1 -2
- package/dist/commands/provider.js +4 -11
- package/dist/commands/skills.js +34 -4
- package/dist/commands/steer.d.ts +11 -0
- package/dist/commands/steer.js +13 -0
- package/dist/commands/threads.d.ts +9 -0
- package/dist/commands/threads.js +26 -0
- package/dist/commands/whoami.js +6 -5
- package/dist/config/store.d.ts +53 -12
- package/dist/config/store.js +163 -73
- package/dist/config/trust.d.ts +30 -8
- package/dist/config/trust.js +139 -14
- package/dist/headless.d.ts +7 -0
- package/dist/headless.js +213 -0
- package/dist/index.js +19 -3
- package/dist/lib/debug.d.ts +2 -0
- package/dist/lib/debug.js +22 -0
- package/dist/lib/diagnostics.d.ts +48 -4
- package/dist/lib/diagnostics.js +90 -8
- package/dist/lib/highlight-subset.d.ts +21 -0
- package/dist/lib/highlight-subset.js +88 -0
- package/dist/lib/http.d.ts +11 -1
- package/dist/lib/http.js +62 -4
- package/dist/lib/lifecycle.d.ts +5 -0
- package/dist/lib/lifecycle.js +62 -0
- package/dist/lib/markdown.js +22 -4
- package/dist/lib/model-meta.d.ts +19 -0
- package/dist/lib/model-meta.js +113 -0
- package/dist/lib/native-tools.d.ts +12 -0
- package/dist/lib/native-tools.js +21 -0
- package/dist/lib/net.d.ts +33 -0
- package/dist/lib/net.js +168 -0
- package/dist/patch/apply.d.ts +15 -0
- package/dist/patch/apply.js +172 -0
- package/dist/patch/parse.d.ts +29 -0
- package/dist/patch/parse.js +112 -0
- package/dist/patch/plan.d.ts +49 -0
- package/dist/patch/plan.js +273 -0
- package/dist/providers/anthropic.d.ts +1 -0
- package/dist/providers/anthropic.js +12 -0
- package/dist/providers/custom.d.ts +9 -0
- package/dist/providers/deepseek.d.ts +10 -0
- package/dist/providers/deepseek.js +3 -2
- package/dist/providers/google.d.ts +9 -0
- package/dist/providers/groq.d.ts +10 -0
- package/dist/providers/groq.js +2 -1
- package/dist/providers/index.d.ts +10 -201
- package/dist/providers/index.js +30 -0
- package/dist/providers/mistral.d.ts +10 -0
- package/dist/providers/mistral.js +2 -1
- package/dist/providers/moonshot.d.ts +10 -0
- package/dist/providers/moonshot.js +2 -1
- package/dist/providers/ollama.d.ts +9 -0
- package/dist/providers/openai.d.ts +10 -0
- package/dist/providers/openai.js +2 -1
- package/dist/providers/openaiCompatible.d.ts +33 -2
- package/dist/providers/openaiCompatible.js +78 -16
- package/dist/providers/openrouter.d.ts +10 -0
- package/dist/providers/openrouter.js +2 -1
- package/dist/providers/qwen.d.ts +10 -0
- package/dist/providers/qwen.js +2 -1
- package/dist/providers/spec.d.ts +12 -0
- package/dist/providers/spec.js +29 -0
- package/dist/providers/xai.d.ts +10 -0
- package/dist/providers/xai.js +2 -1
- package/dist/providers/zhipu.d.ts +10 -0
- package/dist/providers/zhipu.js +2 -1
- package/dist/session/log.d.ts +49 -14
- package/dist/session/log.js +236 -11
- package/dist/session/projection.d.ts +76 -0
- package/dist/session/projection.js +526 -0
- package/dist/skills/manager.d.ts +15 -0
- package/dist/skills/manager.js +135 -18
- package/dist/types.d.ts +75 -1
- package/dist/ui/Message.d.ts +1 -7
- package/dist/ui/Message.js +13 -2
- package/dist/ui/Picker.d.ts +8 -0
- package/dist/ui/Picker.js +24 -0
- package/dist/ui/Prompt.d.ts +1 -2
- package/dist/ui/Prompt.js +2 -2
- package/dist/ui/Spinner.js +11 -5
- package/dist/ui/StatusBar.d.ts +2 -1
- package/dist/ui/StatusBar.js +6 -5
- package/package.json +3 -2
- package/src/agent/approval-json.ts +103 -0
- package/src/agent/approval-native.ts +168 -0
- package/src/agent/approval-store.ts +38 -0
- package/src/agent/approval.ts +94 -0
- package/src/agent/compaction.ts +321 -0
- package/src/agent/context.ts +97 -0
- package/src/agent/guards.ts +1239 -19
- package/src/agent/ignore.ts +147 -0
- package/src/agent/policy/defaults.ts +34 -0
- package/src/agent/policy/egress.ts +335 -0
- package/src/agent/process-tree.ts +114 -0
- package/src/agent/protocol-mode.ts +59 -0
- package/src/agent/protocol.ts +191 -40
- package/src/agent/registry.ts +270 -0
- package/src/agent/runner.ts +486 -0
- package/src/agent/shell-ast.ts +227 -0
- package/src/agent/shell-session.ts +673 -0
- package/src/agent/stream.ts +143 -0
- package/src/agent/tool-schemas.ts +86 -0
- package/src/agent/tools/append_file.ts +30 -0
- package/src/agent/tools/apply_patch.ts +95 -0
- package/src/agent/tools/copy_file.ts +56 -0
- package/src/agent/tools/delete_file.ts +45 -0
- package/src/agent/tools/grep.ts +57 -0
- package/src/agent/tools/index.ts +34 -0
- package/src/agent/tools/list_files.ts +19 -0
- package/src/agent/tools/make_dir.ts +28 -0
- package/src/agent/tools/move_path.ts +83 -0
- package/src/agent/tools/paths.ts +92 -0
- package/src/agent/tools/pwd.ts +12 -0
- package/src/agent/tools/read_file.ts +84 -0
- package/src/agent/tools/read_many.ts +45 -0
- package/src/agent/tools/readers.ts +491 -0
- package/src/agent/tools/replace_in_file.ts +66 -0
- package/src/agent/tools/shell.ts +53 -0
- package/src/agent/tools/stat_path.ts +33 -0
- package/src/agent/tools/types.ts +84 -0
- package/src/agent/tools/validate.ts +104 -0
- package/src/agent/tools/write_file.ts +30 -0
- package/src/agent/tools.ts +3 -377
- package/src/app.ts +853 -170
- package/src/{cli.ts → cli-main.ts} +64 -9
- package/src/commands/about.ts +1 -1
- package/src/commands/attach.ts +16 -0
- package/src/commands/bg.ts +9 -0
- package/src/commands/cancel.ts +10 -0
- package/src/commands/clear.ts +1 -1
- package/src/commands/compact.ts +35 -0
- package/src/commands/context.ts +26 -0
- package/src/commands/edit.ts +37 -0
- package/src/commands/effort.ts +49 -0
- package/src/commands/expand.ts +8 -0
- package/src/commands/index.ts +24 -0
- package/src/commands/jobs.ts +18 -0
- package/src/commands/kill.ts +14 -0
- package/src/commands/login.ts +24 -22
- package/src/commands/logout.ts +8 -16
- package/src/commands/model.ts +10 -22
- package/src/commands/provider.ts +4 -11
- package/src/commands/skills.ts +34 -4
- package/src/commands/steer.ts +13 -0
- package/src/commands/threads.ts +28 -0
- package/src/commands/whoami.ts +6 -5
- package/src/config/store.ts +168 -77
- package/src/config/trust.ts +181 -22
- package/src/headless.ts +225 -0
- package/src/index.ts +19 -3
- package/src/lib/debug.ts +24 -0
- package/src/lib/diagnostics.ts +90 -8
- package/src/lib/highlight-subset.ts +93 -0
- package/src/lib/http.ts +66 -4
- package/src/lib/lifecycle.ts +69 -0
- package/src/lib/markdown.ts +22 -4
- package/src/lib/model-meta.ts +138 -0
- package/src/lib/native-tools.ts +32 -0
- package/src/lib/net.ts +204 -0
- package/src/patch/apply.ts +194 -0
- package/src/patch/parse.ts +131 -0
- package/src/patch/plan.ts +317 -0
- package/src/providers/anthropic.ts +13 -1
- package/src/providers/deepseek.ts +3 -2
- package/src/providers/groq.ts +2 -1
- package/src/providers/index.ts +32 -1
- package/src/providers/mistral.ts +2 -1
- package/src/providers/moonshot.ts +2 -1
- package/src/providers/openai.ts +2 -1
- package/src/providers/openaiCompatible.ts +104 -19
- package/src/providers/openrouter.ts +2 -1
- package/src/providers/qwen.ts +2 -1
- package/src/providers/spec.ts +42 -0
- package/src/providers/xai.ts +2 -1
- package/src/providers/zhipu.ts +2 -1
- package/src/session/log.ts +311 -29
- package/src/session/projection.ts +621 -0
- package/src/skills/manager.ts +140 -18
- package/src/types/highlight-shims.d.ts +14 -0
- package/src/types.ts +79 -2
- package/src/ui/Message.ts +13 -2
- package/src/ui/Picker.ts +24 -0
- package/src/ui/Prompt.ts +2 -2
- package/src/ui/Spinner.ts +11 -5
- package/src/ui/StatusBar.ts +6 -5
- package/tsconfig.json +1 -0
- /package/dist/{cli.d.ts → cli-main.d.ts} +0 -0
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
import { mkdirSync, rmSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { basename, join, resolve, sep } from "node:path";
|
|
3
|
+
import { estMessagesTokens, estTokens } from "../lib/model-meta.js";
|
|
4
|
+
import { getPaths } from "../config/paths.js";
|
|
5
|
+
import type { ChatMessage } from "../types.js";
|
|
6
|
+
import { parseToolCalls, stripToolCalls } from "./protocol.js";
|
|
7
|
+
|
|
8
|
+
export interface CompactionConfig {
|
|
9
|
+
enabled: boolean;
|
|
10
|
+
localThresholdRatio: number;
|
|
11
|
+
keepTurns: number;
|
|
12
|
+
serverCompactionAware: boolean;
|
|
13
|
+
serverCompactThreshold?: number;
|
|
14
|
+
clampAboveServerByTokens: number;
|
|
15
|
+
maxSummaryTokens: number;
|
|
16
|
+
heapLimitMb: number;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export const DEFAULT_COMPACTION_CONFIG: CompactionConfig = {
|
|
20
|
+
enabled: true,
|
|
21
|
+
localThresholdRatio: 0.7,
|
|
22
|
+
keepTurns: 6,
|
|
23
|
+
serverCompactionAware: false,
|
|
24
|
+
clampAboveServerByTokens: 8192,
|
|
25
|
+
maxSummaryTokens: 1500,
|
|
26
|
+
heapLimitMb: 220,
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export interface CompactionOptions {
|
|
30
|
+
contextWindow: number;
|
|
31
|
+
providerId?: string;
|
|
32
|
+
model?: string;
|
|
33
|
+
force?: boolean;
|
|
34
|
+
serverOpaqueState?: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface CompactionResult {
|
|
38
|
+
messages: ChatMessage[];
|
|
39
|
+
compacted: boolean;
|
|
40
|
+
reason?: "threshold" | "heap";
|
|
41
|
+
source: "local" | "server" | "none";
|
|
42
|
+
beforeTokens: number;
|
|
43
|
+
afterTokens: number;
|
|
44
|
+
beforeChars: number;
|
|
45
|
+
afterChars: number;
|
|
46
|
+
droppedMessages: number;
|
|
47
|
+
keptMessages: number;
|
|
48
|
+
summary?: string;
|
|
49
|
+
summaryFile?: string;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function compactionConfigFromEnv(): CompactionConfig {
|
|
53
|
+
const config = { ...DEFAULT_COMPACTION_CONFIG };
|
|
54
|
+
const disabled = (process.env["FASTCODE_COMPACTION"] ?? "").trim().toLowerCase();
|
|
55
|
+
if (["0", "false", "no", "off"].includes(disabled))
|
|
56
|
+
config.enabled = false;
|
|
57
|
+
const heap = Number(process.env["FASTCODE_HEAP_LIMIT_MB"]);
|
|
58
|
+
if (Number.isFinite(heap) && heap > 0)
|
|
59
|
+
config.heapLimitMb = heap;
|
|
60
|
+
const ratio = Number(process.env["FASTCODE_COMPACTION_RATIO"]);
|
|
61
|
+
if (Number.isFinite(ratio) && ratio > 0 && ratio < 1)
|
|
62
|
+
config.localThresholdRatio = ratio;
|
|
63
|
+
const keepTurns = Number(process.env["FASTCODE_COMPACTION_KEEP_TURNS"]);
|
|
64
|
+
if (Number.isFinite(keepTurns) && keepTurns > 0)
|
|
65
|
+
config.keepTurns = Math.floor(keepTurns);
|
|
66
|
+
const summaryTokens = Number(process.env["FASTCODE_COMPACTION_SUMMARY_TOKENS"]);
|
|
67
|
+
if (Number.isFinite(summaryTokens) && summaryTokens > 0)
|
|
68
|
+
config.maxSummaryTokens = Math.floor(summaryTokens);
|
|
69
|
+
const server = (process.env["FASTCODE_SERVER_COMPACTION"] ?? "").trim().toLowerCase();
|
|
70
|
+
if (["1", "true", "yes", "on"].includes(server))
|
|
71
|
+
config.serverCompactionAware = true;
|
|
72
|
+
return config;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function localThresholdTokens(config: CompactionConfig, contextWindow: number): number {
|
|
76
|
+
const local = Math.floor(Math.max(1, contextWindow) * config.localThresholdRatio);
|
|
77
|
+
if (config.serverCompactionAware && typeof config.serverCompactThreshold === "number" && config.serverCompactThreshold > 0) {
|
|
78
|
+
return Math.max(local, config.serverCompactThreshold + config.clampAboveServerByTokens);
|
|
79
|
+
}
|
|
80
|
+
return local;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function heapExceeded(config: CompactionConfig): boolean {
|
|
84
|
+
const used = process.memoryUsage().heapUsed;
|
|
85
|
+
return used > Math.max(0, config.heapLimitMb) * 1024 * 1024;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function shouldCompact(messages: ChatMessage[], config: CompactionConfig, options: CompactionOptions): boolean {
|
|
89
|
+
if (!config.enabled)
|
|
90
|
+
return false;
|
|
91
|
+
if (options.force || heapExceeded(config))
|
|
92
|
+
return true;
|
|
93
|
+
return messageTokens(messages, options) > localThresholdTokens(config, options.contextWindow);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function compactMessages(messages: ChatMessage[], config: CompactionConfig = DEFAULT_COMPACTION_CONFIG, options: CompactionOptions): CompactionResult {
|
|
97
|
+
const beforeTokens = messageTokens(messages, options);
|
|
98
|
+
const beforeChars = messageChars(messages);
|
|
99
|
+
const heap = heapExceeded(config);
|
|
100
|
+
const noop: CompactionResult = {
|
|
101
|
+
messages,
|
|
102
|
+
compacted: false,
|
|
103
|
+
source: "none",
|
|
104
|
+
beforeTokens,
|
|
105
|
+
afterTokens: beforeTokens,
|
|
106
|
+
beforeChars,
|
|
107
|
+
afterChars: beforeChars,
|
|
108
|
+
droppedMessages: 0,
|
|
109
|
+
keptMessages: messages.length,
|
|
110
|
+
};
|
|
111
|
+
if (!config.enabled)
|
|
112
|
+
return noop;
|
|
113
|
+
const threshold = localThresholdTokens(config, options.contextWindow);
|
|
114
|
+
if (!options.force && !heap && beforeTokens <= threshold)
|
|
115
|
+
return noop;
|
|
116
|
+
const reason: "threshold" | "heap" = heap && beforeTokens <= threshold ? "heap" : "threshold";
|
|
117
|
+
if (config.serverCompactionAware && options.serverOpaqueState) {
|
|
118
|
+
return {
|
|
119
|
+
...noop,
|
|
120
|
+
compacted: true,
|
|
121
|
+
reason,
|
|
122
|
+
source: "server",
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
const headCount = messages[0]?.role === "user" ? 1 : 0;
|
|
126
|
+
const keep = Math.max(2, Math.floor(config.keepTurns * 2));
|
|
127
|
+
const tailStart = Math.max(headCount, messages.length - keep);
|
|
128
|
+
if (tailStart <= headCount)
|
|
129
|
+
return noop;
|
|
130
|
+
const middle = messages.slice(headCount, tailStart);
|
|
131
|
+
const summary = buildSummary(middle, config, options);
|
|
132
|
+
const summaryFile = writeSummaryFile(summary);
|
|
133
|
+
const marker: ChatMessage = {
|
|
134
|
+
role: "system",
|
|
135
|
+
content: `[context compacted: ${middle.length} earlier messages were summarized to stay within the context window.` +
|
|
136
|
+
`${summaryFile ? ` Summary file: ${summaryFile}.` : ""} Recent turns are kept verbatim.` +
|
|
137
|
+
" If an earlier detail is missing, re-read that file with a tool.]\n\n" +
|
|
138
|
+
summary,
|
|
139
|
+
};
|
|
140
|
+
const next = [...messages.slice(0, headCount), marker, ...messages.slice(tailStart)];
|
|
141
|
+
return {
|
|
142
|
+
messages: next,
|
|
143
|
+
compacted: true,
|
|
144
|
+
reason,
|
|
145
|
+
source: "local",
|
|
146
|
+
beforeTokens,
|
|
147
|
+
afterTokens: messageTokens(next, options),
|
|
148
|
+
beforeChars,
|
|
149
|
+
afterChars: messageChars(next),
|
|
150
|
+
droppedMessages: middle.length,
|
|
151
|
+
keptMessages: next.length - headCount - 1,
|
|
152
|
+
summary,
|
|
153
|
+
summaryFile,
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function messageTokens(messages: ChatMessage[], options: CompactionOptions): number {
|
|
158
|
+
return estMessagesTokens(messages, { providerId: options.providerId, model: options.model });
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function messageChars(messages: ChatMessage[]): number {
|
|
162
|
+
return messages.reduce((sum, message) => sum + (message.content?.length ?? 0), 0);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export function buildSummary(messages: ChatMessage[], config: CompactionConfig, options: Pick<CompactionOptions, "providerId" | "model">): string {
|
|
166
|
+
const requests: string[] = [];
|
|
167
|
+
const actions: string[] = [];
|
|
168
|
+
const outcomes: string[] = [];
|
|
169
|
+
const notes: string[] = [];
|
|
170
|
+
const files = new Set<string>();
|
|
171
|
+
for (const message of messages) {
|
|
172
|
+
if (!message.content)
|
|
173
|
+
continue;
|
|
174
|
+
if (message.role === "user") {
|
|
175
|
+
if (message.content.startsWith("FastCode executed the requested local tools.")) {
|
|
176
|
+
for (const line of message.content.split(/\r?\n/)) {
|
|
177
|
+
if (/error|failed|not found|denied|timed out/i.test(line))
|
|
178
|
+
outcomes.push(clip(line, 240));
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
else {
|
|
182
|
+
requests.push(clip(message.content, 300));
|
|
183
|
+
}
|
|
184
|
+
collectFiles(message.content, files);
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
if (message.role === "assistant") {
|
|
188
|
+
const calls = parseToolCalls(message.content).calls;
|
|
189
|
+
for (const call of calls)
|
|
190
|
+
actions.push(describeCall(call));
|
|
191
|
+
const visible = stripToolCalls(message.content);
|
|
192
|
+
if (visible)
|
|
193
|
+
notes.push(clip(visible, 300));
|
|
194
|
+
collectFiles(message.content, files);
|
|
195
|
+
continue;
|
|
196
|
+
}
|
|
197
|
+
const lines = message.content.split(/\r?\n/);
|
|
198
|
+
for (const line of lines) {
|
|
199
|
+
if (/error|failed|not found|denied|timed out/i.test(line))
|
|
200
|
+
outcomes.push(clip(line, 240));
|
|
201
|
+
}
|
|
202
|
+
collectFiles(message.content, files);
|
|
203
|
+
}
|
|
204
|
+
const fileList = Array.from(files);
|
|
205
|
+
const sections = [
|
|
206
|
+
"# Session summary",
|
|
207
|
+
"",
|
|
208
|
+
// Conclusions first: when the token budget runs out, keep what the
|
|
209
|
+
// session decided rather than the oldest requests.
|
|
210
|
+
"## Notes",
|
|
211
|
+
...limitLines(notes, 10, "notes").map((line) => `- ${line}`),
|
|
212
|
+
"",
|
|
213
|
+
"## Outcomes",
|
|
214
|
+
...limitLines(outcomes, 10, "outcomes").map((line) => `- ${line}`),
|
|
215
|
+
"",
|
|
216
|
+
"## Files touched",
|
|
217
|
+
...(fileList.length > 0
|
|
218
|
+
? [
|
|
219
|
+
...fileList.slice(0, 40).map((file) => `- ${file}`),
|
|
220
|
+
...(fileList.length > 40 ? [`- (${fileList.length - 40} more files not listed)`] : []),
|
|
221
|
+
]
|
|
222
|
+
: ["- (none recorded)"]),
|
|
223
|
+
"",
|
|
224
|
+
"## Requests",
|
|
225
|
+
...limitLines(requests, 8, "requests").map((line) => `- ${line}`),
|
|
226
|
+
"",
|
|
227
|
+
"## Actions",
|
|
228
|
+
...limitLines(actions, 20, "actions").map((line) => `- ${line}`),
|
|
229
|
+
];
|
|
230
|
+
const budget = Math.max(200, Math.floor(config.maxSummaryTokens));
|
|
231
|
+
const kept: string[] = [];
|
|
232
|
+
let used = 0;
|
|
233
|
+
for (const line of sections) {
|
|
234
|
+
const cost = estTokens(line + "\n", options);
|
|
235
|
+
if (used + cost > budget) {
|
|
236
|
+
kept.push("[... summary truncated ...]");
|
|
237
|
+
break;
|
|
238
|
+
}
|
|
239
|
+
kept.push(line);
|
|
240
|
+
used += cost;
|
|
241
|
+
}
|
|
242
|
+
return kept.join("\n").trimEnd() + "\n";
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function describeCall(call: any): string {
|
|
246
|
+
const tool = String(call?.tool ?? "unknown");
|
|
247
|
+
if (tool === "shell" && typeof call.command === "string")
|
|
248
|
+
return `shell: ${clip(call.command, 200)}`;
|
|
249
|
+
if (typeof call.path === "string")
|
|
250
|
+
return `${tool}: ${call.path}`;
|
|
251
|
+
if (typeof call.from === "string" || typeof call.to === "string")
|
|
252
|
+
return `${tool}: ${call.from ?? "?"} -> ${call.to ?? "?"}`;
|
|
253
|
+
if (typeof call.pattern === "string")
|
|
254
|
+
return `${tool}: /${clip(call.pattern, 120)}/`;
|
|
255
|
+
return tool;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function collectFiles(text: string, files: Set<string>): void {
|
|
259
|
+
const matches = text.match(/[A-Za-z0-9_@./\\-]+\.[A-Za-z0-9]{1,8}\b/g);
|
|
260
|
+
if (!matches)
|
|
261
|
+
return;
|
|
262
|
+
for (const match of matches) {
|
|
263
|
+
if (!looksLikeProjectFile(match))
|
|
264
|
+
continue;
|
|
265
|
+
files.add(match);
|
|
266
|
+
if (files.size >= 200)
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function looksLikeProjectFile(value: string): boolean {
|
|
272
|
+
if (value.length <= 3 || value.length > 240)
|
|
273
|
+
return false;
|
|
274
|
+
if (/^summary-\d/.test(value) || value.includes("//") || value.includes("\\.") || value.startsWith("s/"))
|
|
275
|
+
return false;
|
|
276
|
+
if (/^\d+(\.\d+)+$/.test(value))
|
|
277
|
+
return false;
|
|
278
|
+
return /\.(ts|tsx|js|jsx|mjs|cjs|json|md|txt|py|rs|go|java|kt|kts|c|cc|cpp|h|hpp|sh|bash|zsh|fish|yml|yaml|toml|ini|conf|cfg|env|lock|sql|html|css|scss|sass|less|vue|svelte|rb|php|swift|dart|xml|gradle|properties)$/i.test(value);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function limitLines(lines: string[], max: number, label: string): string[] {
|
|
282
|
+
if (lines.length <= max)
|
|
283
|
+
return lines;
|
|
284
|
+
return [...lines.slice(0, max), `(${lines.length - max} more ${label} not listed)`];
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function clip(text: string, max: number): string {
|
|
288
|
+
const oneLine = String(text).replace(/\s+/g, " ").trim();
|
|
289
|
+
return oneLine.length > max ? oneLine.slice(0, max) + "…" : oneLine;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/** Delete a summary file only when it is one of our summary-*.md artifacts. */
|
|
293
|
+
export function removeSummaryFile(file?: string): void {
|
|
294
|
+
if (!file)
|
|
295
|
+
return;
|
|
296
|
+
const name = basename(file);
|
|
297
|
+
if (!/^summary-\d+.*\.md$/i.test(name))
|
|
298
|
+
return;
|
|
299
|
+
const sessions = resolve(getPaths().sessions) + sep;
|
|
300
|
+
if (!resolve(file).startsWith(sessions))
|
|
301
|
+
return;
|
|
302
|
+
try {
|
|
303
|
+
rmSync(file, { force: true });
|
|
304
|
+
}
|
|
305
|
+
catch {
|
|
306
|
+
// Best effort; an orphan summary is not worth failing compaction.
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function writeSummaryFile(summary: string): string | undefined {
|
|
311
|
+
try {
|
|
312
|
+
const dir = getPaths().sessions;
|
|
313
|
+
mkdirSync(dir, { recursive: true });
|
|
314
|
+
const file = join(dir, `summary-${Date.now()}-${Math.random().toString(36).slice(2, 6)}.md`);
|
|
315
|
+
writeFileSync(file, summary, "utf8");
|
|
316
|
+
return file;
|
|
317
|
+
}
|
|
318
|
+
catch {
|
|
319
|
+
return undefined;
|
|
320
|
+
}
|
|
321
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Context compaction for long agent sessions.
|
|
3
|
+
*
|
|
4
|
+
* The chat loop keeps appending tool results to the conversation, so a long
|
|
5
|
+
* investigation can grow past the model's window and fail with a provider 400.
|
|
6
|
+
* Instead of silently truncating the tail (which loses the current task), we
|
|
7
|
+
* keep the first user request and the most recent turns, and shrink the middle
|
|
8
|
+
* into short summaries with an explicit marker.
|
|
9
|
+
*
|
|
10
|
+
* Token counts are approximated at 4 characters per token. That is deliberately
|
|
11
|
+
* crude: it must work for every provider without a tokenizer dependency.
|
|
12
|
+
*/
|
|
13
|
+
import type { ChatMessage } from "../types.js";
|
|
14
|
+
|
|
15
|
+
export interface CompactOptions {
|
|
16
|
+
/** Character budget for the whole history (default 400k ≈ 100k tokens). */
|
|
17
|
+
maxChars?: number;
|
|
18
|
+
/** Number of trailing messages to keep verbatim. */
|
|
19
|
+
keepRecent?: number;
|
|
20
|
+
/** Maximum characters kept from one old tool result. */
|
|
21
|
+
toolSummaryChars?: number;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface CompactResult {
|
|
25
|
+
messages: ChatMessage[];
|
|
26
|
+
compacted: boolean;
|
|
27
|
+
beforeChars: number;
|
|
28
|
+
afterChars: number;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const TOOL_MARKER = "FastCode executed the requested local tools.";
|
|
32
|
+
|
|
33
|
+
export function historyChars(messages: ChatMessage[]): number {
|
|
34
|
+
return messages.reduce((sum, message) => sum + (message.content?.length ?? 0), 0);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function compactHistory(messages: ChatMessage[], options: CompactOptions = {}): CompactResult {
|
|
38
|
+
const beforeChars = historyChars(messages);
|
|
39
|
+
const maxChars = positive(options.maxChars, 400_000);
|
|
40
|
+
const keepRecent = Math.max(2, Math.floor(positive(options.keepRecent, 6)));
|
|
41
|
+
const toolSummaryChars = Math.max(200, Math.floor(positive(options.toolSummaryChars, 2_000)));
|
|
42
|
+
if (beforeChars <= maxChars || messages.length <= keepRecent + 1) {
|
|
43
|
+
return { messages, compacted: false, beforeChars, afterChars: beforeChars };
|
|
44
|
+
}
|
|
45
|
+
// Keep the original request (usually the first message) plus the newest
|
|
46
|
+
// turns. Everything between is summarized, never dropped without a marker.
|
|
47
|
+
const head = messages[0]?.role === "user" ? [messages[0]] : [];
|
|
48
|
+
const tailStart = Math.max(head.length, messages.length - keepRecent);
|
|
49
|
+
const middle = messages.slice(head.length, tailStart);
|
|
50
|
+
const summarized = middle.map((message) => ({
|
|
51
|
+
role: message.role,
|
|
52
|
+
content: summarizeMessage(message.content, toolSummaryChars),
|
|
53
|
+
}));
|
|
54
|
+
const marker: ChatMessage = {
|
|
55
|
+
role: "system",
|
|
56
|
+
content: `[context compacted: ${middle.length} earlier message(s) were summarized to stay within the context window. ` +
|
|
57
|
+
`Recent tool results are kept in full. If an earlier detail is missing, re-read that file with a tool.]`,
|
|
58
|
+
};
|
|
59
|
+
const next = [...head, marker, ...summarized, ...messages.slice(tailStart)];
|
|
60
|
+
return {
|
|
61
|
+
messages: next,
|
|
62
|
+
compacted: true,
|
|
63
|
+
beforeChars,
|
|
64
|
+
afterChars: historyChars(next),
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function summarizeMessage(content: string, limit: number): string {
|
|
69
|
+
if (!content)
|
|
70
|
+
return content;
|
|
71
|
+
if (content.includes(TOOL_MARKER))
|
|
72
|
+
return summarizeToolMessage(content, limit);
|
|
73
|
+
if (content.length <= limit)
|
|
74
|
+
return content;
|
|
75
|
+
const head = content.slice(0, Math.floor(limit * 0.6));
|
|
76
|
+
const tail = content.slice(-Math.floor(limit * 0.3));
|
|
77
|
+
return `${head}\n[… ${content.length - head.length - tail.length} chars summarized …]\n${tail}`;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Tool results have the shape:
|
|
82
|
+
* FastCode executed ...\n\nTool result 1 (ok)\ncall: {...}\noutput:\n```...```
|
|
83
|
+
* Keep the call and the start/end of each output so file paths and errors
|
|
84
|
+
* survive compaction.
|
|
85
|
+
*/
|
|
86
|
+
function summarizeToolMessage(content: string, limit: number): string {
|
|
87
|
+
if (content.length <= limit)
|
|
88
|
+
return content;
|
|
89
|
+
const keep = Math.max(120, Math.floor((limit - 240) / 2));
|
|
90
|
+
const head = content.slice(0, keep);
|
|
91
|
+
const tail = content.slice(-keep);
|
|
92
|
+
return `${head}\n[… ${content.length - keep * 2} chars of tool output summarized; re-run the tool if you need the middle …]\n${tail}`;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function positive(value: number | undefined, fallback: number): number {
|
|
96
|
+
return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : fallback;
|
|
97
|
+
}
|