@townco/agent 0.1.83 → 0.1.85

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.
Files changed (36) hide show
  1. package/dist/acp-server/adapter.js +140 -43
  2. package/dist/acp-server/http.js +55 -0
  3. package/dist/acp-server/session-storage.d.ts +31 -6
  4. package/dist/acp-server/session-storage.js +60 -1
  5. package/dist/definition/index.d.ts +2 -4
  6. package/dist/definition/index.js +1 -2
  7. package/dist/runner/agent-runner.d.ts +2 -3
  8. package/dist/runner/hooks/executor.d.ts +5 -2
  9. package/dist/runner/hooks/executor.js +26 -2
  10. package/dist/runner/hooks/predefined/document-context-extractor/chunk-manager.d.ts +37 -0
  11. package/dist/runner/hooks/predefined/document-context-extractor/chunk-manager.js +134 -0
  12. package/dist/runner/hooks/predefined/document-context-extractor/content-extractor.d.ts +20 -0
  13. package/dist/runner/hooks/predefined/document-context-extractor/content-extractor.js +171 -0
  14. package/dist/runner/hooks/predefined/document-context-extractor/extraction-state.d.ts +57 -0
  15. package/dist/runner/hooks/predefined/document-context-extractor/extraction-state.js +126 -0
  16. package/dist/runner/hooks/predefined/document-context-extractor/index.d.ts +22 -0
  17. package/dist/runner/hooks/predefined/document-context-extractor/index.js +338 -0
  18. package/dist/runner/hooks/predefined/document-context-extractor/relevance-scorer.d.ts +19 -0
  19. package/dist/runner/hooks/predefined/document-context-extractor/relevance-scorer.js +156 -0
  20. package/dist/runner/hooks/predefined/document-context-extractor/types.d.ts +130 -0
  21. package/dist/runner/hooks/predefined/document-context-extractor/types.js +8 -0
  22. package/dist/runner/hooks/predefined/tool-response-compactor.d.ts +0 -4
  23. package/dist/runner/hooks/predefined/tool-response-compactor.js +101 -222
  24. package/dist/runner/hooks/types.d.ts +18 -12
  25. package/dist/runner/langchain/index.js +64 -11
  26. package/dist/runner/langchain/tools/artifacts.js +6 -9
  27. package/dist/runner/langchain/tools/document_extract.d.ts +26 -0
  28. package/dist/runner/langchain/tools/document_extract.js +135 -0
  29. package/dist/runner/tools.d.ts +2 -2
  30. package/dist/runner/tools.js +1 -0
  31. package/dist/templates/index.d.ts +1 -2
  32. package/dist/tsconfig.tsbuildinfo +1 -1
  33. package/dist/utils/context-size-calculator.d.ts +1 -10
  34. package/dist/utils/context-size-calculator.js +1 -12
  35. package/package.json +6 -6
  36. package/templates/index.ts +1 -2
@@ -382,7 +382,7 @@ export class AgentAcpAdapter {
382
382
  : {}),
383
383
  ...block._meta,
384
384
  };
385
- // Debug: log subagent data being replayed
385
+ // Debug: log subagent data and compaction metadata being replayed
386
386
  logger.info("Replaying tool_call", {
387
387
  toolCallId: block.id,
388
388
  title: block.title,
@@ -391,6 +391,8 @@ export class AgentAcpAdapter {
391
391
  hasSubagentSessionId: !!block.subagentSessionId,
392
392
  hasSubagentMessages: !!block.subagentMessages,
393
393
  subagentMessagesCount: block.subagentMessages?.length,
394
+ blockMeta: block._meta,
395
+ replayMeta,
394
396
  });
395
397
  this.connection.sessionUpdate({
396
398
  sessionId: params.sessionId,
@@ -436,6 +438,7 @@ export class AgentAcpAdapter {
436
438
  status: block.status,
437
439
  rawOutput: block.rawOutput,
438
440
  error: block.error,
441
+ _meta: replayMeta,
439
442
  },
440
443
  });
441
444
  }
@@ -600,9 +603,8 @@ export class AgentAcpAdapter {
600
603
  contextMessages.push(entry.message);
601
604
  }
602
605
  }
603
- // Calculate context size - no LLM call yet, so only estimated values
604
- const context_size = calculateContextSize(contextMessages, this.agent.definition.systemPrompt ?? undefined, undefined, // No LLM-reported tokens yet
605
- this.currentToolOverheadTokens, // Include tool overhead
606
+ // Calculate context size - only estimated values
607
+ const context_size = calculateContextSize(contextMessages, this.agent.definition.systemPrompt ?? undefined, this.currentToolOverheadTokens, // Include tool overhead
606
608
  this.currentMcpOverheadTokens, // Include MCP overhead
607
609
  getModelContextWindow(this.agent.definition.model));
608
610
  const contextSnapshot = createContextSnapshot(session.messages.length - 1, // Exclude the newly added user message (it will be passed separately via prompt)
@@ -721,19 +723,8 @@ export class AgentAcpAdapter {
721
723
  if (tokenUsage.inputTokens !== undefined &&
722
724
  tokenUsage.inputTokens > 0) {
723
725
  turnTokenUsage.inputTokens = tokenUsage.inputTokens;
724
- // Update the LAST context entry with LLM-reported tokens
725
- if (!this.noSession && session.context.length > 0) {
726
- const lastContext = session.context[session.context.length - 1];
727
- if (lastContext) {
728
- lastContext.context_size.llmReportedInputTokens =
729
- tokenUsage.inputTokens;
730
- logger.debug("Updated context entry with LLM-reported tokens", {
731
- contextIndex: session.context.length - 1,
732
- llmReportedTokens: tokenUsage.inputTokens,
733
- estimatedTokens: lastContext.context_size.totalEstimated,
734
- });
735
- }
736
- }
726
+ // Note: We no longer update context entries with LLM-reported tokens
727
+ // as they can cause mismatches when updated on mid-turn snapshots
737
728
  }
738
729
  turnTokenUsage.outputTokens += tokenUsage.outputTokens ?? 0;
739
730
  turnTokenUsage.totalTokens += tokenUsage.totalTokens ?? 0;
@@ -910,19 +901,71 @@ export class AgentAcpAdapter {
910
901
  // Get the raw output
911
902
  let rawOutput = outputMsg.rawOutput || outputMsg.output;
912
903
  let truncationWarning;
904
+ // Check for compaction metadata embedded by LangChain tool wrapper
905
+ // This happens when compaction runs in the wrapper (before reaching adapter)
906
+ if (rawOutput &&
907
+ typeof rawOutput === "object" &&
908
+ "_compactionMeta" in rawOutput) {
909
+ const compactionMeta = rawOutput._compactionMeta;
910
+ // Store in _meta for UI persistence
911
+ if (!toolCallBlock._meta) {
912
+ toolCallBlock._meta = {};
913
+ }
914
+ if (compactionMeta.action) {
915
+ toolCallBlock._meta.compactionAction = compactionMeta.action;
916
+ }
917
+ if (compactionMeta.originalTokens !== undefined) {
918
+ toolCallBlock._meta.originalTokens =
919
+ compactionMeta.originalTokens;
920
+ }
921
+ if (compactionMeta.finalTokens !== undefined) {
922
+ toolCallBlock._meta.finalTokens = compactionMeta.finalTokens;
923
+ }
924
+ // Store original content only if compaction or truncation actually occurred
925
+ const wasCompacted = compactionMeta.action === "compacted" ||
926
+ compactionMeta.action === "truncated" ||
927
+ compactionMeta.action === "compacted_then_truncated";
928
+ if (compactionMeta.originalContent &&
929
+ wasCompacted &&
930
+ this.storage) {
931
+ try {
932
+ const toolName = toolCallBlock.title || "unknown";
933
+ const originalContentPath = this.storage.saveToolOriginal(params.sessionId, toolName, outputMsg.toolCallId, compactionMeta.originalContent);
934
+ toolCallBlock._meta.originalContentPath = originalContentPath;
935
+ logger.info("Saved original content to artifacts", {
936
+ toolCallId: outputMsg.toolCallId,
937
+ toolName,
938
+ path: originalContentPath,
939
+ });
940
+ }
941
+ catch (error) {
942
+ logger.warn("Failed to save original tool content", {
943
+ toolCallId: outputMsg.toolCallId,
944
+ error: error instanceof Error ? error.message : String(error),
945
+ });
946
+ }
947
+ }
948
+ logger.info("Extracted compaction metadata from tool response wrapper", {
949
+ toolCallId: outputMsg.toolCallId,
950
+ action: compactionMeta.action,
951
+ originalTokens: compactionMeta.originalTokens,
952
+ finalTokens: compactionMeta.finalTokens,
953
+ hasOriginalContent: !!compactionMeta.originalContent,
954
+ });
955
+ // Remove the metadata from rawOutput to keep it clean
956
+ const { _compactionMeta: _, ...cleanOutput } = rawOutput;
957
+ rawOutput = cleanOutput;
958
+ }
913
959
  if (rawOutput && !this.noSession) {
914
960
  // Execute tool_response hooks if configured
915
961
  const hooks = this.agent.definition.hooks ?? [];
916
962
  if (hooks.some((h) => h.type === "tool_response")) {
917
963
  const latestContext = session.context[session.context.length - 1];
918
- // Use max of estimated and LLM-reported tokens (same logic as UI)
919
- const currentContextTokens = Math.max(latestContext?.context_size.totalEstimated ?? 0, latestContext?.context_size.llmReportedInputTokens ?? 0);
920
- // Send the context size to the UI so it shows the same value we're using for hook evaluation
964
+ // Use estimated tokens for hook evaluation
965
+ const currentContextTokens = latestContext?.context_size.totalEstimated ?? 0;
966
+ // Send the context size to the UI
921
967
  if (latestContext?.context_size) {
922
- const contextSizeForUI = {
923
- ...latestContext.context_size,
924
- totalEstimated: currentContextTokens,
925
- };
968
+ const contextSizeForUI = latestContext.context_size;
926
969
  this.connection.sessionUpdate({
927
970
  sessionId: params.sessionId,
928
971
  update: {
@@ -964,6 +1007,12 @@ export class AgentAcpAdapter {
964
1007
  });
965
1008
  // Note: Notifications are now sent in real-time via the callback
966
1009
  // The hookResult.notifications array is kept for backwards compatibility
1010
+ // Capture original content BEFORE any modification (needed for storage)
1011
+ const originalContentStr = typeof rawOutput === "object" &&
1012
+ rawOutput !== null &&
1013
+ "content" in rawOutput
1014
+ ? String(rawOutput.content)
1015
+ : JSON.stringify(rawOutput);
967
1016
  // Apply modifications if hook returned them
968
1017
  if (hookResult.modifiedOutput) {
969
1018
  rawOutput = hookResult.modifiedOutput;
@@ -974,6 +1023,49 @@ export class AgentAcpAdapter {
974
1023
  });
975
1024
  }
976
1025
  truncationWarning = hookResult.truncationWarning;
1026
+ // Store hook result metadata in toolCallBlock._meta for persistence
1027
+ if (hookResult.metadata) {
1028
+ if (!toolCallBlock._meta) {
1029
+ toolCallBlock._meta = {};
1030
+ }
1031
+ // Store compaction action and stats
1032
+ if (hookResult.metadata.action) {
1033
+ toolCallBlock._meta.compactionAction = hookResult.metadata
1034
+ .action;
1035
+ }
1036
+ if (hookResult.metadata.originalTokens !== undefined) {
1037
+ toolCallBlock._meta.originalTokens = hookResult.metadata
1038
+ .originalTokens;
1039
+ }
1040
+ if (hookResult.metadata.finalTokens !== undefined) {
1041
+ toolCallBlock._meta.finalTokens = hookResult.metadata
1042
+ .finalTokens;
1043
+ }
1044
+ // Store original content if compaction occurred (action !== "none")
1045
+ if (hookResult.metadata.action &&
1046
+ hookResult.metadata.action !== "none" &&
1047
+ this.storage) {
1048
+ try {
1049
+ const toolName = toolCallBlock.title || "unknown";
1050
+ const originalContentPath = this.storage.saveToolOriginal(params.sessionId, toolName, outputMsg.toolCallId, originalContentStr);
1051
+ toolCallBlock._meta.originalContentPath =
1052
+ originalContentPath;
1053
+ logger.info("Saved original content to artifacts", {
1054
+ toolCallId: outputMsg.toolCallId,
1055
+ toolName,
1056
+ path: originalContentPath,
1057
+ });
1058
+ }
1059
+ catch (error) {
1060
+ logger.warn("Failed to save original tool content", {
1061
+ toolCallId: outputMsg.toolCallId,
1062
+ error: error instanceof Error
1063
+ ? error.message
1064
+ : String(error),
1065
+ });
1066
+ }
1067
+ }
1068
+ }
977
1069
  }
978
1070
  }
979
1071
  // Store the (potentially modified) output
@@ -987,6 +1079,22 @@ export class AgentAcpAdapter {
987
1079
  }
988
1080
  toolCallBlock._meta.truncationWarning = truncationWarning;
989
1081
  }
1082
+ // Send compaction metadata to the client if present
1083
+ // This is needed for live streaming (not just replay)
1084
+ if (toolCallBlock._meta?.compactionAction) {
1085
+ logger.info("Sending compaction metadata to client during live streaming", {
1086
+ toolCallId: outputMsg.toolCallId,
1087
+ _meta: toolCallBlock._meta,
1088
+ });
1089
+ this.connection.sessionUpdate({
1090
+ sessionId: params.sessionId,
1091
+ update: {
1092
+ sessionUpdate: "tool_call_update",
1093
+ toolCallId: outputMsg.toolCallId,
1094
+ _meta: toolCallBlock._meta,
1095
+ },
1096
+ });
1097
+ }
990
1098
  // Note: content blocks are handled by the transport for display
991
1099
  // We store the raw output here for session persistence
992
1100
  // Create mid-turn context snapshot after tool completes
@@ -1062,8 +1170,7 @@ export class AgentAcpAdapter {
1062
1170
  }
1063
1171
  }
1064
1172
  // Calculate context size - tool result is now in the message, but hasn't been sent to LLM yet
1065
- const context_size = calculateContextSize(contextMessages, this.agent.definition.systemPrompt ?? undefined, undefined, // Tool result hasn't been sent to LLM yet, so no new LLM-reported tokens
1066
- this.currentToolOverheadTokens, // Include tool overhead
1173
+ const context_size = calculateContextSize(contextMessages, this.agent.definition.systemPrompt ?? undefined, this.currentToolOverheadTokens, // Include tool overhead
1067
1174
  this.currentMcpOverheadTokens, // Include MCP overhead
1068
1175
  getModelContextWindow(this.agent.definition.model));
1069
1176
  // Create snapshot with a pointer to the partial message (not a full copy!)
@@ -1217,9 +1324,8 @@ export class AgentAcpAdapter {
1217
1324
  contextMessages.push(entry.message);
1218
1325
  }
1219
1326
  }
1220
- // Calculate context size with LLM-reported tokens from this turn
1221
- const context_size = calculateContextSize(contextMessages, this.agent.definition.systemPrompt ?? undefined, turnTokenUsage.inputTokens, // Final LLM-reported tokens from this turn
1222
- this.currentToolOverheadTokens, // Include tool overhead
1327
+ // Calculate context size - only estimated values
1328
+ const context_size = calculateContextSize(contextMessages, this.agent.definition.systemPrompt ?? undefined, this.currentToolOverheadTokens, // Include tool overhead
1223
1329
  this.currentMcpOverheadTokens, // Include MCP overhead
1224
1330
  getModelContextWindow(this.agent.definition.model));
1225
1331
  const contextSnapshot = createContextSnapshot(session.messages.length, new Date().toISOString(), previousContext, context_size);
@@ -1297,28 +1403,19 @@ export class AgentAcpAdapter {
1297
1403
  context: session.context,
1298
1404
  requestParams: session.requestParams,
1299
1405
  };
1300
- // Get actual input token count from latest context entry
1406
+ // Get input token count from latest context entry
1301
1407
  const latestContext = session.context.length > 0
1302
1408
  ? session.context[session.context.length - 1]
1303
1409
  : undefined;
1304
- // Use max of estimated and LLM-reported tokens (same logic as UI)
1305
- // This is conservative - we want to trigger compaction earlier rather than later
1306
- const actualInputTokens = Math.max(latestContext?.context_size.totalEstimated ?? 0, latestContext?.context_size.llmReportedInputTokens ?? 0);
1410
+ // Use estimated tokens for hook evaluation
1411
+ const actualInputTokens = latestContext?.context_size.totalEstimated ?? 0;
1307
1412
  logger.debug("Using tokens for hook execution", {
1308
- llmReported: latestContext?.context_size.llmReportedInputTokens,
1309
1413
  estimated: latestContext?.context_size.totalEstimated,
1310
1414
  used: actualInputTokens,
1311
1415
  });
1312
- // Send the context size to the UI so it shows the same value we're using for hook evaluation
1313
- // This ensures the UI percentage matches what the hook sees
1416
+ // Send the context size to the UI
1314
1417
  if (latestContext?.context_size) {
1315
- // Create an updated context_size with the actualInputTokens we computed
1316
- // so UI can calculate the same percentage
1317
- const contextSizeForUI = {
1318
- ...latestContext.context_size,
1319
- // Override with the max value we computed (what hooks actually use)
1320
- totalEstimated: actualInputTokens,
1321
- };
1418
+ const contextSizeForUI = latestContext.context_size;
1322
1419
  this.connection.sessionUpdate({
1323
1420
  sessionId,
1324
1421
  update: {
@@ -455,6 +455,61 @@ export function makeHttpTransport(agent, agentDir, agentName) {
455
455
  }, 500);
456
456
  }
457
457
  });
458
+ // Serve files from session artifacts folder
459
+ app.get("/sessions/:sessionId/artifacts/*", async (c) => {
460
+ if (!agentDir || !agentName) {
461
+ return c.json({ error: "Session storage not configured" }, 500);
462
+ }
463
+ const noSession = process.env.TOWN_NO_SESSION === "true";
464
+ if (noSession) {
465
+ return c.json({ error: "Sessions disabled" }, 500);
466
+ }
467
+ const sessionId = c.req.param("sessionId");
468
+ // Extract the filename from the wildcard path
469
+ const filename = c.req.path.replace(`/sessions/${sessionId}/artifacts/`, "");
470
+ if (!sessionId || !filename) {
471
+ return c.json({ error: "Session ID and filename required" }, 400);
472
+ }
473
+ // Build path to artifacts folder
474
+ const storage = new SessionStorage(agentDir, agentName);
475
+ const artifactsDir = storage.getArtifactsDir(sessionId);
476
+ const filePath = join(artifactsDir, filename);
477
+ // Security check: ensure the file is within the artifacts directory
478
+ const normalizedPath = resolve(filePath);
479
+ const normalizedArtifactsDir = resolve(artifactsDir);
480
+ if (!normalizedPath.startsWith(normalizedArtifactsDir)) {
481
+ logger.warn("Attempted to access file outside artifacts directory", {
482
+ filename,
483
+ filePath,
484
+ artifactsDir,
485
+ });
486
+ return c.json({ error: "Invalid path" }, 403);
487
+ }
488
+ try {
489
+ const file = Bun.file(filePath);
490
+ const exists = await file.exists();
491
+ if (!exists) {
492
+ logger.warn("Artifact file not found", {
493
+ sessionId,
494
+ filename,
495
+ filePath,
496
+ });
497
+ return c.json({ error: "File not found" }, 404);
498
+ }
499
+ const content = await file.text();
500
+ return c.json({ content });
501
+ }
502
+ catch (error) {
503
+ logger.error("Failed to load artifact", {
504
+ error,
505
+ sessionId,
506
+ filename,
507
+ });
508
+ return c.json({
509
+ error: error instanceof Error ? error.message : String(error),
510
+ }, 500);
511
+ }
512
+ });
458
513
  // Serve static files from agent directory (for generated images, etc.)
459
514
  if (agentDir) {
460
515
  app.get("/static/*", async (c) => {
@@ -66,11 +66,13 @@ export interface ToolCallBlock {
66
66
  startedAt?: number | undefined;
67
67
  completedAt?: number | undefined;
68
68
  _meta?: {
69
- truncationWarning?: string;
70
- compactionAction?: "compacted" | "truncated";
71
- originalTokens?: number;
72
- finalTokens?: number;
73
- };
69
+ truncationWarning?: string | undefined;
70
+ compactionAction?: "compacted" | "truncated" | undefined;
71
+ originalTokens?: number | undefined;
72
+ finalTokens?: number | undefined;
73
+ originalContentPreview?: string | undefined;
74
+ originalContentPath?: string | undefined;
75
+ } | undefined;
74
76
  /** Sub-agent HTTP port (for reference, not used in replay) */
75
77
  subagentPort?: number | undefined;
76
78
  /** Sub-agent session ID (for reference, not used in replay) */
@@ -124,7 +126,6 @@ export interface ContextEntry {
124
126
  toolInputTokens: number;
125
127
  toolResultsTokens: number;
126
128
  totalEstimated: number;
127
- llmReportedInputTokens?: number | undefined;
128
129
  modelContextWindow?: number | undefined;
129
130
  };
130
131
  }
@@ -212,4 +213,28 @@ export declare class SessionStorage {
212
213
  messageCount: number;
213
214
  firstUserMessage?: string;
214
215
  }>>;
216
+ /**
217
+ * Get the directory for storing large content files for a session (artifacts folder)
218
+ */
219
+ getArtifactsDir(sessionId: string): string;
220
+ /**
221
+ * Get the file path for a tool's original content
222
+ * Follows the pattern: artifacts/tool-<ToolName>/<toolCallId>.original.txt
223
+ */
224
+ private getToolOriginalPath;
225
+ /**
226
+ * Save original tool response to a separate file
227
+ * Follows the pattern: artifacts/tool-<ToolName>/<toolCallId>.original.txt
228
+ * @param toolName - The name of the tool (e.g., "Read", "Grep")
229
+ * @returns Relative file path (for storage in _meta.originalContentPath)
230
+ */
231
+ saveToolOriginal(sessionId: string, toolName: string, toolCallId: string, content: string): string;
232
+ /**
233
+ * Load original tool response from separate file
234
+ */
235
+ loadToolOriginal(sessionId: string, toolName: string, toolCallId: string): string | null;
236
+ /**
237
+ * Check if original content exists for a tool call
238
+ */
239
+ hasToolOriginal(sessionId: string, toolName: string, toolCallId: string): boolean;
215
240
  }
@@ -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
  });
@@ -345,4 +354,54 @@ export class SessionStorage {
345
354
  sessions.sort((a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime());
346
355
  return sessions;
347
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 });
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));
406
+ }
348
407
  }
@@ -21,8 +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
- maxContextThreshold: z.ZodOptional<z.ZodNumber>;
25
- responseTruncationThreshold: z.ZodOptional<z.ZodNumber>;
24
+ maxTokensSize: z.ZodOptional<z.ZodNumber>;
26
25
  }, z.core.$strip>]>>;
27
26
  callback: z.ZodString;
28
27
  }, z.core.$strip>;
@@ -78,8 +77,7 @@ export declare const AgentDefinitionSchema: z.ZodObject<{
78
77
  setting: z.ZodOptional<z.ZodUnion<readonly [z.ZodObject<{
79
78
  threshold: z.ZodNumber;
80
79
  }, z.core.$strip>, z.ZodObject<{
81
- maxContextThreshold: z.ZodOptional<z.ZodNumber>;
82
- responseTruncationThreshold: z.ZodOptional<z.ZodNumber>;
80
+ maxTokensSize: z.ZodOptional<z.ZodNumber>;
83
81
  }, z.core.$strip>]>>;
84
82
  callback: z.ZodString;
85
83
  }, z.core.$strip>>>;
@@ -64,8 +64,7 @@ export const HookConfigSchema = z.object({
64
64
  }),
65
65
  // For tool_response hooks
66
66
  z.object({
67
- maxContextThreshold: z.number().min(0).max(100).optional(),
68
- responseTruncationThreshold: z.number().min(0).max(100).optional(),
67
+ maxTokensSize: z.number().min(0).optional(),
69
68
  }),
70
69
  ])
71
70
  .optional(),
@@ -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<"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<{
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.ZodLiteral<"document_extract">]>, z.ZodObject<{
13
13
  type: z.ZodLiteral<"custom">;
14
14
  modulePath: z.ZodString;
15
15
  }, z.core.$strip>, z.ZodObject<{
@@ -48,8 +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
- maxContextThreshold: z.ZodOptional<z.ZodNumber>;
52
- responseTruncationThreshold: z.ZodOptional<z.ZodNumber>;
51
+ maxTokensSize: z.ZodOptional<z.ZodNumber>;
53
52
  }, z.core.$strip>]>>;
54
53
  callback: z.ZodString;
55
54
  }, z.core.$strip>>>;
@@ -1,5 +1,5 @@
1
1
  import type { ContextEntry } from "../../acp-server/session-storage";
2
- import type { HookCallback, HookConfig, HookNotification, ReadonlySession } from "./types";
2
+ import type { HookCallback, HookConfig, HookNotification, HookStorageInterface, ReadonlySession } from "./types";
3
3
  /**
4
4
  * Callback for streaming hook notifications in real-time
5
5
  */
@@ -12,7 +12,9 @@ export declare class HookExecutor {
12
12
  private model;
13
13
  private loadCallback;
14
14
  private onNotification;
15
- constructor(hooks: HookConfig[], model: string, loadCallback: (callbackRef: string) => Promise<HookCallback>, onNotification?: OnHookNotification);
15
+ private storage;
16
+ private sessionId;
17
+ constructor(hooks: HookConfig[], model: string, loadCallback: (callbackRef: string) => Promise<HookCallback>, onNotification?: OnHookNotification, storage?: HookStorageInterface, sessionId?: string);
16
18
  /**
17
19
  * Emit a notification - sends immediately if callback provided, otherwise collects for batch return
18
20
  */
@@ -41,6 +43,7 @@ export declare class HookExecutor {
41
43
  }): Promise<{
42
44
  modifiedOutput?: Record<string, unknown>;
43
45
  truncationWarning?: string;
46
+ metadata?: Record<string, unknown>;
44
47
  notifications: HookNotification[];
45
48
  }>;
46
49
  /**
@@ -9,11 +9,15 @@ export class HookExecutor {
9
9
  model;
10
10
  loadCallback;
11
11
  onNotification;
12
- constructor(hooks, model, loadCallback, onNotification) {
12
+ storage;
13
+ sessionId;
14
+ constructor(hooks, model, loadCallback, onNotification, storage, sessionId) {
13
15
  this.hooks = hooks;
14
16
  this.model = model;
15
17
  this.loadCallback = loadCallback;
16
18
  this.onNotification = onNotification;
19
+ this.storage = storage;
20
+ this.sessionId = sessionId;
17
21
  }
18
22
  /**
19
23
  * Emit a notification - sends immediately if callback provided, otherwise collects for batch return
@@ -84,6 +88,8 @@ export class HookExecutor {
84
88
  maxTokens,
85
89
  percentage,
86
90
  model: this.model,
91
+ sessionId: this.sessionId,
92
+ storage: this.storage,
87
93
  };
88
94
  const result = await callback(hookContext);
89
95
  // Notify completion
@@ -137,6 +143,9 @@ export class HookExecutor {
137
143
  if (result.truncationWarning !== undefined) {
138
144
  response.truncationWarning = result.truncationWarning;
139
145
  }
146
+ if (result.metadata !== undefined) {
147
+ response.metadata = result.metadata;
148
+ }
140
149
  return response;
141
150
  }
142
151
  }
@@ -152,7 +161,11 @@ export class HookExecutor {
152
161
  const notifications = [];
153
162
  // Get threshold from settings
154
163
  const settings = hook.setting;
155
- const threshold = settings?.maxContextThreshold ?? 80;
164
+ // For notifications, calculate a percentage based on maxTokensSize relative to maxTokens
165
+ // Default to 20000 tokens, which we'll convert to a percentage for display
166
+ const maxTokensSize = settings?.maxTokensSize ?? 20000;
167
+ // Calculate approximate percentage: maxTokensSize / maxTokens * 100
168
+ const threshold = Math.min(100, Math.round((maxTokensSize / maxTokens) * 100));
156
169
  // Capture start time and emit hook_triggered BEFORE callback runs
157
170
  // This allows the UI to show the loading state immediately
158
171
  const triggeredAt = Date.now();
@@ -182,9 +195,18 @@ export class HookExecutor {
182
195
  maxTokens,
183
196
  percentage,
184
197
  model: this.model,
198
+ sessionId: this.sessionId,
199
+ storage: this.storage,
185
200
  toolResponse,
186
201
  };
187
202
  const result = await callback(hookContext);
203
+ logger.info("Hook callback result", {
204
+ hasMetadata: !!result.metadata,
205
+ metadataAction: result.metadata?.action,
206
+ hasModifiedOutput: !!result.metadata?.modifiedOutput,
207
+ modifiedOutputType: typeof result.metadata?.modifiedOutput,
208
+ toolCallId: toolResponse.toolCallId,
209
+ });
188
210
  // Extract modified output and warnings from metadata
189
211
  if (result.metadata?.modifiedOutput) {
190
212
  // Hook took action - emit completed notification
@@ -194,6 +216,8 @@ export class HookExecutor {
194
216
  response.truncationWarning = result.metadata
195
217
  .truncationWarning;
196
218
  }
219
+ // Include full metadata for persistence
220
+ response.metadata = result.metadata;
197
221
  // Notify completion
198
222
  this.emitNotification({
199
223
  type: "hook_completed",