@townco/agent 0.1.49 → 0.1.51

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 (43) hide show
  1. package/dist/acp-server/adapter.d.ts +15 -0
  2. package/dist/acp-server/adapter.js +445 -67
  3. package/dist/acp-server/http.js +8 -1
  4. package/dist/acp-server/session-storage.d.ts +19 -0
  5. package/dist/acp-server/session-storage.js +9 -0
  6. package/dist/definition/index.d.ts +16 -4
  7. package/dist/definition/index.js +17 -4
  8. package/dist/index.d.ts +2 -1
  9. package/dist/index.js +10 -1
  10. package/dist/runner/agent-runner.d.ts +13 -2
  11. package/dist/runner/agent-runner.js +4 -0
  12. package/dist/runner/hooks/executor.d.ts +18 -1
  13. package/dist/runner/hooks/executor.js +74 -62
  14. package/dist/runner/hooks/predefined/compaction-tool.js +19 -3
  15. package/dist/runner/hooks/predefined/tool-response-compactor.d.ts +6 -0
  16. package/dist/runner/hooks/predefined/tool-response-compactor.js +461 -0
  17. package/dist/runner/hooks/registry.js +2 -0
  18. package/dist/runner/hooks/types.d.ts +39 -3
  19. package/dist/runner/hooks/types.js +9 -1
  20. package/dist/runner/langchain/index.d.ts +1 -0
  21. package/dist/runner/langchain/index.js +523 -321
  22. package/dist/runner/langchain/model-factory.js +1 -1
  23. package/dist/runner/langchain/otel-callbacks.d.ts +18 -0
  24. package/dist/runner/langchain/otel-callbacks.js +123 -0
  25. package/dist/runner/langchain/tools/subagent.js +21 -1
  26. package/dist/scaffold/link-local.d.ts +1 -0
  27. package/dist/scaffold/link-local.js +54 -0
  28. package/dist/scaffold/project-scaffold.js +1 -0
  29. package/dist/telemetry/index.d.ts +83 -0
  30. package/dist/telemetry/index.js +172 -0
  31. package/dist/telemetry/setup.d.ts +22 -0
  32. package/dist/telemetry/setup.js +141 -0
  33. package/dist/templates/index.d.ts +7 -0
  34. package/dist/tsconfig.tsbuildinfo +1 -1
  35. package/dist/utils/context-size-calculator.d.ts +29 -0
  36. package/dist/utils/context-size-calculator.js +78 -0
  37. package/dist/utils/index.d.ts +2 -0
  38. package/dist/utils/index.js +2 -0
  39. package/dist/utils/token-counter.d.ts +19 -0
  40. package/dist/utils/token-counter.js +44 -0
  41. package/index.ts +16 -1
  42. package/package.json +24 -7
  43. package/templates/index.ts +18 -6
@@ -1,8 +1,9 @@
1
1
  import { createHash } from "node:crypto";
2
+ import { join } from "node:path";
2
3
  import { gzipSync } from "node:zlib";
3
4
  import * as acp from "@agentclientprotocol/sdk";
4
5
  import { PGlite } from "@electric-sql/pglite";
5
- import { createLogger } from "@townco/core";
6
+ import { configureLogsDir, createLogger } from "@townco/core";
6
7
  import { Hono } from "hono";
7
8
  import { cors } from "hono/cors";
8
9
  import { streamSSE } from "hono/streaming";
@@ -48,6 +49,12 @@ function safeChannelName(prefix, id) {
48
49
  return `${prefix}_${hash}`;
49
50
  }
50
51
  export function makeHttpTransport(agent, agentDir, agentName) {
52
+ // Configure logger to write to .logs/ directory if agentDir is provided
53
+ if (agentDir) {
54
+ const logsDir = join(agentDir, ".logs");
55
+ configureLogsDir(logsDir);
56
+ logger.info("Configured logs directory", { logsDir });
57
+ }
51
58
  const inbound = new TransformStream();
52
59
  const outbound = new TransformStream();
53
60
  const bridge = acp.ndJsonStream(outbound.writable, inbound.readable);
@@ -16,6 +16,12 @@ export interface ToolCallBlock {
16
16
  error?: string | undefined;
17
17
  startedAt?: number | undefined;
18
18
  completedAt?: number | undefined;
19
+ _meta?: {
20
+ truncationWarning?: string;
21
+ compactionAction?: "compacted" | "truncated";
22
+ originalTokens?: number;
23
+ finalTokens?: number;
24
+ };
19
25
  }
20
26
  export type ContentBlock = TextBlock | ToolCallBlock;
21
27
  /**
@@ -50,6 +56,19 @@ export interface ContextEntry {
50
56
  * compacted into the full message(s) in this entry
51
57
  */
52
58
  compactedUpTo?: number | undefined;
59
+ /**
60
+ * Complete breakdown of context size at this snapshot.
61
+ * Calculated by counting ALL tokens in the messages referenced by this context entry.
62
+ */
63
+ context_size: {
64
+ systemPromptTokens: number;
65
+ userMessagesTokens: number;
66
+ assistantMessagesTokens: number;
67
+ toolInputTokens: number;
68
+ toolResultsTokens: number;
69
+ totalEstimated: number;
70
+ llmReportedInputTokens?: number | undefined;
71
+ };
53
72
  }
54
73
  /**
55
74
  * Session metadata
@@ -56,6 +56,15 @@ const contextEntrySchema = z.object({
56
56
  timestamp: z.string(),
57
57
  messages: z.array(contextMessageEntrySchema),
58
58
  compactedUpTo: z.number().optional(),
59
+ context_size: z.object({
60
+ systemPromptTokens: z.number(),
61
+ userMessagesTokens: z.number(),
62
+ assistantMessagesTokens: z.number(),
63
+ toolInputTokens: z.number(),
64
+ toolResultsTokens: z.number(),
65
+ totalEstimated: z.number(),
66
+ llmReportedInputTokens: z.number().optional(),
67
+ }),
59
68
  });
60
69
  const sessionMetadataSchema = z.object({
61
70
  createdAt: z.string(),
@@ -16,14 +16,22 @@ export declare const McpConfigSchema: z.ZodUnion<readonly [z.ZodObject<{
16
16
  export declare const HookConfigSchema: z.ZodObject<{
17
17
  type: z.ZodEnum<{
18
18
  context_size: "context_size";
19
+ tool_response: "tool_response";
19
20
  }>;
20
- setting: z.ZodOptional<z.ZodObject<{
21
+ setting: z.ZodOptional<z.ZodUnion<readonly [z.ZodObject<{
21
22
  threshold: z.ZodNumber;
22
- }, z.core.$strip>>;
23
+ }, z.core.$strip>, z.ZodObject<{
24
+ maxContextThreshold: z.ZodOptional<z.ZodNumber>;
25
+ responseTruncationThreshold: z.ZodOptional<z.ZodNumber>;
26
+ }, z.core.$strip>]>>;
23
27
  callback: z.ZodString;
24
28
  }, z.core.$strip>;
25
29
  /** Agent definition schema. */
26
30
  export declare const AgentDefinitionSchema: z.ZodObject<{
31
+ displayName: z.ZodOptional<z.ZodString>;
32
+ version: z.ZodOptional<z.ZodString>;
33
+ description: z.ZodOptional<z.ZodString>;
34
+ suggestedPrompts: z.ZodOptional<z.ZodArray<z.ZodString>>;
27
35
  systemPrompt: z.ZodNullable<z.ZodString>;
28
36
  model: z.ZodString;
29
37
  tools: z.ZodOptional<z.ZodArray<z.ZodUnion<readonly [z.ZodString, z.ZodObject<{
@@ -56,10 +64,14 @@ export declare const AgentDefinitionSchema: z.ZodObject<{
56
64
  hooks: z.ZodOptional<z.ZodArray<z.ZodObject<{
57
65
  type: z.ZodEnum<{
58
66
  context_size: "context_size";
67
+ tool_response: "tool_response";
59
68
  }>;
60
- setting: z.ZodOptional<z.ZodObject<{
69
+ setting: z.ZodOptional<z.ZodUnion<readonly [z.ZodObject<{
61
70
  threshold: z.ZodNumber;
62
- }, z.core.$strip>>;
71
+ }, z.core.$strip>, z.ZodObject<{
72
+ maxContextThreshold: z.ZodOptional<z.ZodNumber>;
73
+ responseTruncationThreshold: z.ZodOptional<z.ZodNumber>;
74
+ }, z.core.$strip>]>>;
63
75
  callback: z.ZodString;
64
76
  }, z.core.$strip>>>;
65
77
  }, z.core.$strip>;
@@ -54,16 +54,29 @@ const ToolSchema = z.union([
54
54
  ]);
55
55
  /** Hook configuration schema. */
56
56
  export const HookConfigSchema = z.object({
57
- type: z.enum(["context_size"]),
57
+ type: z.enum(["context_size", "tool_response"]),
58
58
  setting: z
59
- .object({
60
- threshold: z.number().min(0).max(100),
61
- })
59
+ .union([
60
+ // For context_size hooks
61
+ z.object({
62
+ threshold: z.number().min(0).max(100),
63
+ }),
64
+ // For tool_response hooks
65
+ z.object({
66
+ maxContextThreshold: z.number().min(0).max(100).optional(),
67
+ responseTruncationThreshold: z.number().min(0).max(100).optional(),
68
+ }),
69
+ ])
62
70
  .optional(),
63
71
  callback: z.string(),
64
72
  });
65
73
  /** Agent definition schema. */
66
74
  export const AgentDefinitionSchema = z.object({
75
+ /** Human-readable display name for the agent (shown in UI). */
76
+ displayName: z.string().optional(),
77
+ version: z.string().optional(),
78
+ description: z.string().optional(),
79
+ suggestedPrompts: z.array(z.string()).optional(),
67
80
  systemPrompt: z.string().nullable(),
68
81
  model: z.string(),
69
82
  tools: z.array(ToolSchema).optional(),
package/dist/index.d.ts CHANGED
@@ -1 +1,2 @@
1
- export {};
1
+ export { configureTelemetry, type TelemetryConfig } from "./telemetry/index.js";
2
+ export { initializeOpenTelemetry, initializeOpenTelemetryFromEnv, type TelemetrySetupOptions, } from "./telemetry/setup.js";
package/dist/index.js CHANGED
@@ -1,7 +1,16 @@
1
1
  import { basename } from "node:path";
2
2
  import { createLogger } from "@townco/core";
3
3
  import { makeHttpTransport, makeStdioTransport } from "./acp-server";
4
+ import { initializeOpenTelemetryFromEnv } from "./telemetry/setup.js";
4
5
  import { makeSubagentsTool } from "./utils";
6
+ // Re-export telemetry configuration for library users
7
+ export { configureTelemetry } from "./telemetry/index.js";
8
+ export { initializeOpenTelemetry, initializeOpenTelemetryFromEnv, } from "./telemetry/setup.js";
9
+ // Configure OpenTelemetry if enabled via environment variable
10
+ // Example: ENABLE_TELEMETRY=true bun run index.ts stdio
11
+ if (process.env.ENABLE_TELEMETRY === "true") {
12
+ initializeOpenTelemetryFromEnv();
13
+ }
5
14
  const logger = createLogger("agent-index");
6
15
  const exampleAgent = {
7
16
  model: "claude-sonnet-4-5-20250929",
@@ -27,7 +36,7 @@ const exampleAgent = {
27
36
  {
28
37
  type: "context_size",
29
38
  setting: {
30
- threshold: 95,
39
+ threshold: 80,
31
40
  },
32
41
  callback: "compaction_tool",
33
42
  },
@@ -2,6 +2,10 @@ import type { PromptRequest, PromptResponse, SessionNotification } from "@agentc
2
2
  import { z } from "zod";
3
3
  import type { ContentBlock } from "../acp-server/session-storage.js";
4
4
  export declare const zAgentRunnerParams: z.ZodObject<{
5
+ displayName: z.ZodOptional<z.ZodString>;
6
+ version: z.ZodOptional<z.ZodString>;
7
+ description: z.ZodOptional<z.ZodString>;
8
+ suggestedPrompts: z.ZodOptional<z.ZodArray<z.ZodString>>;
5
9
  systemPrompt: z.ZodNullable<z.ZodString>;
6
10
  model: z.ZodString;
7
11
  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<"filesystem">]>, z.ZodObject<{
@@ -33,10 +37,14 @@ export declare const zAgentRunnerParams: z.ZodObject<{
33
37
  hooks: z.ZodOptional<z.ZodArray<z.ZodObject<{
34
38
  type: z.ZodEnum<{
35
39
  context_size: "context_size";
40
+ tool_response: "tool_response";
36
41
  }>;
37
- setting: z.ZodOptional<z.ZodObject<{
42
+ setting: z.ZodOptional<z.ZodUnion<readonly [z.ZodObject<{
38
43
  threshold: z.ZodNumber;
39
- }, z.core.$strip>>;
44
+ }, z.core.$strip>, z.ZodObject<{
45
+ maxContextThreshold: z.ZodOptional<z.ZodNumber>;
46
+ responseTruncationThreshold: z.ZodOptional<z.ZodNumber>;
47
+ }, z.core.$strip>]>>;
40
48
  callback: z.ZodString;
41
49
  }, z.core.$strip>>>;
42
50
  }, z.core.$strip>;
@@ -64,6 +72,7 @@ export interface AgentMessageChunkWithTokens {
64
72
  };
65
73
  _meta?: {
66
74
  tokenUsage?: TokenUsage;
75
+ contextInputTokens?: number;
67
76
  [key: string]: unknown;
68
77
  };
69
78
  }
@@ -97,6 +106,8 @@ export type ExtendedSessionUpdate = (SessionNotification["update"] & {
97
106
  rawOutput?: Record<string, unknown>;
98
107
  _meta?: {
99
108
  messageId?: string;
109
+ contextInputTokens?: number;
110
+ [key: string]: unknown;
100
111
  };
101
112
  } | AgentMessageChunkWithTokens | HookNotificationUpdate;
102
113
  /** Describes an object that can run an agent definition */
@@ -2,6 +2,10 @@ import { z } from "zod";
2
2
  import { HookConfigSchema, McpConfigSchema } from "../definition";
3
3
  import { zToolType } from "./tools";
4
4
  export const zAgentRunnerParams = z.object({
5
+ displayName: z.string().optional(),
6
+ version: z.string().optional(),
7
+ description: z.string().optional(),
8
+ suggestedPrompts: z.array(z.string()).optional(),
5
9
  systemPrompt: z.string().nullable(),
6
10
  model: z.string(),
7
11
  tools: z.array(zToolType).optional(),
@@ -12,7 +12,7 @@ export declare class HookExecutor {
12
12
  * Execute hooks before agent invocation
13
13
  * Returns new context entries to append and any notifications to send
14
14
  */
15
- executeHooks(session: ReadonlySession): Promise<{
15
+ executeHooks(session: ReadonlySession, actualInputTokens: number): Promise<{
16
16
  newContextEntries: ContextEntry[];
17
17
  notifications: HookNotification[];
18
18
  }>;
@@ -20,4 +20,21 @@ export declare class HookExecutor {
20
20
  * Execute a context_size hook
21
21
  */
22
22
  private executeContextSizeHook;
23
+ /**
24
+ * Execute tool_response hooks when a tool returns output
25
+ */
26
+ executeToolResponseHooks(session: ReadonlySession, currentContextTokens: number, toolResponse: {
27
+ toolCallId: string;
28
+ toolName: string;
29
+ toolInput: Record<string, unknown>;
30
+ rawOutput: Record<string, unknown>;
31
+ outputTokens: number;
32
+ }): Promise<{
33
+ modifiedOutput?: Record<string, unknown>;
34
+ truncationWarning?: string;
35
+ }>;
36
+ /**
37
+ * Execute a single tool_response hook
38
+ */
39
+ private executeToolResponseHook;
23
40
  }
@@ -7,58 +7,6 @@ const logger = createLogger("hook-executor");
7
7
  function getModelMaxTokens(model) {
8
8
  return MODEL_CONTEXT_WINDOWS[model] ?? DEFAULT_CONTEXT_SIZE;
9
9
  }
10
- /**
11
- * Estimate token count for session messages
12
- * This is a rough estimate: ~4 characters per token
13
- */
14
- function estimateTokens(messages) {
15
- let totalChars = 0;
16
- for (const message of messages) {
17
- // Count characters in content blocks
18
- for (const block of message.content) {
19
- if (block.type === "text") {
20
- totalChars += block.text.length;
21
- }
22
- else if (block.type === "tool_call") {
23
- // Estimate tool call size (title + inputs/outputs)
24
- totalChars += block.title.length;
25
- if (block.rawInput) {
26
- totalChars += JSON.stringify(block.rawInput).length;
27
- }
28
- if (block.rawOutput) {
29
- totalChars += JSON.stringify(block.rawOutput).length;
30
- }
31
- }
32
- }
33
- }
34
- // Rough estimate: 4 characters per token
35
- return Math.ceil(totalChars / 4);
36
- }
37
- /**
38
- * Resolve context entries to session messages
39
- */
40
- function resolveContextToMessages(context, allMessages) {
41
- if (context.length === 0) {
42
- return [];
43
- }
44
- const latestContext = context[context.length - 1];
45
- if (!latestContext) {
46
- return [];
47
- }
48
- const resolved = [];
49
- for (const entry of latestContext.messages) {
50
- if (entry.type === "pointer") {
51
- const message = allMessages[entry.index];
52
- if (message) {
53
- resolved.push(message);
54
- }
55
- }
56
- else if (entry.type === "full") {
57
- resolved.push(entry.message);
58
- }
59
- }
60
- return resolved;
61
- }
62
10
  /**
63
11
  * Hook executor manages hook lifecycle
64
12
  */
@@ -75,17 +23,18 @@ export class HookExecutor {
75
23
  * Execute hooks before agent invocation
76
24
  * Returns new context entries to append and any notifications to send
77
25
  */
78
- async executeHooks(session) {
26
+ async executeHooks(session, actualInputTokens) {
79
27
  logger.info(`Executing hooks - found ${this.hooks.length} hook(s)`, {
80
28
  hooks: this.hooks.map((h) => h.type),
81
29
  contextEntries: session.context.length,
82
30
  totalMessages: session.messages.length,
31
+ actualInputTokens,
83
32
  });
84
33
  const newContextEntries = [];
85
34
  const notifications = [];
86
35
  for (const hook of this.hooks) {
87
36
  if (hook.type === "context_size") {
88
- const result = await this.executeContextSizeHook(hook, session);
37
+ const result = await this.executeContextSizeHook(hook, session, actualInputTokens);
89
38
  if (result) {
90
39
  notifications.push(...result.notifications);
91
40
  if (result.newContextEntry) {
@@ -99,20 +48,17 @@ export class HookExecutor {
99
48
  /**
100
49
  * Execute a context_size hook
101
50
  */
102
- async executeContextSizeHook(hook, session) {
103
- // Resolve context to messages for token estimation
104
- const resolvedMessages = resolveContextToMessages(session.context, session.messages);
51
+ async executeContextSizeHook(hook, session, actualInputTokens) {
105
52
  const maxTokens = getModelMaxTokens(this.model);
106
- const currentTokens = estimateTokens(resolvedMessages);
107
- const percentage = (currentTokens / maxTokens) * 100;
53
+ const percentage = (actualInputTokens / maxTokens) * 100;
108
54
  // Default threshold is 95%
109
55
  const threshold = hook.setting?.threshold ?? 95;
110
56
  // Check if threshold exceeded
111
57
  if (percentage < threshold) {
112
- logger.info(`Context hook not triggered: ${currentTokens} tokens (${percentage.toFixed(1)}%) < threshold ${threshold}%`);
58
+ logger.info(`Context hook not triggered: ${actualInputTokens} tokens (${percentage.toFixed(1)}%) < threshold ${threshold}%`);
113
59
  return null; // No action needed
114
60
  }
115
- logger.info(`Context hook triggered: ${currentTokens} tokens (${percentage.toFixed(1)}%) exceeds threshold ${threshold}%`);
61
+ logger.info(`Context hook triggered: ${actualInputTokens} tokens (${percentage.toFixed(1)}%) exceeds threshold ${threshold}%`);
116
62
  const notifications = [];
117
63
  // Notify that hook is triggered
118
64
  notifications.push({
@@ -127,7 +73,7 @@ export class HookExecutor {
127
73
  const callback = await this.loadCallback(hook.callback);
128
74
  const hookContext = {
129
75
  session,
130
- currentTokens,
76
+ currentTokens: actualInputTokens,
131
77
  maxTokens,
132
78
  percentage,
133
79
  model: this.model,
@@ -160,4 +106,70 @@ export class HookExecutor {
160
106
  };
161
107
  }
162
108
  }
109
+ /**
110
+ * Execute tool_response hooks when a tool returns output
111
+ */
112
+ async executeToolResponseHooks(session, currentContextTokens, toolResponse) {
113
+ logger.info(`Executing tool_response hooks - found ${this.hooks.length} hook(s)`, {
114
+ toolCallId: toolResponse.toolCallId,
115
+ toolName: toolResponse.toolName,
116
+ outputTokens: toolResponse.outputTokens,
117
+ });
118
+ for (const hook of this.hooks) {
119
+ if (hook.type === "tool_response") {
120
+ const result = await this.executeToolResponseHook(hook, session, currentContextTokens, toolResponse);
121
+ if (result) {
122
+ return result;
123
+ }
124
+ }
125
+ }
126
+ return {}; // No modifications
127
+ }
128
+ /**
129
+ * Execute a single tool_response hook
130
+ */
131
+ async executeToolResponseHook(hook, session, currentContextTokens, toolResponse) {
132
+ const maxTokens = getModelMaxTokens(this.model);
133
+ try {
134
+ // Load and execute callback
135
+ const callback = await this.loadCallback(hook.callback);
136
+ // Pass hook settings through requestParams
137
+ const sessionWithSettings = {
138
+ ...session,
139
+ requestParams: {
140
+ ...session.requestParams,
141
+ hookSettings: hook.setting,
142
+ },
143
+ };
144
+ const hookContext = {
145
+ session: sessionWithSettings,
146
+ currentTokens: currentContextTokens,
147
+ maxTokens,
148
+ percentage: (currentContextTokens / maxTokens) * 100,
149
+ model: this.model,
150
+ toolResponse,
151
+ };
152
+ const result = await callback(hookContext);
153
+ // Extract modified output and warnings from metadata
154
+ if (result.metadata) {
155
+ const response = {};
156
+ if (result.metadata.modifiedOutput) {
157
+ response.modifiedOutput = result.metadata.modifiedOutput;
158
+ }
159
+ if (result.metadata.truncationWarning) {
160
+ response.truncationWarning = result.metadata
161
+ .truncationWarning;
162
+ }
163
+ return response;
164
+ }
165
+ return null;
166
+ }
167
+ catch (error) {
168
+ logger.error("Tool response hook execution failed", {
169
+ callback: hook.callback,
170
+ error: error instanceof Error ? error.message : String(error),
171
+ });
172
+ return null; // Return original output on error
173
+ }
174
+ }
163
175
  }
@@ -84,22 +84,38 @@ Please provide your summary based on the conversation above, following this stru
84
84
  .map((block) => block.text)
85
85
  .join("\n")
86
86
  : "Failed to extract summary";
87
+ // Extract token usage from LLM response
88
+ const responseUsage = response.usage_metadata;
89
+ const summaryTokens = responseUsage?.output_tokens ?? 0;
90
+ const inputTokensUsed = responseUsage?.input_tokens ?? ctx.currentTokens;
87
91
  logger.info("Generated compaction summary", {
88
92
  originalMessages: messagesToCompact.length,
89
93
  summaryLength: summaryText.length,
90
- estimatedTokensSaved: Math.round(ctx.currentTokens * 0.7),
94
+ inputTokens: inputTokensUsed,
95
+ summaryTokens,
96
+ tokensSaved: inputTokensUsed - summaryTokens,
91
97
  });
92
98
  // Create a new context entry with the summary
93
99
  const summaryEntry = createFullMessageEntry("user", `This session is being continued from a previous conversation that ran out of context. The conversation is summarized below:\n${summaryText}`);
94
100
  // Set compactedUpTo to indicate all messages have been compacted into the summary
95
101
  const lastMessageIndex = messagesToCompact.length - 1;
96
- const newContextEntry = createContextEntry([summaryEntry], undefined, lastMessageIndex);
102
+ const newContextEntry = createContextEntry([summaryEntry], undefined, lastMessageIndex, {
103
+ // Store summary tokens in userMessagesTokens since the summary is a user message
104
+ systemPromptTokens: 0,
105
+ userMessagesTokens: summaryTokens,
106
+ assistantMessagesTokens: 0,
107
+ toolInputTokens: 0,
108
+ toolResultsTokens: 0,
109
+ totalEstimated: summaryTokens,
110
+ });
97
111
  return {
98
112
  newContextEntry,
99
113
  metadata: {
100
114
  action: "compacted",
101
115
  messagesRemoved: messagesToCompact.length - 1,
102
- tokensSaved: Math.round(ctx.currentTokens * 0.7),
116
+ tokensBeforeCompaction: inputTokensUsed,
117
+ tokensSaved: inputTokensUsed - summaryTokens,
118
+ summaryTokens, // Token count of the summary itself
103
119
  summaryGenerated: true,
104
120
  },
105
121
  };
@@ -0,0 +1,6 @@
1
+ import type { HookCallback } from "../types.js";
2
+ /**
3
+ * Tool response compaction hook - compacts or truncates large tool responses
4
+ * to prevent context overflow
5
+ */
6
+ export declare const toolResponseCompactor: HookCallback;