@shawnstack/quickforge 1.4.1 → 1.5.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/README.md +12 -12
- package/bin/quickforge.mjs +9 -0
- package/dist/assets/AgentProfilesPage-BIwd5Nzg.js +1 -0
- package/dist/assets/ChatPanelHost-De-DMjx5.js +242 -0
- package/dist/assets/PluginsPage-kRzB5k8J.js +1 -0
- package/dist/assets/ScheduledTasksPage-ZnjohaPS.js +2 -0
- package/dist/assets/SharedConversationPage-EQdZgWCM.js +1 -0
- package/dist/assets/TerminalDock-P2pJH_tx.js +2 -0
- package/dist/assets/WorkspaceInspector-CkLAqYQ6.js +3 -0
- package/dist/assets/WorkspaceReaderDialog-BwzZ8Tgv.js +1 -0
- package/dist/assets/diff-line-counts-CeZC7b0z.js +10 -0
- package/dist/assets/icons-DJqt-rnw.js +1 -0
- package/dist/assets/index-CcGy4TXo.js +1354 -0
- package/dist/assets/index-DuTUuAMk.css +3 -0
- package/dist/assets/{monaco-evITXh-m.js → monaco-CNEfYIy1.js} +1 -1
- package/dist/assets/{react-vendor-Mthyt1p4.js → react-vendor-CZCcjpSR.js} +1 -1
- package/dist/favicon.svg +16 -1
- package/dist/index.html +5 -5
- package/dist/manifest.webmanifest +30 -30
- package/package.json +3 -2
- package/server/acp/server.mjs +921 -0
- package/server/agent-manager.mjs +200 -34
- package/server/agent-profile-files.mjs +179 -0
- package/server/agent-profiles.mjs +59 -5
- package/server/auto-compaction.mjs +82 -39
- package/server/channels/process-channel.mjs +278 -0
- package/server/channels/providers/wechat.mjs +271 -0
- package/server/channels/registry.mjs +58 -0
- package/server/custom-commands.mjs +13 -1
- package/server/frontmatter.mjs +167 -0
- package/server/index.mjs +52 -3
- package/server/project-config.mjs +43 -6
- package/server/routes/agent-profiles.mjs +6 -2
- package/server/routes/agent.mjs +12 -1
- package/server/routes/channels.mjs +145 -0
- package/server/routes/models.mjs +68 -0
- package/server/routes/project.mjs +2 -2
- package/server/routes/scheduled-tasks.mjs +6 -5
- package/server/routes/storage.mjs +4 -2
- package/server/routes/system.mjs +27 -0
- package/server/routes/tools.mjs +17 -6
- package/server/routes/workspace.mjs +142 -20
- package/server/session-utils.mjs +10 -2
- package/server/storage.mjs +29 -2
- package/server/system-prompt.mjs +1 -0
- package/server/tools/definitions.mjs +18 -0
- package/server/tools/index.mjs +86 -0
- package/server/utils/package-update.mjs +156 -0
- package/server/utils/workspace.mjs +1 -1
- package/dist/assets/AgentProfilesPage-CNK5PxA3.js +0 -1
- package/dist/assets/ChatPanelHost-FqPQwwMO.js +0 -217
- package/dist/assets/PluginsPage-BCu1Ept0.js +0 -1
- package/dist/assets/ScheduledTasksPage-Bx04rjui.js +0 -2
- package/dist/assets/SharedConversationPage-55vX9sqe.js +0 -1
- package/dist/assets/TerminalDock-DLN_pLkJ.js +0 -2
- package/dist/assets/WorkspaceInspector-DoemHHnY.js +0 -3
- package/dist/assets/WorkspaceReaderDialog-C6xUHBCw.js +0 -6
- package/dist/assets/icons-BWtivFsx.js +0 -1
- package/dist/assets/index-CxOHP41X.css +0 -3
- package/dist/assets/index-Dcf73EL8.js +0 -895
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import { existsSync, promises as fs } from 'node:fs'
|
|
2
|
+
import os from 'node:os'
|
|
3
|
+
import path from 'node:path'
|
|
4
|
+
import { dataDir } from './storage.mjs'
|
|
5
|
+
import { firstOptionalBoolean, firstString, parseFrontmatter, splitDelimitedList } from './frontmatter.mjs'
|
|
6
|
+
|
|
7
|
+
const DEFAULT_MAX_RUNTIME_MS = 30 * 60 * 1000
|
|
8
|
+
const DEFAULT_MAX_TOOL_CALLS = 300
|
|
9
|
+
const nameRegex = /^[a-z][a-z0-9_-]{1,39}$/
|
|
10
|
+
const allowedToolNames = new Set(['read_file', 'grep_files', 'write_file', 'edit_file', 'run_command'])
|
|
11
|
+
|
|
12
|
+
const toolAliases = new Map([
|
|
13
|
+
['Read', 'read_file'],
|
|
14
|
+
['Grep', 'grep_files'],
|
|
15
|
+
['Bash', 'run_command'],
|
|
16
|
+
['Write', 'write_file'],
|
|
17
|
+
['Edit', 'edit_file'],
|
|
18
|
+
])
|
|
19
|
+
|
|
20
|
+
const claudeUserAgentsDir = path.join(os.homedir(), '.claude', 'agents')
|
|
21
|
+
const userAgentsDir = path.join(dataDir, 'agents')
|
|
22
|
+
|
|
23
|
+
function normalizeString(value) {
|
|
24
|
+
return typeof value === 'string' && value.trim() ? value.trim() : undefined
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function normalizeName(value) {
|
|
28
|
+
const name = normalizeString(value)?.toLowerCase()
|
|
29
|
+
return name && nameRegex.test(name) ? name : null
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function normalizeRuntime(value) {
|
|
33
|
+
if (value === undefined || value === null || value === '') return DEFAULT_MAX_RUNTIME_MS
|
|
34
|
+
const parsed = Number(value)
|
|
35
|
+
if (!Number.isFinite(parsed) || parsed <= 0) return DEFAULT_MAX_RUNTIME_MS
|
|
36
|
+
return Math.min(Math.max(Math.round(parsed), 1000), DEFAULT_MAX_RUNTIME_MS)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function normalizeToolCalls(value) {
|
|
40
|
+
if (value === undefined || value === null || value === '') return DEFAULT_MAX_TOOL_CALLS
|
|
41
|
+
const parsed = Number(value)
|
|
42
|
+
if (!Number.isInteger(parsed) || parsed <= 0) return DEFAULT_MAX_TOOL_CALLS
|
|
43
|
+
return Math.min(parsed, DEFAULT_MAX_TOOL_CALLS)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function normalizeTools(value) {
|
|
47
|
+
const tools = []
|
|
48
|
+
const seen = new Set()
|
|
49
|
+
for (const item of splitDelimitedList(value)) {
|
|
50
|
+
const mapped = toolAliases.get(item) || item
|
|
51
|
+
if (!allowedToolNames.has(mapped) || seen.has(mapped)) continue
|
|
52
|
+
seen.add(mapped)
|
|
53
|
+
tools.push(mapped)
|
|
54
|
+
}
|
|
55
|
+
return tools.length ? tools : ['read_file', 'grep_files']
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function hasMutationTool(allowedTools) {
|
|
59
|
+
return allowedTools.some((toolName) => toolName === 'write_file' || toolName === 'edit_file')
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function agentProfileFromMarkdown(file, text, options = {}) {
|
|
63
|
+
const parsed = parseFrontmatter(text)
|
|
64
|
+
if (!parsed.body) return null
|
|
65
|
+
|
|
66
|
+
const metadata = parsed.metadata || {}
|
|
67
|
+
const name = normalizeName(metadata.name) || normalizeName(path.basename(file, '.md'))
|
|
68
|
+
if (!name) return null
|
|
69
|
+
if (options.reservedNames?.has(name)) return null
|
|
70
|
+
|
|
71
|
+
const allowedTools = normalizeTools(
|
|
72
|
+
metadata.tools ?? metadata['allowed-tools'] ?? metadata.allowedTools,
|
|
73
|
+
)
|
|
74
|
+
const label = firstString(metadata.label, metadata.displayName, metadata.title) || name
|
|
75
|
+
const enabledAsSubagent = firstOptionalBoolean(
|
|
76
|
+
metadata['enabled-as-subagent'],
|
|
77
|
+
metadata.enabled_as_subagent,
|
|
78
|
+
metadata.enabledAsSubagent,
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
return {
|
|
82
|
+
id: `${options.idPrefix || 'file'}:${name}`,
|
|
83
|
+
name,
|
|
84
|
+
label: label.slice(0, 80),
|
|
85
|
+
description: String(firstString(metadata.description) || '').slice(0, 500),
|
|
86
|
+
systemPrompt: parsed.body,
|
|
87
|
+
allowedTools,
|
|
88
|
+
maxRuntimeMs: normalizeRuntime(metadata['max-runtime-ms'] ?? metadata.max_runtime_ms ?? metadata.maxRuntimeMs),
|
|
89
|
+
maxToolCalls: normalizeToolCalls(metadata['max-tool-calls'] ?? metadata.max_tool_calls ?? metadata.maxToolCalls),
|
|
90
|
+
enabledAsSubagent: enabledAsSubagent === undefined ? true : enabledAsSubagent,
|
|
91
|
+
builtin: false,
|
|
92
|
+
source: options.source || 'file',
|
|
93
|
+
readonly: true,
|
|
94
|
+
filePath: file,
|
|
95
|
+
relativePath: options.relativePath || path.basename(file),
|
|
96
|
+
allowFileMutations: hasMutationTool(allowedTools),
|
|
97
|
+
createdAt: 'file',
|
|
98
|
+
updatedAt: 'file',
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async function listAgentFilesFromDirectory(dir, options = {}) {
|
|
103
|
+
if (!dir || !existsSync(dir)) return []
|
|
104
|
+
let entries
|
|
105
|
+
try {
|
|
106
|
+
entries = await fs.readdir(dir, { withFileTypes: true })
|
|
107
|
+
} catch (error) {
|
|
108
|
+
if (error?.code === 'ENOENT' || error?.code === 'ENOTDIR' || error?.code === 'EACCES' || error?.code === 'EPERM') return []
|
|
109
|
+
throw error
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const profiles = []
|
|
113
|
+
for (const entry of entries) {
|
|
114
|
+
if (!entry.isFile() || !entry.name.toLowerCase().endsWith('.md')) continue
|
|
115
|
+
const file = path.join(dir, entry.name)
|
|
116
|
+
try {
|
|
117
|
+
const relativePath = options.relativeRoot
|
|
118
|
+
? `${options.relativeRoot}/${entry.name}`.replace(/\\/g, '/')
|
|
119
|
+
: entry.name
|
|
120
|
+
const profile = agentProfileFromMarkdown(file, await fs.readFile(file, 'utf8'), {
|
|
121
|
+
...options,
|
|
122
|
+
relativePath,
|
|
123
|
+
})
|
|
124
|
+
if (profile) profiles.push(profile)
|
|
125
|
+
} catch (error) {
|
|
126
|
+
console.warn(`Failed to load agent profile ${file}:`, error.message || error)
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return profiles
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function projectClaudeAgentsDir(workspaceRoot) {
|
|
133
|
+
return workspaceRoot ? path.join(path.resolve(workspaceRoot), '.claude', 'agents') : ''
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function projectQuickForgeAgentsDir(workspaceRoot) {
|
|
137
|
+
return workspaceRoot ? path.join(path.resolve(workspaceRoot), '.quickforge', 'agents') : ''
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export async function loadUserAgentProfiles(options = {}) {
|
|
141
|
+
const byName = new Map()
|
|
142
|
+
const sources = [
|
|
143
|
+
{ dir: claudeUserAgentsDir, source: 'user-claude', relativeRoot: '~/.claude/agents', idPrefix: 'user-claude' },
|
|
144
|
+
{ dir: userAgentsDir, source: 'user', relativeRoot: '~/.quickforge/agents', idPrefix: 'user' },
|
|
145
|
+
]
|
|
146
|
+
for (const source of sources) {
|
|
147
|
+
for (const profile of await listAgentFilesFromDirectory(source.dir, { ...options, ...source })) {
|
|
148
|
+
byName.set(profile.name, profile)
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
return [...byName.values()]
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export async function loadProjectAgentProfiles(workspaceRoot, options = {}) {
|
|
155
|
+
if (!workspaceRoot) return []
|
|
156
|
+
const byName = new Map()
|
|
157
|
+
const sources = [
|
|
158
|
+
{ dir: projectClaudeAgentsDir(workspaceRoot), source: 'project-claude', relativeRoot: '.claude/agents', idPrefix: 'project-claude' },
|
|
159
|
+
{ dir: projectQuickForgeAgentsDir(workspaceRoot), source: 'project', relativeRoot: '.quickforge/agents', idPrefix: 'project' },
|
|
160
|
+
]
|
|
161
|
+
for (const source of sources) {
|
|
162
|
+
for (const profile of await listAgentFilesFromDirectory(source.dir, { ...options, ...source })) {
|
|
163
|
+
byName.set(profile.name, profile)
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
return [...byName.values()]
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export async function loadFileAgentProfiles(workspaceRoot, options = {}) {
|
|
170
|
+
const byName = new Map()
|
|
171
|
+
for (const profile of await loadUserAgentProfiles(options)) byName.set(profile.name, profile)
|
|
172
|
+
for (const profile of await loadProjectAgentProfiles(workspaceRoot, options)) byName.set(profile.name, profile)
|
|
173
|
+
return [...byName.values()].sort((a, b) => a.name.localeCompare(b.name))
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export const agentProfileSearchPaths = {
|
|
177
|
+
global: ['~/.claude/agents', '~/.quickforge/agents'],
|
|
178
|
+
project: ['<project>/.claude/agents', '<project>/.quickforge/agents'],
|
|
179
|
+
}
|
|
@@ -2,6 +2,8 @@ import { randomUUID } from 'node:crypto'
|
|
|
2
2
|
import { readStore, atomicUpdate } from './storage.mjs'
|
|
3
3
|
import { subagentDefinitions } from './subagents.mjs'
|
|
4
4
|
import { workspaceTools } from './tools/definitions.mjs'
|
|
5
|
+
import { defaultGlobalWorkspaceContext, projectContextFromId } from './project-config.mjs'
|
|
6
|
+
import { loadFileAgentProfiles } from './agent-profile-files.mjs'
|
|
5
7
|
|
|
6
8
|
const STORE = 'custom-agents'
|
|
7
9
|
const RESERVED_NAMES = new Set(subagentDefinitions.map((definition) => definition.name))
|
|
@@ -56,6 +58,9 @@ function builtinProfileFromSubagent(definition) {
|
|
|
56
58
|
maxToolCalls: definition.maxToolCalls || DEFAULT_MAX_TOOL_CALLS,
|
|
57
59
|
enabledAsSubagent: true,
|
|
58
60
|
builtin: true,
|
|
61
|
+
source: 'builtin',
|
|
62
|
+
readonly: true,
|
|
63
|
+
allowFileMutations: definition.allowFileMutations === true,
|
|
59
64
|
createdAt: 'builtin',
|
|
60
65
|
updatedAt: 'builtin',
|
|
61
66
|
}
|
|
@@ -92,6 +97,9 @@ function normalizeProfileInput(input, existing = null, { creating = false } = {}
|
|
|
92
97
|
maxToolCalls: normalizeOptionalPositiveInteger(input?.maxToolCalls ?? existing?.maxToolCalls, DEFAULT_MAX_TOOL_CALLS, 300),
|
|
93
98
|
enabledAsSubagent: input?.enabledAsSubagent === undefined ? Boolean(existing?.enabledAsSubagent ?? true) : input.enabledAsSubagent === true,
|
|
94
99
|
builtin: false,
|
|
100
|
+
source: 'store',
|
|
101
|
+
readonly: false,
|
|
102
|
+
allowFileMutations: allowedTools.some((toolName) => toolName === 'write_file' || toolName === 'edit_file'),
|
|
95
103
|
createdAt: existing?.createdAt || now,
|
|
96
104
|
updatedAt: now,
|
|
97
105
|
}
|
|
@@ -102,20 +110,61 @@ async function readCustomAgentMap() {
|
|
|
102
110
|
return data && typeof data === 'object' ? data : {}
|
|
103
111
|
}
|
|
104
112
|
|
|
113
|
+
async function resolveWorkspaceRoot(options = {}) {
|
|
114
|
+
if (options.workspaceRoot) return options.workspaceRoot
|
|
115
|
+
if (options.projectId) {
|
|
116
|
+
try {
|
|
117
|
+
return (await projectContextFromId(options.projectId))?.workspaceRoot || null
|
|
118
|
+
} catch {
|
|
119
|
+
return null
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return defaultGlobalWorkspaceContext()?.workspaceRoot || null
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function mergeProfiles({ builtin = [], file = [], custom = [] }) {
|
|
126
|
+
const reservedNames = new Set(builtin.map((profile) => profile.name))
|
|
127
|
+
const byName = new Map()
|
|
128
|
+
|
|
129
|
+
for (const profile of builtin) {
|
|
130
|
+
byName.set(profile.name, profile)
|
|
131
|
+
}
|
|
132
|
+
for (const profile of file) {
|
|
133
|
+
if (!profile?.name || reservedNames.has(profile.name)) continue
|
|
134
|
+
byName.set(profile.name, profile)
|
|
135
|
+
}
|
|
136
|
+
for (const profile of custom) {
|
|
137
|
+
if (!profile?.id) continue
|
|
138
|
+
if (!reservedNames.has(profile.name) && !byName.has(profile.name)) byName.set(profile.name, profile)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return [...byName.values()].sort((a, b) => {
|
|
142
|
+
if (a.builtin && !b.builtin) return -1
|
|
143
|
+
if (!a.builtin && b.builtin) return 1
|
|
144
|
+
return a.name.localeCompare(b.name)
|
|
145
|
+
})
|
|
146
|
+
}
|
|
147
|
+
|
|
105
148
|
export async function listAgentProfiles(options = {}) {
|
|
106
149
|
const custom = Object.values(await readCustomAgentMap())
|
|
107
|
-
const
|
|
150
|
+
const workspaceRoot = await resolveWorkspaceRoot(options)
|
|
151
|
+
const file = await loadFileAgentProfiles(workspaceRoot, { reservedNames: RESERVED_NAMES })
|
|
152
|
+
const profiles = mergeProfiles({ builtin: listBuiltinAgentProfiles(), file, custom })
|
|
108
153
|
return options.includeDisabled ? profiles : profiles.filter((profile) => profile.enabledAsSubagent || profile.builtin || profile.enabledAsSubagent === false)
|
|
109
154
|
}
|
|
110
155
|
|
|
111
|
-
export async function listSubagentProfiles() {
|
|
112
|
-
return (await listAgentProfiles({ includeDisabled: true })).filter((profile) => profile.enabledAsSubagent)
|
|
156
|
+
export async function listSubagentProfiles(options = {}) {
|
|
157
|
+
return (await listAgentProfiles({ ...options, includeDisabled: true })).filter((profile) => profile.enabledAsSubagent)
|
|
113
158
|
}
|
|
114
159
|
|
|
115
|
-
export async function getAgentProfile(idOrName) {
|
|
160
|
+
export async function getAgentProfile(idOrName, options = {}) {
|
|
116
161
|
const key = String(idOrName || '').trim().toLowerCase()
|
|
117
162
|
if (!key) return null
|
|
118
|
-
|
|
163
|
+
const profiles = await listAgentProfiles({ ...options, includeDisabled: true })
|
|
164
|
+
const byName = profiles.find((profile) => profile.name === key)
|
|
165
|
+
if (byName) return byName
|
|
166
|
+
const custom = Object.values(await readCustomAgentMap())
|
|
167
|
+
return custom.find((profile) => profile?.id === key) || profiles.find((profile) => profile.id === key) || null
|
|
119
168
|
}
|
|
120
169
|
|
|
121
170
|
export async function createCustomAgentProfile(input) {
|
|
@@ -167,7 +216,12 @@ export function agentProfileSnapshot(profile) {
|
|
|
167
216
|
allowedTools: [...profile.allowedTools],
|
|
168
217
|
maxRuntimeMs: profile.maxRuntimeMs,
|
|
169
218
|
maxToolCalls: profile.maxToolCalls,
|
|
219
|
+
enabledAsSubagent: profile.enabledAsSubagent === true,
|
|
170
220
|
builtin: profile.builtin === true,
|
|
221
|
+
source: profile.source || (profile.builtin ? 'builtin' : 'store'),
|
|
222
|
+
readonly: profile.readonly === true || profile.builtin === true,
|
|
223
|
+
filePath: profile.filePath,
|
|
224
|
+
relativePath: profile.relativePath,
|
|
171
225
|
}
|
|
172
226
|
}
|
|
173
227
|
|
|
@@ -151,6 +151,76 @@ export function buildAutoCompactLoopMessages(session, messages) {
|
|
|
151
151
|
return [summaryMessage, ...source.slice(compactedUpToIndex)]
|
|
152
152
|
}
|
|
153
153
|
|
|
154
|
+
export async function compactSessionInPlace({
|
|
155
|
+
session,
|
|
156
|
+
messages,
|
|
157
|
+
keepRecentTurns = DEFAULT_AUTO_COMPACT_SETTINGS.keepRecentTurns,
|
|
158
|
+
minSourceChars = DEFAULT_AUTO_COMPACT_SETTINGS.minSourceChars,
|
|
159
|
+
usage,
|
|
160
|
+
thresholdPercent,
|
|
161
|
+
emitSessionEvent,
|
|
162
|
+
persistSession,
|
|
163
|
+
reason = 'manual_compact',
|
|
164
|
+
summaryIntro = 'The previous conversation has been compacted. Treat the following summary as the authoritative replacement for earlier history. If information is missing, ask for clarification instead of guessing.',
|
|
165
|
+
onBeforePersist,
|
|
166
|
+
}) {
|
|
167
|
+
const source = Array.isArray(messages) ? messages : []
|
|
168
|
+
const normalizedKeepRecentTurns = clampNumber(keepRecentTurns, DEFAULT_AUTO_COMPACT_SETTINGS.keepRecentTurns, 1, 20)
|
|
169
|
+
const normalizedMinSourceChars = clampNumber(minSourceChars, DEFAULT_AUTO_COMPACT_SETTINGS.minSourceChars, 0, 200000)
|
|
170
|
+
const tailStart = tailStartForRecentTurns(source, normalizedKeepRecentTurns)
|
|
171
|
+
const sourceMessages = buildCompactionSourceMessages(session, source, tailStart)
|
|
172
|
+
if (sourceMessages.length < 2 || estimateMessagesChars(sourceMessages) < normalizedMinSourceChars) {
|
|
173
|
+
return { compacted: false, usage, reason: 'not_enough_history' }
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const result = await compactConversation({
|
|
177
|
+
messages: sourceMessages,
|
|
178
|
+
model: session.model,
|
|
179
|
+
thinkingLevel: session.thinkingLevel,
|
|
180
|
+
getApiKey: session.getApiKey,
|
|
181
|
+
keepTurns: 0,
|
|
182
|
+
})
|
|
183
|
+
|
|
184
|
+
if (result.skipped) return { compacted: false, usage, reason: result.reason || 'skipped' }
|
|
185
|
+
|
|
186
|
+
await saveCompactBackup(session.sessionId, sourceMessages)
|
|
187
|
+
const summaryMessage = userTextMessage([
|
|
188
|
+
summaryIntro,
|
|
189
|
+
'',
|
|
190
|
+
'<compact_summary>',
|
|
191
|
+
result.summary,
|
|
192
|
+
'</compact_summary>',
|
|
193
|
+
].join('\n'))
|
|
194
|
+
session.contextCompaction = {
|
|
195
|
+
summaryMessage,
|
|
196
|
+
compactedUpToIndex: tailStart,
|
|
197
|
+
compactedAt: new Date().toISOString(),
|
|
198
|
+
keepRecentTurns: normalizedKeepRecentTurns,
|
|
199
|
+
sourceMessageCount: source.length,
|
|
200
|
+
usageBefore: usage,
|
|
201
|
+
thresholdPercent,
|
|
202
|
+
}
|
|
203
|
+
onBeforePersist?.({ result, sourceMessages, tailStart, summaryMessage })
|
|
204
|
+
await persistSession?.(session)
|
|
205
|
+
const contextUsage = estimateSessionContextUsage(session, source)
|
|
206
|
+
emitSessionEvent?.(session, {
|
|
207
|
+
type: 'auto_compact_completed',
|
|
208
|
+
reason,
|
|
209
|
+
usage,
|
|
210
|
+
thresholdPercent,
|
|
211
|
+
contextCompaction: session.contextCompaction,
|
|
212
|
+
contextUsage,
|
|
213
|
+
})
|
|
214
|
+
emitSessionEvent?.(session, {
|
|
215
|
+
type: 'messages_replaced',
|
|
216
|
+
reason,
|
|
217
|
+
messages: source,
|
|
218
|
+
contextCompaction: session.contextCompaction,
|
|
219
|
+
contextUsage,
|
|
220
|
+
})
|
|
221
|
+
return { compacted: true, usage, result, sourceMessages, tailStart }
|
|
222
|
+
}
|
|
223
|
+
|
|
154
224
|
export function estimateSessionContextUsage(session, messages = session?.agent?.state?.messages ?? []) {
|
|
155
225
|
if (!session?.agent?.state) return null
|
|
156
226
|
const sourceMessages = Array.isArray(messages) ? messages : []
|
|
@@ -274,50 +344,23 @@ export async function maybeAutoCompactSession({ session, messages, signal, emitS
|
|
|
274
344
|
|
|
275
345
|
session.autoCompacting = true
|
|
276
346
|
try {
|
|
277
|
-
const result = await
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
thinkingLevel: session.thinkingLevel,
|
|
281
|
-
getApiKey: session.getApiKey,
|
|
282
|
-
keepTurns: 0,
|
|
283
|
-
})
|
|
284
|
-
|
|
285
|
-
if (result.skipped) return { compacted: false, usage, reason: result.reason || 'skipped' }
|
|
286
|
-
|
|
287
|
-
await saveCompactBackup(session.sessionId, sourceMessages)
|
|
288
|
-
const summaryMessage = userTextMessage([
|
|
289
|
-
'The previous conversation has been automatically compacted. Treat the following summary as the authoritative replacement for earlier history. If information is missing, ask for clarification instead of guessing.',
|
|
290
|
-
'',
|
|
291
|
-
'<compact_summary>',
|
|
292
|
-
result.summary,
|
|
293
|
-
'</compact_summary>',
|
|
294
|
-
].join('\n'))
|
|
295
|
-
session.contextCompaction = {
|
|
296
|
-
summaryMessage,
|
|
297
|
-
compactedUpToIndex: tailStart,
|
|
298
|
-
compactedAt: new Date().toISOString(),
|
|
347
|
+
const result = await compactSessionInPlace({
|
|
348
|
+
session,
|
|
349
|
+
messages,
|
|
299
350
|
keepRecentTurns: settings.keepRecentTurns,
|
|
300
|
-
|
|
301
|
-
usageBefore: usage,
|
|
302
|
-
thresholdPercent: settings.thresholdPercent,
|
|
303
|
-
}
|
|
304
|
-
clearAutoCompactRejected(session)
|
|
305
|
-
session.lastAutoCompactAt = now
|
|
306
|
-
await persistSession(session)
|
|
307
|
-
emitSessionEvent(session, {
|
|
308
|
-
type: 'auto_compact_completed',
|
|
351
|
+
minSourceChars: settings.minSourceChars,
|
|
309
352
|
usage,
|
|
310
353
|
thresholdPercent: settings.thresholdPercent,
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
})
|
|
314
|
-
emitSessionEvent(session, {
|
|
315
|
-
type: 'messages_replaced',
|
|
354
|
+
emitSessionEvent,
|
|
355
|
+
persistSession,
|
|
316
356
|
reason: 'auto_compact',
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
357
|
+
summaryIntro: 'The previous conversation has been automatically compacted. Treat the following summary as the authoritative replacement for earlier history. If information is missing, ask for clarification instead of guessing.',
|
|
358
|
+
onBeforePersist: () => {
|
|
359
|
+
clearAutoCompactRejected(session)
|
|
360
|
+
session.lastAutoCompactAt = now
|
|
361
|
+
},
|
|
320
362
|
})
|
|
363
|
+
if (!result.compacted) return result
|
|
321
364
|
return { compacted: true, usage }
|
|
322
365
|
} catch (error) {
|
|
323
366
|
logger?.warn?.(`Auto compact failed for session ${session.sessionId}:`, error?.message || error, { sessionId: session.sessionId })
|