openrecall 0.2.2 → 0.4.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/package.json +8 -2
- package/src/agent.ts +97 -11
- package/src/dcp/auth.ts +37 -0
- package/src/dcp/commands/context.ts +265 -0
- package/src/dcp/commands/help.ts +73 -0
- package/src/dcp/commands/manual.ts +131 -0
- package/src/dcp/commands/stats.ts +73 -0
- package/src/dcp/commands/sweep.ts +263 -0
- package/src/dcp/config.ts +981 -0
- package/src/dcp/hooks.ts +224 -0
- package/src/dcp/index.ts +123 -0
- package/src/dcp/logger.ts +211 -0
- package/src/dcp/messages/index.ts +2 -0
- package/src/dcp/messages/inject.ts +316 -0
- package/src/dcp/messages/prune.ts +217 -0
- package/src/dcp/messages/utils.ts +269 -0
- package/src/dcp/prompts/_codegen/compress-nudge.generated.ts +15 -0
- package/src/dcp/prompts/_codegen/compress.generated.ts +56 -0
- package/src/dcp/prompts/_codegen/distill.generated.ts +33 -0
- package/src/dcp/prompts/_codegen/nudge.generated.ts +17 -0
- package/src/dcp/prompts/_codegen/prune.generated.ts +23 -0
- package/src/dcp/prompts/_codegen/system.generated.ts +57 -0
- package/src/dcp/prompts/index.ts +59 -0
- package/src/dcp/protected-file-patterns.ts +113 -0
- package/src/dcp/shared-utils.ts +26 -0
- package/src/dcp/state/index.ts +3 -0
- package/src/dcp/state/persistence.ts +196 -0
- package/src/dcp/state/state.ts +143 -0
- package/src/dcp/state/tool-cache.ts +112 -0
- package/src/dcp/state/types.ts +55 -0
- package/src/dcp/state/utils.ts +55 -0
- package/src/dcp/strategies/deduplication.ts +123 -0
- package/src/dcp/strategies/index.ts +4 -0
- package/src/dcp/strategies/purge-errors.ts +84 -0
- package/src/dcp/strategies/supersede-writes.ts +115 -0
- package/src/dcp/strategies/utils.ts +135 -0
- package/src/dcp/tools/compress.ts +218 -0
- package/src/dcp/tools/distill.ts +60 -0
- package/src/dcp/tools/index.ts +4 -0
- package/src/dcp/tools/prune-shared.ts +174 -0
- package/src/dcp/tools/prune.ts +36 -0
- package/src/dcp/tools/types.ts +11 -0
- package/src/dcp/tools/utils.ts +244 -0
- package/src/dcp/ui/notification.ts +273 -0
- package/src/dcp/ui/utils.ts +133 -0
- package/src/index.ts +101 -49
package/src/dcp/hooks.ts
ADDED
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
import type { SessionState, WithParts } from "./state"
|
|
2
|
+
import type { Logger } from "./logger"
|
|
3
|
+
import type { PluginConfig } from "./config"
|
|
4
|
+
import { syncToolCache } from "./state/tool-cache"
|
|
5
|
+
import { deduplicate, supersedeWrites, purgeErrors } from "./strategies"
|
|
6
|
+
import { prune, insertPruneToolContext } from "./messages"
|
|
7
|
+
import { buildToolIdList, isIgnoredUserMessage } from "./messages/utils"
|
|
8
|
+
import { checkSession } from "./state"
|
|
9
|
+
import { renderSystemPrompt } from "./prompts"
|
|
10
|
+
import { handleStatsCommand } from "./commands/stats"
|
|
11
|
+
import { handleContextCommand } from "./commands/context"
|
|
12
|
+
import { handleHelpCommand } from "./commands/help"
|
|
13
|
+
import { handleSweepCommand } from "./commands/sweep"
|
|
14
|
+
import { handleManualToggleCommand, handleManualTriggerCommand } from "./commands/manual"
|
|
15
|
+
import { ensureSessionInitialized } from "./state/state"
|
|
16
|
+
import { getCurrentParams } from "./strategies/utils"
|
|
17
|
+
|
|
18
|
+
const INTERNAL_AGENT_SIGNATURES = [
|
|
19
|
+
"You are a title generator",
|
|
20
|
+
"You are a helpful AI assistant tasked with summarizing conversations",
|
|
21
|
+
"Summarize what was done in this conversation",
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
function applyPendingManualTriggerPrompt(
|
|
25
|
+
state: SessionState,
|
|
26
|
+
messages: WithParts[],
|
|
27
|
+
logger: Logger,
|
|
28
|
+
): void {
|
|
29
|
+
const pending = state.pendingManualTrigger
|
|
30
|
+
if (!pending) {
|
|
31
|
+
return
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (!state.sessionId || pending.sessionId !== state.sessionId) {
|
|
35
|
+
state.pendingManualTrigger = null
|
|
36
|
+
return
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
40
|
+
const msg = messages[i]!
|
|
41
|
+
if (msg.info.role !== "user" || isIgnoredUserMessage(msg)) {
|
|
42
|
+
continue
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
for (const part of msg.parts) {
|
|
46
|
+
if (part.type !== "text" || part.ignored || part.synthetic) {
|
|
47
|
+
continue
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
part.text = pending.prompt
|
|
51
|
+
state.pendingManualTrigger = null
|
|
52
|
+
logger.debug("Applied pending manual trigger prompt", { sessionId: pending.sessionId })
|
|
53
|
+
return
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
state.pendingManualTrigger = null
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function createSystemPromptHandler(
|
|
61
|
+
state: SessionState,
|
|
62
|
+
logger: Logger,
|
|
63
|
+
config: PluginConfig,
|
|
64
|
+
) {
|
|
65
|
+
return async (
|
|
66
|
+
input: { sessionID?: string; model: { limit: { context: number } } },
|
|
67
|
+
output: { system: string[] },
|
|
68
|
+
) => {
|
|
69
|
+
if (input.model?.limit?.context) {
|
|
70
|
+
state.modelContextLimit = input.model.limit.context
|
|
71
|
+
logger.debug("Cached model context limit", { limit: state.modelContextLimit })
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (state.isSubAgent) {
|
|
75
|
+
return
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const systemText = output.system.join("\n")
|
|
79
|
+
if (INTERNAL_AGENT_SIGNATURES.some((sig) => systemText.includes(sig))) {
|
|
80
|
+
logger.info("Skipping DCP system prompt injection for internal agent")
|
|
81
|
+
return
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const flags = {
|
|
85
|
+
prune: config.tools.prune.permission !== "deny",
|
|
86
|
+
distill: config.tools.distill.permission !== "deny",
|
|
87
|
+
compress: config.tools.compress.permission !== "deny",
|
|
88
|
+
manual: state.manualMode,
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (!flags.prune && !flags.distill && !flags.compress) {
|
|
92
|
+
return
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
output.system.push(renderSystemPrompt(flags))
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function createChatMessageTransformHandler(
|
|
100
|
+
client: any,
|
|
101
|
+
state: SessionState,
|
|
102
|
+
logger: Logger,
|
|
103
|
+
config: PluginConfig,
|
|
104
|
+
) {
|
|
105
|
+
return async (input: {}, output: { messages: WithParts[] }) => {
|
|
106
|
+
await checkSession(client, state, logger, output.messages, config.manualMode.enabled)
|
|
107
|
+
|
|
108
|
+
if (state.isSubAgent) {
|
|
109
|
+
return
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
syncToolCache(state, config, logger, output.messages)
|
|
113
|
+
buildToolIdList(state, output.messages, logger)
|
|
114
|
+
|
|
115
|
+
deduplicate(state, logger, config, output.messages)
|
|
116
|
+
supersedeWrites(state, logger, config, output.messages)
|
|
117
|
+
purgeErrors(state, logger, config, output.messages)
|
|
118
|
+
|
|
119
|
+
prune(state, logger, config, output.messages)
|
|
120
|
+
insertPruneToolContext(state, config, logger, output.messages)
|
|
121
|
+
|
|
122
|
+
applyPendingManualTriggerPrompt(state, output.messages, logger)
|
|
123
|
+
|
|
124
|
+
if (state.sessionId) {
|
|
125
|
+
await logger.saveContext(state.sessionId, output.messages)
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export function createCommandExecuteHandler(
|
|
131
|
+
client: any,
|
|
132
|
+
state: SessionState,
|
|
133
|
+
logger: Logger,
|
|
134
|
+
config: PluginConfig,
|
|
135
|
+
workingDirectory: string,
|
|
136
|
+
) {
|
|
137
|
+
return async (
|
|
138
|
+
input: { command: string; sessionID: string; arguments: string },
|
|
139
|
+
output: { parts: any[] },
|
|
140
|
+
) => {
|
|
141
|
+
if (!config.commands.enabled) {
|
|
142
|
+
return
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (input.command === "dcp") {
|
|
146
|
+
const messagesResponse = await client.session.messages({
|
|
147
|
+
path: { id: input.sessionID },
|
|
148
|
+
})
|
|
149
|
+
const messages = (messagesResponse.data || messagesResponse) as WithParts[]
|
|
150
|
+
|
|
151
|
+
await ensureSessionInitialized(
|
|
152
|
+
client,
|
|
153
|
+
state,
|
|
154
|
+
input.sessionID,
|
|
155
|
+
logger,
|
|
156
|
+
messages,
|
|
157
|
+
config.manualMode.enabled,
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
const args = (input.arguments || "").trim().split(/\s+/).filter(Boolean)
|
|
161
|
+
const subcommand = args[0]?.toLowerCase() || ""
|
|
162
|
+
const subArgs = args.slice(1)
|
|
163
|
+
|
|
164
|
+
const commandCtx = {
|
|
165
|
+
client,
|
|
166
|
+
state,
|
|
167
|
+
config,
|
|
168
|
+
logger,
|
|
169
|
+
sessionId: input.sessionID,
|
|
170
|
+
messages,
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (subcommand === "context") {
|
|
174
|
+
await handleContextCommand(commandCtx)
|
|
175
|
+
throw new Error("__DCP_CONTEXT_HANDLED__")
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (subcommand === "stats") {
|
|
179
|
+
await handleStatsCommand(commandCtx)
|
|
180
|
+
throw new Error("__DCP_STATS_HANDLED__")
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (subcommand === "sweep") {
|
|
184
|
+
await handleSweepCommand({
|
|
185
|
+
...commandCtx,
|
|
186
|
+
args: subArgs,
|
|
187
|
+
workingDirectory,
|
|
188
|
+
})
|
|
189
|
+
throw new Error("__DCP_SWEEP_HANDLED__")
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (subcommand === "manual") {
|
|
193
|
+
await handleManualToggleCommand(commandCtx, subArgs[0]?.toLowerCase())
|
|
194
|
+
throw new Error("__DCP_MANUAL_HANDLED__")
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
if (
|
|
198
|
+
(subcommand === "prune" || subcommand === "distill" || subcommand === "compress") &&
|
|
199
|
+
config.tools[subcommand].permission !== "deny"
|
|
200
|
+
) {
|
|
201
|
+
const userFocus = subArgs.join(" ").trim()
|
|
202
|
+
const prompt = await handleManualTriggerCommand(commandCtx, subcommand, userFocus)
|
|
203
|
+
if (!prompt) {
|
|
204
|
+
throw new Error("__DCP_MANUAL_TRIGGER_BLOCKED__")
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
state.pendingManualTrigger = {
|
|
208
|
+
sessionId: input.sessionID,
|
|
209
|
+
prompt,
|
|
210
|
+
}
|
|
211
|
+
const rawArgs = (input.arguments || "").trim()
|
|
212
|
+
output.parts.length = 0
|
|
213
|
+
output.parts.push({
|
|
214
|
+
type: "text",
|
|
215
|
+
text: rawArgs ? `/dcp ${rawArgs}` : `/dcp ${subcommand}`,
|
|
216
|
+
})
|
|
217
|
+
return
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
await handleHelpCommand(commandCtx)
|
|
221
|
+
throw new Error("__DCP_HELP_HANDLED__")
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
package/src/dcp/index.ts
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import type { PluginInput, Hooks } from "@opencode-ai/plugin"
|
|
2
|
+
import { getConfig } from "./config"
|
|
3
|
+
import { Logger } from "./logger"
|
|
4
|
+
import { createSessionState } from "./state"
|
|
5
|
+
import { createPruneTool, createDistillTool, createCompressTool } from "./strategies"
|
|
6
|
+
import {
|
|
7
|
+
createChatMessageTransformHandler,
|
|
8
|
+
createCommandExecuteHandler,
|
|
9
|
+
createSystemPromptHandler,
|
|
10
|
+
} from "./hooks"
|
|
11
|
+
import { configureClientAuth, isSecureMode } from "./auth"
|
|
12
|
+
|
|
13
|
+
export async function createDcpPlugin(ctx: PluginInput): Promise<Partial<Hooks>> {
|
|
14
|
+
const config = getConfig(ctx)
|
|
15
|
+
|
|
16
|
+
if (!config.enabled) {
|
|
17
|
+
return {}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const logger = new Logger(config.debug)
|
|
21
|
+
const state = createSessionState()
|
|
22
|
+
|
|
23
|
+
if (isSecureMode()) {
|
|
24
|
+
configureClientAuth(ctx.client)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
logger.info("DCP initialized", {
|
|
28
|
+
strategies: config.strategies,
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
return {
|
|
32
|
+
"experimental.chat.system.transform": createSystemPromptHandler(state, logger, config),
|
|
33
|
+
|
|
34
|
+
"experimental.chat.messages.transform": createChatMessageTransformHandler(
|
|
35
|
+
ctx.client,
|
|
36
|
+
state,
|
|
37
|
+
logger,
|
|
38
|
+
config,
|
|
39
|
+
) as any,
|
|
40
|
+
"chat.message": async (
|
|
41
|
+
input: {
|
|
42
|
+
sessionID: string
|
|
43
|
+
agent?: string
|
|
44
|
+
model?: { providerID: string; modelID: string }
|
|
45
|
+
messageID?: string
|
|
46
|
+
variant?: string
|
|
47
|
+
},
|
|
48
|
+
_output: any,
|
|
49
|
+
) => {
|
|
50
|
+
state.variant = input.variant
|
|
51
|
+
logger.debug("Cached variant from chat.message hook", { variant: input.variant })
|
|
52
|
+
},
|
|
53
|
+
"command.execute.before": createCommandExecuteHandler(
|
|
54
|
+
ctx.client,
|
|
55
|
+
state,
|
|
56
|
+
logger,
|
|
57
|
+
config,
|
|
58
|
+
ctx.directory,
|
|
59
|
+
),
|
|
60
|
+
tool: {
|
|
61
|
+
...(config.tools.distill.permission !== "deny" && {
|
|
62
|
+
distill: createDistillTool({
|
|
63
|
+
client: ctx.client,
|
|
64
|
+
state,
|
|
65
|
+
logger,
|
|
66
|
+
config,
|
|
67
|
+
workingDirectory: ctx.directory,
|
|
68
|
+
}),
|
|
69
|
+
}),
|
|
70
|
+
...(config.tools.compress.permission !== "deny" && {
|
|
71
|
+
compress: createCompressTool({
|
|
72
|
+
client: ctx.client,
|
|
73
|
+
state,
|
|
74
|
+
logger,
|
|
75
|
+
config,
|
|
76
|
+
workingDirectory: ctx.directory,
|
|
77
|
+
}),
|
|
78
|
+
}),
|
|
79
|
+
...(config.tools.prune.permission !== "deny" && {
|
|
80
|
+
prune: createPruneTool({
|
|
81
|
+
client: ctx.client,
|
|
82
|
+
state,
|
|
83
|
+
logger,
|
|
84
|
+
config,
|
|
85
|
+
workingDirectory: ctx.directory,
|
|
86
|
+
}),
|
|
87
|
+
}),
|
|
88
|
+
},
|
|
89
|
+
config: async (opencodeConfig: any) => {
|
|
90
|
+
if (config.commands.enabled) {
|
|
91
|
+
opencodeConfig.command ??= {}
|
|
92
|
+
opencodeConfig.command["dcp"] = {
|
|
93
|
+
template: "",
|
|
94
|
+
description: "Show available DCP commands",
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const toolsToAdd: string[] = []
|
|
99
|
+
if (config.tools.distill.permission !== "deny") toolsToAdd.push("distill")
|
|
100
|
+
if (config.tools.compress.permission !== "deny") toolsToAdd.push("compress")
|
|
101
|
+
if (config.tools.prune.permission !== "deny") toolsToAdd.push("prune")
|
|
102
|
+
|
|
103
|
+
if (toolsToAdd.length > 0) {
|
|
104
|
+
const existingPrimaryTools = opencodeConfig.experimental?.primary_tools ?? []
|
|
105
|
+
opencodeConfig.experimental = {
|
|
106
|
+
...opencodeConfig.experimental,
|
|
107
|
+
primary_tools: [...existingPrimaryTools, ...toolsToAdd],
|
|
108
|
+
}
|
|
109
|
+
logger.info(
|
|
110
|
+
`Added ${toolsToAdd.map((t) => `'${t}'`).join(" and ")} to experimental.primary_tools via config mutation`,
|
|
111
|
+
)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const permission = opencodeConfig.permission ?? {}
|
|
115
|
+
opencodeConfig.permission = {
|
|
116
|
+
...permission,
|
|
117
|
+
distill: config.tools.distill.permission,
|
|
118
|
+
compress: config.tools.compress.permission,
|
|
119
|
+
prune: config.tools.prune.permission,
|
|
120
|
+
} as typeof permission
|
|
121
|
+
},
|
|
122
|
+
}
|
|
123
|
+
}
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import { writeFile, mkdir } from "fs/promises"
|
|
2
|
+
import { join } from "path"
|
|
3
|
+
import { existsSync } from "fs"
|
|
4
|
+
import { homedir } from "os"
|
|
5
|
+
|
|
6
|
+
export class Logger {
|
|
7
|
+
private logDir: string
|
|
8
|
+
public enabled: boolean
|
|
9
|
+
|
|
10
|
+
constructor(enabled: boolean) {
|
|
11
|
+
this.enabled = enabled
|
|
12
|
+
const configHome = process.env.XDG_CONFIG_HOME || join(homedir(), ".config")
|
|
13
|
+
this.logDir = join(configHome, "opencode", "logs", "dcp")
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
private async ensureLogDir() {
|
|
17
|
+
if (!existsSync(this.logDir)) {
|
|
18
|
+
await mkdir(this.logDir, { recursive: true })
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
private formatData(data?: any): string {
|
|
23
|
+
if (!data) return ""
|
|
24
|
+
|
|
25
|
+
const parts: string[] = []
|
|
26
|
+
for (const [key, value] of Object.entries(data)) {
|
|
27
|
+
if (value === undefined || value === null) continue
|
|
28
|
+
|
|
29
|
+
// Format arrays compactly
|
|
30
|
+
if (Array.isArray(value)) {
|
|
31
|
+
if (value.length === 0) continue
|
|
32
|
+
parts.push(
|
|
33
|
+
`${key}=[${value.slice(0, 3).join(",")}${value.length > 3 ? `...+${value.length - 3}` : ""}]`,
|
|
34
|
+
)
|
|
35
|
+
} else if (typeof value === "object") {
|
|
36
|
+
const str = JSON.stringify(value)
|
|
37
|
+
if (str.length < 50) {
|
|
38
|
+
parts.push(`${key}=${str}`)
|
|
39
|
+
}
|
|
40
|
+
} else {
|
|
41
|
+
parts.push(`${key}=${value}`)
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return parts.join(" ")
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
private getCallerFile(skipFrames: number = 3): string {
|
|
48
|
+
const originalPrepareStackTrace = Error.prepareStackTrace
|
|
49
|
+
try {
|
|
50
|
+
const err = new Error()
|
|
51
|
+
Error.prepareStackTrace = (_, stack) => stack
|
|
52
|
+
const stack = err.stack as unknown as NodeJS.CallSite[]
|
|
53
|
+
Error.prepareStackTrace = originalPrepareStackTrace
|
|
54
|
+
|
|
55
|
+
// Skip specified number of frames to get to actual caller
|
|
56
|
+
for (let i = skipFrames; i < stack.length; i++) {
|
|
57
|
+
const filename = stack[i]?.getFileName()
|
|
58
|
+
if (filename && !filename.includes("/logger.")) {
|
|
59
|
+
// Extract just the filename without path and extension
|
|
60
|
+
const match = filename.match(/([^/\\]+)\.[tj]s$/)
|
|
61
|
+
return match ? match[1]! : filename
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return "unknown"
|
|
65
|
+
} catch {
|
|
66
|
+
return "unknown"
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
private async write(level: string, component: string, message: string, data?: any) {
|
|
71
|
+
if (!this.enabled) return
|
|
72
|
+
|
|
73
|
+
try {
|
|
74
|
+
await this.ensureLogDir()
|
|
75
|
+
|
|
76
|
+
const timestamp = new Date().toISOString()
|
|
77
|
+
const dataStr = this.formatData(data)
|
|
78
|
+
|
|
79
|
+
const logLine = `${timestamp} ${level.padEnd(5)} ${component}: ${message}${dataStr ? " | " + dataStr : ""}\n`
|
|
80
|
+
|
|
81
|
+
const dailyLogDir = join(this.logDir, "daily")
|
|
82
|
+
if (!existsSync(dailyLogDir)) {
|
|
83
|
+
await mkdir(dailyLogDir, { recursive: true })
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const logFile = join(dailyLogDir, `${new Date().toISOString().split("T")[0]!}.log`)
|
|
87
|
+
await writeFile(logFile, logLine, { flag: "a" })
|
|
88
|
+
} catch (error) {}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
info(message: string, data?: any) {
|
|
92
|
+
const component = this.getCallerFile(2)
|
|
93
|
+
return this.write("INFO", component, message, data)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
debug(message: string, data?: any) {
|
|
97
|
+
const component = this.getCallerFile(2)
|
|
98
|
+
return this.write("DEBUG", component, message, data)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
warn(message: string, data?: any) {
|
|
102
|
+
const component = this.getCallerFile(2)
|
|
103
|
+
return this.write("WARN", component, message, data)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
error(message: string, data?: any) {
|
|
107
|
+
const component = this.getCallerFile(2)
|
|
108
|
+
return this.write("ERROR", component, message, data)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Strips unnecessary metadata from messages for cleaner debug logs.
|
|
113
|
+
*
|
|
114
|
+
* Removed:
|
|
115
|
+
* - All IDs (id, sessionID, messageID, parentID, callID on parts)
|
|
116
|
+
* - summary, path, cost, model, agent, mode, finish, providerID, modelID
|
|
117
|
+
* - step-start and step-finish parts entirely
|
|
118
|
+
* - snapshot fields
|
|
119
|
+
* - ignored text parts
|
|
120
|
+
*
|
|
121
|
+
* Kept:
|
|
122
|
+
* - role, time (created only), tokens (input, output, reasoning, cache)
|
|
123
|
+
* - text, reasoning, tool parts with content
|
|
124
|
+
* - tool calls with: tool, callID, input, output
|
|
125
|
+
*/
|
|
126
|
+
private minimizeForDebug(messages: any[]): any[] {
|
|
127
|
+
return messages.map((msg) => {
|
|
128
|
+
const minimized: any = {
|
|
129
|
+
role: msg.info?.role,
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (msg.info?.time?.created) {
|
|
133
|
+
minimized.time = msg.info.time.created
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (msg.info?.tokens) {
|
|
137
|
+
minimized.tokens = {
|
|
138
|
+
input: msg.info.tokens.input,
|
|
139
|
+
output: msg.info.tokens.output,
|
|
140
|
+
reasoning: msg.info.tokens.reasoning,
|
|
141
|
+
cache: msg.info.tokens.cache,
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (msg.parts) {
|
|
146
|
+
minimized.parts = msg.parts
|
|
147
|
+
.map((part: any) => {
|
|
148
|
+
if (part.type === "step-start" || part.type === "step-finish") {
|
|
149
|
+
return null
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (part.type === "text") {
|
|
153
|
+
if (part.ignored) return null
|
|
154
|
+
return { type: "text", text: part.text }
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (part.type === "reasoning") {
|
|
158
|
+
return {
|
|
159
|
+
type: "reasoning",
|
|
160
|
+
text: part.text,
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (part.type === "tool") {
|
|
165
|
+
const toolPart: any = {
|
|
166
|
+
type: "tool",
|
|
167
|
+
tool: part.tool,
|
|
168
|
+
callID: part.callID,
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (part.state?.status) {
|
|
172
|
+
toolPart.status = part.state.status
|
|
173
|
+
}
|
|
174
|
+
if (part.state?.input) {
|
|
175
|
+
toolPart.input = part.state.input
|
|
176
|
+
}
|
|
177
|
+
if (part.state?.output) {
|
|
178
|
+
toolPart.output = part.state.output
|
|
179
|
+
}
|
|
180
|
+
if (part.state?.error) {
|
|
181
|
+
toolPart.error = part.state.error
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return toolPart
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return null
|
|
188
|
+
})
|
|
189
|
+
.filter(Boolean)
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
return minimized
|
|
193
|
+
})
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
async saveContext(sessionId: string, messages: any[]) {
|
|
197
|
+
if (!this.enabled) return
|
|
198
|
+
|
|
199
|
+
try {
|
|
200
|
+
const contextDir = join(this.logDir, "context", sessionId)
|
|
201
|
+
if (!existsSync(contextDir)) {
|
|
202
|
+
await mkdir(contextDir, { recursive: true })
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const minimized = this.minimizeForDebug(messages)
|
|
206
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, "-")
|
|
207
|
+
const contextFile = join(contextDir, `${timestamp}.json`)
|
|
208
|
+
await writeFile(contextFile, JSON.stringify(minimized, null, 2))
|
|
209
|
+
} catch (error) {}
|
|
210
|
+
}
|
|
211
|
+
}
|