@townco/agent 0.1.82 → 0.1.84
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/dist/acp-server/adapter.js +150 -49
- package/dist/acp-server/http.js +56 -1
- package/dist/acp-server/session-storage.d.ts +44 -12
- package/dist/acp-server/session-storage.js +153 -59
- package/dist/definition/index.d.ts +2 -2
- package/dist/definition/index.js +1 -1
- package/dist/runner/agent-runner.d.ts +4 -2
- package/dist/runner/hooks/executor.d.ts +1 -0
- package/dist/runner/hooks/executor.js +18 -2
- package/dist/runner/hooks/predefined/compaction-tool.js +3 -2
- package/dist/runner/hooks/predefined/tool-response-compactor.d.ts +0 -4
- package/dist/runner/hooks/predefined/tool-response-compactor.js +30 -16
- package/dist/runner/hooks/types.d.ts +4 -5
- package/dist/runner/langchain/index.d.ts +1 -0
- package/dist/runner/langchain/index.js +156 -33
- package/dist/runner/langchain/tools/artifacts.d.ts +68 -0
- package/dist/runner/langchain/tools/artifacts.js +466 -0
- package/dist/runner/langchain/tools/browser.js +15 -3
- package/dist/runner/langchain/tools/filesystem.d.ts +8 -4
- package/dist/runner/langchain/tools/filesystem.js +118 -82
- package/dist/runner/langchain/tools/generate_image.d.ts +19 -0
- package/dist/runner/langchain/tools/generate_image.js +54 -14
- package/dist/runner/langchain/tools/subagent.js +2 -2
- package/dist/runner/langchain/tools/todo.js +3 -0
- package/dist/runner/langchain/tools/web_search.js +6 -0
- package/dist/runner/session-context.d.ts +40 -0
- package/dist/runner/session-context.js +69 -0
- package/dist/runner/tools.d.ts +2 -2
- package/dist/runner/tools.js +2 -0
- package/dist/scaffold/project-scaffold.js +7 -3
- package/dist/telemetry/setup.js +1 -1
- package/dist/templates/index.d.ts +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/utils/context-size-calculator.d.ts +1 -10
- package/dist/utils/context-size-calculator.js +1 -12
- package/dist/utils/token-counter.js +2 -2
- package/package.json +10 -10
- package/templates/index.ts +1 -1
|
@@ -78,6 +78,16 @@ const toolCallBlockSchema = z.object({
|
|
|
78
78
|
subagentPort: z.number().optional(),
|
|
79
79
|
subagentSessionId: z.string().optional(),
|
|
80
80
|
subagentMessages: z.array(subagentMessageSchema).optional(),
|
|
81
|
+
_meta: z
|
|
82
|
+
.object({
|
|
83
|
+
truncationWarning: z.string().optional(),
|
|
84
|
+
compactionAction: z.enum(["compacted", "truncated"]).optional(),
|
|
85
|
+
originalTokens: z.number().optional(),
|
|
86
|
+
finalTokens: z.number().optional(),
|
|
87
|
+
originalContentPreview: z.string().optional(),
|
|
88
|
+
originalContentPath: z.string().optional(),
|
|
89
|
+
})
|
|
90
|
+
.optional(),
|
|
81
91
|
});
|
|
82
92
|
const contentBlockSchema = z.discriminatedUnion("type", [
|
|
83
93
|
textBlockSchema,
|
|
@@ -114,7 +124,6 @@ const contextEntrySchema = z.object({
|
|
|
114
124
|
toolInputTokens: z.number(),
|
|
115
125
|
toolResultsTokens: z.number(),
|
|
116
126
|
totalEstimated: z.number(),
|
|
117
|
-
llmReportedInputTokens: z.number().optional(),
|
|
118
127
|
modelContextWindow: z.number().optional(),
|
|
119
128
|
}),
|
|
120
129
|
});
|
|
@@ -131,7 +140,8 @@ const storedSessionSchema = z.object({
|
|
|
131
140
|
});
|
|
132
141
|
/**
|
|
133
142
|
* File-based session storage
|
|
134
|
-
* Stores sessions in agents/<agent-name>/.sessions/<session_id
|
|
143
|
+
* Stores sessions in agents/<agent-name>/.sessions/<session_id>/session.json
|
|
144
|
+
* (Legacy: agents/<agent-name>/.sessions/<session_id>.json)
|
|
135
145
|
*/
|
|
136
146
|
export class SessionStorage {
|
|
137
147
|
sessionsDir;
|
|
@@ -146,17 +156,25 @@ export class SessionStorage {
|
|
|
146
156
|
this.agentName = agentName;
|
|
147
157
|
}
|
|
148
158
|
/**
|
|
149
|
-
* Ensure the
|
|
159
|
+
* Ensure the session directory exists
|
|
150
160
|
*/
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
161
|
+
ensureSessionDir(sessionId) {
|
|
162
|
+
const sessionDir = join(this.sessionsDir, sessionId);
|
|
163
|
+
if (!existsSync(sessionDir)) {
|
|
164
|
+
mkdirSync(sessionDir, { recursive: true });
|
|
154
165
|
}
|
|
155
166
|
}
|
|
156
167
|
/**
|
|
157
168
|
* Get the file path for a session
|
|
158
169
|
*/
|
|
159
170
|
getSessionPath(sessionId) {
|
|
171
|
+
return join(this.sessionsDir, sessionId, "session.json");
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Get the legacy file path for a session (for backwards compatibility)
|
|
175
|
+
* Legacy format: .sessions/<session_id>.json (flat file, not directory)
|
|
176
|
+
*/
|
|
177
|
+
getLegacySessionPath(sessionId) {
|
|
160
178
|
return join(this.sessionsDir, `${sessionId}.json`);
|
|
161
179
|
}
|
|
162
180
|
/**
|
|
@@ -164,7 +182,7 @@ export class SessionStorage {
|
|
|
164
182
|
* Uses atomic write (write to temp file, then rename)
|
|
165
183
|
*/
|
|
166
184
|
async saveSession(sessionId, messages, context) {
|
|
167
|
-
this.
|
|
185
|
+
this.ensureSessionDir(sessionId);
|
|
168
186
|
const sessionPath = this.getSessionPath(sessionId);
|
|
169
187
|
const tempPath = `${sessionPath}.tmp`;
|
|
170
188
|
const now = new Date().toISOString();
|
|
@@ -207,14 +225,24 @@ export class SessionStorage {
|
|
|
207
225
|
}
|
|
208
226
|
/**
|
|
209
227
|
* Synchronous session loading (for internal use)
|
|
228
|
+
* Checks new location first, falls back to legacy location
|
|
210
229
|
*/
|
|
211
230
|
loadSessionSync(sessionId) {
|
|
212
231
|
const sessionPath = this.getSessionPath(sessionId);
|
|
232
|
+
const legacyPath = this.getLegacySessionPath(sessionId);
|
|
233
|
+
// Check new location first
|
|
234
|
+
let actualPath = sessionPath;
|
|
213
235
|
if (!existsSync(sessionPath)) {
|
|
214
|
-
|
|
236
|
+
// Fall back to legacy location
|
|
237
|
+
if (existsSync(legacyPath)) {
|
|
238
|
+
actualPath = legacyPath;
|
|
239
|
+
}
|
|
240
|
+
else {
|
|
241
|
+
return null;
|
|
242
|
+
}
|
|
215
243
|
}
|
|
216
244
|
try {
|
|
217
|
-
const content = readFileSync(
|
|
245
|
+
const content = readFileSync(actualPath, "utf-8");
|
|
218
246
|
const parsed = JSON.parse(content);
|
|
219
247
|
// Validate with zod
|
|
220
248
|
const validated = storedSessionSchema.parse(parsed);
|
|
@@ -225,39 +253,64 @@ export class SessionStorage {
|
|
|
225
253
|
}
|
|
226
254
|
}
|
|
227
255
|
/**
|
|
228
|
-
* Check if a session exists
|
|
256
|
+
* Check if a session exists (checks both new and legacy locations)
|
|
229
257
|
*/
|
|
230
258
|
sessionExists(sessionId) {
|
|
231
|
-
return existsSync(this.getSessionPath(sessionId))
|
|
259
|
+
return (existsSync(this.getSessionPath(sessionId)) ||
|
|
260
|
+
existsSync(this.getLegacySessionPath(sessionId)));
|
|
232
261
|
}
|
|
233
262
|
/**
|
|
234
|
-
* Delete a session
|
|
263
|
+
* Delete a session (deletes entire session directory or legacy file)
|
|
235
264
|
*/
|
|
236
265
|
async deleteSession(sessionId) {
|
|
237
|
-
const
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
}
|
|
266
|
+
const sessionDir = join(this.sessionsDir, sessionId);
|
|
267
|
+
const legacyPath = this.getLegacySessionPath(sessionId);
|
|
268
|
+
let deleted = false;
|
|
241
269
|
try {
|
|
242
|
-
|
|
243
|
-
|
|
270
|
+
// Delete session directory (.sessions/<sessionId>/)
|
|
271
|
+
if (existsSync(sessionDir)) {
|
|
272
|
+
// Recursively delete the session directory
|
|
273
|
+
const { rmSync } = await import("node:fs");
|
|
274
|
+
rmSync(sessionDir, { recursive: true, force: true });
|
|
275
|
+
deleted = true;
|
|
276
|
+
}
|
|
277
|
+
// Also delete legacy location if it exists (.sessions/<sessionId>.json)
|
|
278
|
+
if (existsSync(legacyPath)) {
|
|
279
|
+
unlinkSync(legacyPath);
|
|
280
|
+
deleted = true;
|
|
281
|
+
}
|
|
282
|
+
return deleted;
|
|
244
283
|
}
|
|
245
284
|
catch (error) {
|
|
246
285
|
throw new Error(`Failed to delete session ${sessionId}: ${error instanceof Error ? error.message : String(error)}`);
|
|
247
286
|
}
|
|
248
287
|
}
|
|
249
288
|
/**
|
|
250
|
-
* List all session IDs
|
|
289
|
+
* List all session IDs (checks both new and legacy locations)
|
|
251
290
|
*/
|
|
252
291
|
async listSessions() {
|
|
253
|
-
|
|
254
|
-
return [];
|
|
255
|
-
}
|
|
292
|
+
const sessionIds = new Set();
|
|
256
293
|
try {
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
294
|
+
// Check .sessions/ directory
|
|
295
|
+
if (existsSync(this.sessionsDir)) {
|
|
296
|
+
const entries = readdirSync(this.sessionsDir, { withFileTypes: true });
|
|
297
|
+
for (const entry of entries) {
|
|
298
|
+
if (entry.isDirectory()) {
|
|
299
|
+
// New format: .sessions/<sessionId>/session.json
|
|
300
|
+
const sessionJsonPath = join(this.sessionsDir, entry.name, "session.json");
|
|
301
|
+
if (existsSync(sessionJsonPath)) {
|
|
302
|
+
sessionIds.add(entry.name);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
else if (entry.isFile() &&
|
|
306
|
+
entry.name.endsWith(".json") &&
|
|
307
|
+
!entry.name.endsWith(".tmp")) {
|
|
308
|
+
// Legacy format: .sessions/<sessionId>.json
|
|
309
|
+
sessionIds.add(entry.name.replace(".json", ""));
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
return Array.from(sessionIds);
|
|
261
314
|
}
|
|
262
315
|
catch (error) {
|
|
263
316
|
throw new Error(`Failed to list sessions: ${error instanceof Error ? error.message : String(error)}`);
|
|
@@ -271,43 +324,84 @@ export class SessionStorage {
|
|
|
271
324
|
* Returns sessions sorted by updatedAt (most recent first)
|
|
272
325
|
*/
|
|
273
326
|
async listSessionsWithMetadata() {
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
const
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
updatedAt: session.metadata.updatedAt,
|
|
293
|
-
messageCount: session.messages.length,
|
|
294
|
-
};
|
|
295
|
-
if (firstUserText && "text" in firstUserText) {
|
|
296
|
-
entry.firstUserMessage = firstUserText.text.slice(0, 100);
|
|
297
|
-
}
|
|
298
|
-
sessions.push(entry);
|
|
327
|
+
// Get all session IDs from both locations
|
|
328
|
+
const sessionIds = await this.listSessions();
|
|
329
|
+
const sessions = [];
|
|
330
|
+
for (const sessionId of sessionIds) {
|
|
331
|
+
try {
|
|
332
|
+
const session = this.loadSessionSync(sessionId);
|
|
333
|
+
if (session) {
|
|
334
|
+
// Find the first user message for preview
|
|
335
|
+
const firstUserMsg = session.messages.find((m) => m.role === "user");
|
|
336
|
+
const firstUserText = firstUserMsg?.content.find((c) => c.type === "text");
|
|
337
|
+
const entry = {
|
|
338
|
+
sessionId: session.sessionId,
|
|
339
|
+
createdAt: session.metadata.createdAt,
|
|
340
|
+
updatedAt: session.metadata.updatedAt,
|
|
341
|
+
messageCount: session.messages.length,
|
|
342
|
+
};
|
|
343
|
+
if (firstUserText && "text" in firstUserText) {
|
|
344
|
+
entry.firstUserMessage = firstUserText.text.slice(0, 100);
|
|
299
345
|
}
|
|
300
|
-
|
|
301
|
-
catch {
|
|
302
|
-
// Skip invalid sessions
|
|
346
|
+
sessions.push(entry);
|
|
303
347
|
}
|
|
304
348
|
}
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
349
|
+
catch {
|
|
350
|
+
// Skip invalid sessions
|
|
351
|
+
}
|
|
308
352
|
}
|
|
309
|
-
|
|
310
|
-
|
|
353
|
+
// Sort by updatedAt, most recent first
|
|
354
|
+
sessions.sort((a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime());
|
|
355
|
+
return sessions;
|
|
356
|
+
}
|
|
357
|
+
/**
|
|
358
|
+
* Get the directory for storing large content files for a session (artifacts folder)
|
|
359
|
+
*/
|
|
360
|
+
getArtifactsDir(sessionId) {
|
|
361
|
+
return join(this.sessionsDir, sessionId, "artifacts");
|
|
362
|
+
}
|
|
363
|
+
/**
|
|
364
|
+
* Get the file path for a tool's original content
|
|
365
|
+
* Follows the pattern: artifacts/tool-<ToolName>/<toolCallId>.original.txt
|
|
366
|
+
*/
|
|
367
|
+
getToolOriginalPath(sessionId, toolName, toolCallId) {
|
|
368
|
+
return join(this.getArtifactsDir(sessionId), `tool-${toolName}`, `${toolCallId}.original.txt`);
|
|
369
|
+
}
|
|
370
|
+
/**
|
|
371
|
+
* Save original tool response to a separate file
|
|
372
|
+
* Follows the pattern: artifacts/tool-<ToolName>/<toolCallId>.original.txt
|
|
373
|
+
* @param toolName - The name of the tool (e.g., "Read", "Grep")
|
|
374
|
+
* @returns Relative file path (for storage in _meta.originalContentPath)
|
|
375
|
+
*/
|
|
376
|
+
saveToolOriginal(sessionId, toolName, toolCallId, content) {
|
|
377
|
+
const toolDir = join(this.getArtifactsDir(sessionId), `tool-${toolName}`);
|
|
378
|
+
if (!existsSync(toolDir)) {
|
|
379
|
+
mkdirSync(toolDir, { recursive: true });
|
|
311
380
|
}
|
|
381
|
+
const filePath = this.getToolOriginalPath(sessionId, toolName, toolCallId);
|
|
382
|
+
writeFileSync(filePath, content, "utf-8");
|
|
383
|
+
// Return relative path for storage in metadata
|
|
384
|
+
return `${sessionId}/artifacts/tool-${toolName}/${toolCallId}.original.txt`;
|
|
385
|
+
}
|
|
386
|
+
/**
|
|
387
|
+
* Load original tool response from separate file
|
|
388
|
+
*/
|
|
389
|
+
loadToolOriginal(sessionId, toolName, toolCallId) {
|
|
390
|
+
const filePath = this.getToolOriginalPath(sessionId, toolName, toolCallId);
|
|
391
|
+
if (!existsSync(filePath)) {
|
|
392
|
+
return null;
|
|
393
|
+
}
|
|
394
|
+
try {
|
|
395
|
+
return readFileSync(filePath, "utf-8");
|
|
396
|
+
}
|
|
397
|
+
catch {
|
|
398
|
+
return null;
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
/**
|
|
402
|
+
* Check if original content exists for a tool call
|
|
403
|
+
*/
|
|
404
|
+
hasToolOriginal(sessionId, toolName, toolCallId) {
|
|
405
|
+
return existsSync(this.getToolOriginalPath(sessionId, toolName, toolCallId));
|
|
312
406
|
}
|
|
313
407
|
}
|
|
@@ -21,7 +21,7 @@ export declare const HookConfigSchema: z.ZodObject<{
|
|
|
21
21
|
setting: z.ZodOptional<z.ZodUnion<readonly [z.ZodObject<{
|
|
22
22
|
threshold: z.ZodNumber;
|
|
23
23
|
}, z.core.$strip>, z.ZodObject<{
|
|
24
|
-
|
|
24
|
+
maxTokensSize: z.ZodOptional<z.ZodNumber>;
|
|
25
25
|
responseTruncationThreshold: z.ZodOptional<z.ZodNumber>;
|
|
26
26
|
}, z.core.$strip>]>>;
|
|
27
27
|
callback: z.ZodString;
|
|
@@ -78,7 +78,7 @@ export declare const AgentDefinitionSchema: z.ZodObject<{
|
|
|
78
78
|
setting: z.ZodOptional<z.ZodUnion<readonly [z.ZodObject<{
|
|
79
79
|
threshold: z.ZodNumber;
|
|
80
80
|
}, z.core.$strip>, z.ZodObject<{
|
|
81
|
-
|
|
81
|
+
maxTokensSize: z.ZodOptional<z.ZodNumber>;
|
|
82
82
|
responseTruncationThreshold: z.ZodOptional<z.ZodNumber>;
|
|
83
83
|
}, z.core.$strip>]>>;
|
|
84
84
|
callback: z.ZodString;
|
package/dist/definition/index.js
CHANGED
|
@@ -64,7 +64,7 @@ export const HookConfigSchema = z.object({
|
|
|
64
64
|
}),
|
|
65
65
|
// For tool_response hooks
|
|
66
66
|
z.object({
|
|
67
|
-
|
|
67
|
+
maxTokensSize: z.number().min(0).optional(),
|
|
68
68
|
responseTruncationThreshold: z.number().min(0).max(100).optional(),
|
|
69
69
|
}),
|
|
70
70
|
])
|
|
@@ -9,7 +9,7 @@ export declare const zAgentRunnerParams: z.ZodObject<{
|
|
|
9
9
|
suggestedPrompts: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
10
10
|
systemPrompt: z.ZodNullable<z.ZodString>;
|
|
11
11
|
model: z.ZodString;
|
|
12
|
-
tools: z.ZodOptional<z.ZodArray<z.ZodUnion<readonly [z.ZodUnion<readonly [z.ZodLiteral<"todo_write">, z.ZodLiteral<"get_weather">, z.ZodLiteral<"web_search">, z.ZodLiteral<"town_web_search">, z.ZodLiteral<"filesystem">, z.ZodLiteral<"generate_image">, z.ZodLiteral<"browser">]>, z.ZodObject<{
|
|
12
|
+
tools: z.ZodOptional<z.ZodArray<z.ZodUnion<readonly [z.ZodUnion<readonly [z.ZodLiteral<"artifacts">, z.ZodLiteral<"todo_write">, z.ZodLiteral<"get_weather">, z.ZodLiteral<"web_search">, z.ZodLiteral<"town_web_search">, z.ZodLiteral<"filesystem">, z.ZodLiteral<"generate_image">, z.ZodLiteral<"town_generate_image">, z.ZodLiteral<"browser">]>, z.ZodObject<{
|
|
13
13
|
type: z.ZodLiteral<"custom">;
|
|
14
14
|
modulePath: z.ZodString;
|
|
15
15
|
}, z.core.$strip>, z.ZodObject<{
|
|
@@ -48,7 +48,7 @@ export declare const zAgentRunnerParams: z.ZodObject<{
|
|
|
48
48
|
setting: z.ZodOptional<z.ZodUnion<readonly [z.ZodObject<{
|
|
49
49
|
threshold: z.ZodNumber;
|
|
50
50
|
}, z.core.$strip>, z.ZodObject<{
|
|
51
|
-
|
|
51
|
+
maxTokensSize: z.ZodOptional<z.ZodNumber>;
|
|
52
52
|
responseTruncationThreshold: z.ZodOptional<z.ZodNumber>;
|
|
53
53
|
}, z.core.$strip>]>>;
|
|
54
54
|
callback: z.ZodString;
|
|
@@ -74,6 +74,8 @@ export interface ConfigOverrides {
|
|
|
74
74
|
}
|
|
75
75
|
export type InvokeRequest = Omit<PromptRequest, "_meta"> & {
|
|
76
76
|
messageId: string;
|
|
77
|
+
/** Agent directory path for session-scoped file storage */
|
|
78
|
+
agentDir?: string;
|
|
77
79
|
sessionMeta?: Record<string, unknown>;
|
|
78
80
|
contextMessages?: SessionMessage[];
|
|
79
81
|
configOverrides?: ConfigOverrides;
|
|
@@ -137,6 +137,9 @@ export class HookExecutor {
|
|
|
137
137
|
if (result.truncationWarning !== undefined) {
|
|
138
138
|
response.truncationWarning = result.truncationWarning;
|
|
139
139
|
}
|
|
140
|
+
if (result.metadata !== undefined) {
|
|
141
|
+
response.metadata = result.metadata;
|
|
142
|
+
}
|
|
140
143
|
return response;
|
|
141
144
|
}
|
|
142
145
|
}
|
|
@@ -152,7 +155,11 @@ export class HookExecutor {
|
|
|
152
155
|
const notifications = [];
|
|
153
156
|
// Get threshold from settings
|
|
154
157
|
const settings = hook.setting;
|
|
155
|
-
|
|
158
|
+
// For notifications, calculate a percentage based on maxTokensSize relative to maxTokens
|
|
159
|
+
// Default to 20000 tokens, which we'll convert to a percentage for display
|
|
160
|
+
const maxTokensSize = settings?.maxTokensSize ?? 20000;
|
|
161
|
+
// Calculate approximate percentage: maxTokensSize / maxTokens * 100
|
|
162
|
+
const threshold = Math.min(100, Math.round((maxTokensSize / maxTokens) * 100));
|
|
156
163
|
// Capture start time and emit hook_triggered BEFORE callback runs
|
|
157
164
|
// This allows the UI to show the loading state immediately
|
|
158
165
|
const triggeredAt = Date.now();
|
|
@@ -185,8 +192,15 @@ export class HookExecutor {
|
|
|
185
192
|
toolResponse,
|
|
186
193
|
};
|
|
187
194
|
const result = await callback(hookContext);
|
|
195
|
+
logger.info("Hook callback result", {
|
|
196
|
+
hasMetadata: !!result.metadata,
|
|
197
|
+
metadataAction: result.metadata?.action,
|
|
198
|
+
hasModifiedOutput: !!result.metadata?.modifiedOutput,
|
|
199
|
+
modifiedOutputType: typeof result.metadata?.modifiedOutput,
|
|
200
|
+
toolCallId: toolResponse.toolCallId,
|
|
201
|
+
});
|
|
188
202
|
// Extract modified output and warnings from metadata
|
|
189
|
-
if (result.metadata
|
|
203
|
+
if (result.metadata?.modifiedOutput) {
|
|
190
204
|
// Hook took action - emit completed notification
|
|
191
205
|
const response = { notifications };
|
|
192
206
|
response.modifiedOutput = result.metadata.modifiedOutput;
|
|
@@ -194,6 +208,8 @@ export class HookExecutor {
|
|
|
194
208
|
response.truncationWarning = result.metadata
|
|
195
209
|
.truncationWarning;
|
|
196
210
|
}
|
|
211
|
+
// Include full metadata for persistence
|
|
212
|
+
response.metadata = result.metadata;
|
|
197
213
|
// Notify completion
|
|
198
214
|
this.emitNotification({
|
|
199
215
|
type: "hook_completed",
|
|
@@ -11,7 +11,7 @@ export const compactionTool = async (ctx) => {
|
|
|
11
11
|
logger.info("Compaction tool triggered", {
|
|
12
12
|
currentTokens: ctx.currentTokens,
|
|
13
13
|
maxTokens: ctx.maxTokens,
|
|
14
|
-
percentage: ctx.percentage.toFixed(2)
|
|
14
|
+
percentage: `${ctx.percentage.toFixed(2)}%`,
|
|
15
15
|
contextEntries: ctx.session.context.length,
|
|
16
16
|
totalMessages: ctx.session.messages.length,
|
|
17
17
|
model: ctx.model,
|
|
@@ -107,7 +107,8 @@ Please provide your summary based on the conversation above, following this stru
|
|
|
107
107
|
.join("\n")
|
|
108
108
|
: "Failed to extract summary";
|
|
109
109
|
// Extract token usage from LLM response
|
|
110
|
-
const responseUsage = response
|
|
110
|
+
const responseUsage = response
|
|
111
|
+
.usage_metadata;
|
|
111
112
|
const summaryTokens = responseUsage?.output_tokens ?? 0;
|
|
112
113
|
const inputTokensUsed = responseUsage?.input_tokens ?? ctx.currentTokens;
|
|
113
114
|
logger.info("Generated compaction summary", {
|
|
@@ -15,6 +15,8 @@ const COMPACTION_MODEL_CONTEXT = 200000; // Haiku context size for calculating t
|
|
|
15
15
|
* Tool response compaction hook - compacts or truncates large tool responses
|
|
16
16
|
* to prevent context overflow
|
|
17
17
|
*/
|
|
18
|
+
// Tools that should never be compacted (internal/small response tools)
|
|
19
|
+
const SKIP_COMPACTION_TOOLS = new Set(["todo_write", "TodoWrite"]);
|
|
18
20
|
export const toolResponseCompactor = async (ctx) => {
|
|
19
21
|
// Only process if we have tool response data
|
|
20
22
|
if (!ctx.toolResponse) {
|
|
@@ -22,28 +24,38 @@ export const toolResponseCompactor = async (ctx) => {
|
|
|
22
24
|
return { newContextEntry: null };
|
|
23
25
|
}
|
|
24
26
|
const { toolCallId, toolName, toolInput, rawOutput, outputTokens } = ctx.toolResponse;
|
|
27
|
+
// Skip compaction for certain internal tools
|
|
28
|
+
if (SKIP_COMPACTION_TOOLS.has(toolName)) {
|
|
29
|
+
logger.debug("Skipping compaction for internal tool", { toolName });
|
|
30
|
+
return { newContextEntry: null };
|
|
31
|
+
}
|
|
25
32
|
// Get settings from hook configuration
|
|
26
33
|
const settings = ctx.session.requestParams.hookSettings;
|
|
27
|
-
const
|
|
34
|
+
const maxTokensSize = settings?.maxTokensSize ?? 20000; // Default: 20000 tokens
|
|
28
35
|
const responseTruncationThreshold = settings?.responseTruncationThreshold ?? 30;
|
|
29
|
-
//
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
const
|
|
36
|
+
// Use maxTokensSize directly as it's now in tokens
|
|
37
|
+
const maxAllowedResponseSize = maxTokensSize;
|
|
38
|
+
// Calculate available space in context
|
|
39
|
+
const availableSpace = ctx.maxTokens - ctx.currentTokens;
|
|
40
|
+
// Failsafe: if available space is less than maxTokensSize, use availableSpace - 10%
|
|
41
|
+
const effectiveMaxResponseSize = availableSpace < maxAllowedResponseSize
|
|
42
|
+
? Math.floor(availableSpace * 0.9)
|
|
43
|
+
: maxAllowedResponseSize;
|
|
33
44
|
const compactionLimit = COMPACTION_MODEL_CONTEXT * (responseTruncationThreshold / 100);
|
|
34
45
|
logger.info("Tool response compaction hook triggered", {
|
|
35
46
|
toolCallId,
|
|
36
47
|
toolName,
|
|
37
48
|
outputTokens,
|
|
38
49
|
currentContext: ctx.currentTokens,
|
|
39
|
-
|
|
50
|
+
maxTokens: ctx.maxTokens,
|
|
51
|
+
maxAllowedResponseSize,
|
|
40
52
|
availableSpace,
|
|
41
|
-
|
|
53
|
+
effectiveMaxResponseSize,
|
|
42
54
|
compactionLimit,
|
|
43
55
|
settings,
|
|
44
56
|
});
|
|
45
57
|
// Case 0: Small response, no action needed
|
|
46
|
-
if (
|
|
58
|
+
if (outputTokens <= effectiveMaxResponseSize) {
|
|
47
59
|
logger.info("Tool response fits within threshold, no compaction needed");
|
|
48
60
|
return {
|
|
49
61
|
newContextEntry: null,
|
|
@@ -55,19 +67,20 @@ export const toolResponseCompactor = async (ctx) => {
|
|
|
55
67
|
};
|
|
56
68
|
}
|
|
57
69
|
// Response would exceed threshold, need to compact or truncate
|
|
58
|
-
// Determine target size:
|
|
59
|
-
// IMPORTANT: If context is already
|
|
70
|
+
// Determine target size: use effectiveMaxResponseSize, but cap at compactionLimit for truncation
|
|
71
|
+
// IMPORTANT: If context is already very full, availableSpace might be very small
|
|
60
72
|
// In that case, use a minimum reasonable target size (e.g., 10% of the output or 1000 tokens)
|
|
61
73
|
const minTargetSize = Math.max(Math.floor(outputTokens * 0.1), 1000);
|
|
62
|
-
const targetSize =
|
|
63
|
-
? Math.min(
|
|
74
|
+
const targetSize = effectiveMaxResponseSize > 0
|
|
75
|
+
? Math.min(effectiveMaxResponseSize, compactionLimit)
|
|
64
76
|
: minTargetSize;
|
|
65
77
|
logger.info("Calculated target size for compaction", {
|
|
66
78
|
availableSpace,
|
|
79
|
+
effectiveMaxResponseSize,
|
|
67
80
|
compactionLimit,
|
|
68
81
|
minTargetSize,
|
|
69
82
|
targetSize,
|
|
70
|
-
contextAlreadyOverThreshold: availableSpace <=
|
|
83
|
+
contextAlreadyOverThreshold: availableSpace <= maxAllowedResponseSize,
|
|
71
84
|
});
|
|
72
85
|
// Case 2: Huge response, must truncate (too large for LLM compaction)
|
|
73
86
|
if (outputTokens >= compactionLimit) {
|
|
@@ -89,7 +102,7 @@ export const toolResponseCompactor = async (ctx) => {
|
|
|
89
102
|
// Try more aggressive truncation (70% of target as emergency measure)
|
|
90
103
|
const emergencySize = Math.floor(targetSize * 0.7);
|
|
91
104
|
const emergencyTruncated = truncateToolResponse(rawOutput, emergencySize);
|
|
92
|
-
|
|
105
|
+
const emergencyTokens = countToolResultTokens(emergencyTruncated);
|
|
93
106
|
// Final safety check - if emergency truncation STILL exceeded target, use ultra-conservative fallback
|
|
94
107
|
if (emergencyTokens > targetSize) {
|
|
95
108
|
logger.error("Emergency truncation STILL exceeded target - using ultra-conservative fallback", {
|
|
@@ -133,7 +146,7 @@ export const toolResponseCompactor = async (ctx) => {
|
|
|
133
146
|
originalTokens: outputTokens,
|
|
134
147
|
finalTokens,
|
|
135
148
|
modifiedOutput: truncated,
|
|
136
|
-
truncationWarning: `Tool response was truncated from ${outputTokens.toLocaleString()} to ${finalTokens.toLocaleString()} tokens to fit within
|
|
149
|
+
truncationWarning: `Tool response was truncated from ${outputTokens.toLocaleString()} to ${finalTokens.toLocaleString()} tokens to fit within max response size limit (max allowed: ${effectiveMaxResponseSize.toLocaleString()} tokens)`,
|
|
137
150
|
},
|
|
138
151
|
};
|
|
139
152
|
}
|
|
@@ -141,6 +154,7 @@ export const toolResponseCompactor = async (ctx) => {
|
|
|
141
154
|
logger.info("Tool response requires intelligent compaction", {
|
|
142
155
|
outputTokens,
|
|
143
156
|
targetSize,
|
|
157
|
+
effectiveMaxResponseSize,
|
|
144
158
|
availableSpace,
|
|
145
159
|
compactionLimit,
|
|
146
160
|
});
|
|
@@ -151,7 +165,7 @@ export const toolResponseCompactor = async (ctx) => {
|
|
|
151
165
|
.map((msg) => {
|
|
152
166
|
const text = msg.content
|
|
153
167
|
.filter((b) => b.type === "text")
|
|
154
|
-
.map((b) => b.text)
|
|
168
|
+
.map((b) => (b.type === "text" ? b.text : ""))
|
|
155
169
|
.join("\n");
|
|
156
170
|
return `${msg.role}: ${text}`;
|
|
157
171
|
})
|
|
@@ -18,11 +18,11 @@ export interface ContextSizeSettings {
|
|
|
18
18
|
*/
|
|
19
19
|
export interface ToolResponseSettings {
|
|
20
20
|
/**
|
|
21
|
-
* Maximum
|
|
22
|
-
*
|
|
23
|
-
* Default:
|
|
21
|
+
* Maximum size of a tool response in tokens.
|
|
22
|
+
* Tool responses larger than this will trigger compaction.
|
|
23
|
+
* Default: 20000
|
|
24
24
|
*/
|
|
25
|
-
|
|
25
|
+
maxTokensSize?: number | undefined;
|
|
26
26
|
/**
|
|
27
27
|
* Maximum % of compaction model context (Haiku: 200k) that a tool response can be
|
|
28
28
|
* to attempt LLM-based compaction. Larger responses are truncated instead.
|
|
@@ -141,7 +141,6 @@ export declare function createContextEntry(messages: Array<{
|
|
|
141
141
|
toolInputTokens: number;
|
|
142
142
|
toolResultsTokens: number;
|
|
143
143
|
totalEstimated: number;
|
|
144
|
-
llmReportedInputTokens?: number | undefined;
|
|
145
144
|
}): ContextEntry;
|
|
146
145
|
/**
|
|
147
146
|
* Helper function to create a full message entry for context
|
|
@@ -13,5 +13,6 @@ export declare class LangchainAgent implements AgentRunner {
|
|
|
13
13
|
constructor(params: CreateAgentRunnerParams);
|
|
14
14
|
invoke(req: InvokeRequest): AsyncGenerator<ExtendedSessionUpdate, PromptResponse, undefined>;
|
|
15
15
|
private invokeInternal;
|
|
16
|
+
private invokeWithContext;
|
|
16
17
|
}
|
|
17
18
|
export { makeSubagentsTool } from "./tools/subagent.js";
|