@yeshwanthyk/coding-agent 0.3.40 → 0.3.41
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/examples/auto-compact.ts +158 -0
- package/package.json +2 -1
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Auto-compact hook - triggers compaction when context usage exceeds threshold.
|
|
3
|
+
*
|
|
4
|
+
* Copy to ~/.config/marvin/hooks/auto-compact.ts to enable.
|
|
5
|
+
*
|
|
6
|
+
* Configuration via environment:
|
|
7
|
+
* MARVIN_COMPACT_THRESHOLD - percentage threshold (default: 85)
|
|
8
|
+
* MARVIN_COMPACT_COOLDOWN_MS - min time between compacts (default: 120000)
|
|
9
|
+
* MARVIN_COMPACT_SETTLE_MS - delay before compact call (default: 100)
|
|
10
|
+
* MARVIN_COMPACT_IDLE_TIMEOUT_MS - max wait for idle (default: 30000)
|
|
11
|
+
* MARVIN_COMPACT_IDLE_POLL_MS - idle poll interval (default: 100)
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
interface TokenUsage {
|
|
15
|
+
input: number
|
|
16
|
+
output: number
|
|
17
|
+
cacheRead?: number
|
|
18
|
+
cacheWrite?: number
|
|
19
|
+
total: number
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
interface TurnEndEvent {
|
|
23
|
+
type: "turn.end"
|
|
24
|
+
sessionId: string | null
|
|
25
|
+
tokens: TokenUsage
|
|
26
|
+
contextLimit: number
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
interface AgentEndEvent {
|
|
30
|
+
type: "agent.end"
|
|
31
|
+
sessionId: string | null
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
interface SessionCompactEvent {
|
|
35
|
+
type: "session.compact"
|
|
36
|
+
sessionId: string | null
|
|
37
|
+
summary: string
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
interface SessionEvent {
|
|
41
|
+
type: "session.start" | "session.resume" | "session.clear"
|
|
42
|
+
sessionId: string | null
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
interface HookSessionContext {
|
|
46
|
+
summarize(): Promise<void>
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
interface HookEventContext {
|
|
50
|
+
session: HookSessionContext
|
|
51
|
+
isIdle(): boolean
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
interface HookAPI {
|
|
55
|
+
on(event: "turn.end", handler: (ev: TurnEndEvent, ctx: HookEventContext) => void): void
|
|
56
|
+
on(event: "agent.end", handler: (ev: AgentEndEvent, ctx: HookEventContext) => void): void
|
|
57
|
+
on(event: "session.compact", handler: (ev: SessionCompactEvent, ctx: HookEventContext) => void): void
|
|
58
|
+
on(event: "session.clear", handler: (ev: SessionEvent, ctx: HookEventContext) => void): void
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
interface SessionState {
|
|
62
|
+
pending: boolean
|
|
63
|
+
compacting: boolean
|
|
64
|
+
lastCompact: number
|
|
65
|
+
timer: ReturnType<typeof setTimeout> | null
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const sleep = (ms: number) => new Promise<void>((resolve) => setTimeout(resolve, ms))
|
|
69
|
+
|
|
70
|
+
async function waitUntilIdle(ctx: HookEventContext, timeoutMs: number, pollMs: number): Promise<boolean> {
|
|
71
|
+
const deadline = Date.now() + timeoutMs
|
|
72
|
+
while (Date.now() < deadline) {
|
|
73
|
+
if (ctx.isIdle()) return true
|
|
74
|
+
await sleep(pollMs)
|
|
75
|
+
}
|
|
76
|
+
return ctx.isIdle()
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export default function autoCompact(marvin: HookAPI): void {
|
|
80
|
+
const threshold = Number(process.env.MARVIN_COMPACT_THRESHOLD) || 85
|
|
81
|
+
const cooldownMs = Number(process.env.MARVIN_COMPACT_COOLDOWN_MS) || 120_000
|
|
82
|
+
const settleMs = Number(process.env.MARVIN_COMPACT_SETTLE_MS) || 100
|
|
83
|
+
const idleTimeoutMs = Number(process.env.MARVIN_COMPACT_IDLE_TIMEOUT_MS) || 30_000
|
|
84
|
+
const idlePollMs = Number(process.env.MARVIN_COMPACT_IDLE_POLL_MS) || 100
|
|
85
|
+
|
|
86
|
+
const sessions = new Map<string, SessionState>()
|
|
87
|
+
|
|
88
|
+
const getState = (sessionId: string): SessionState => {
|
|
89
|
+
const state = sessions.get(sessionId)
|
|
90
|
+
if (state) return state
|
|
91
|
+
const created: SessionState = { pending: false, compacting: false, lastCompact: 0, timer: null }
|
|
92
|
+
sessions.set(sessionId, created)
|
|
93
|
+
return created
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const attemptCompact = (sessionId: string, ctx: HookEventContext): void => {
|
|
97
|
+
const state = getState(sessionId)
|
|
98
|
+
if (!state.pending || state.compacting) return
|
|
99
|
+
|
|
100
|
+
const now = Date.now()
|
|
101
|
+
if (now - state.lastCompact < cooldownMs) return
|
|
102
|
+
|
|
103
|
+
state.compacting = true
|
|
104
|
+
void (async () => {
|
|
105
|
+
try {
|
|
106
|
+
const becameIdle = await waitUntilIdle(ctx, idleTimeoutMs, idlePollMs)
|
|
107
|
+
if (!becameIdle) return
|
|
108
|
+
|
|
109
|
+
await sleep(settleMs)
|
|
110
|
+
if (!ctx.isIdle()) return
|
|
111
|
+
|
|
112
|
+
state.pending = false
|
|
113
|
+
await ctx.session.summarize()
|
|
114
|
+
} finally {
|
|
115
|
+
state.compacting = false
|
|
116
|
+
}
|
|
117
|
+
})()
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const scheduleCompact = (sessionId: string, ctx: HookEventContext): void => {
|
|
121
|
+
const state = getState(sessionId)
|
|
122
|
+
if (state.timer !== null) return
|
|
123
|
+
state.timer = setTimeout(() => {
|
|
124
|
+
state.timer = null
|
|
125
|
+
attemptCompact(sessionId, ctx)
|
|
126
|
+
}, 0)
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
marvin.on("turn.end", (event) => {
|
|
130
|
+
if (!event.sessionId) return
|
|
131
|
+
if (!event.tokens?.total || !event.contextLimit) return
|
|
132
|
+
|
|
133
|
+
const percent = (event.tokens.total / event.contextLimit) * 100
|
|
134
|
+
if (percent < threshold) return
|
|
135
|
+
|
|
136
|
+
const state = getState(event.sessionId)
|
|
137
|
+
state.pending = true
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
marvin.on("agent.end", (event, ctx) => {
|
|
141
|
+
if (!event.sessionId) return
|
|
142
|
+
scheduleCompact(event.sessionId, ctx)
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
marvin.on("session.compact", (event) => {
|
|
146
|
+
if (!event.sessionId) return
|
|
147
|
+
const state = getState(event.sessionId)
|
|
148
|
+
state.pending = false
|
|
149
|
+
state.lastCompact = Date.now()
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
marvin.on("session.clear", (event) => {
|
|
153
|
+
if (!event.sessionId) return
|
|
154
|
+
const state = sessions.get(event.sessionId)
|
|
155
|
+
if (state?.timer) clearTimeout(state.timer)
|
|
156
|
+
sessions.delete(event.sessionId)
|
|
157
|
+
})
|
|
158
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yeshwanthyk/coding-agent",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.41",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"bin": {
|
|
6
6
|
"marvin": "./dist/cli.js"
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
"dist",
|
|
19
19
|
"README.md",
|
|
20
20
|
"CHANGELOG.md",
|
|
21
|
+
"examples",
|
|
21
22
|
"package.json"
|
|
22
23
|
],
|
|
23
24
|
"scripts": {
|