@yeshwanthyk/coding-agent 0.3.40 → 0.3.42

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.
@@ -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.40",
3
+ "version": "0.3.42",
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": {