@townco/agent 0.1.80 → 0.1.82

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.
@@ -602,8 +602,7 @@ export class AgentAcpAdapter {
602
602
  const context_size = calculateContextSize(contextMessages, this.agent.definition.systemPrompt ?? undefined, undefined, // No LLM-reported tokens yet
603
603
  this.currentToolOverheadTokens, // Include tool overhead
604
604
  this.currentMcpOverheadTokens, // Include MCP overhead
605
- getModelContextWindow(this.agent.definition.model), // Model context window for UI
606
- true);
605
+ getModelContextWindow(this.agent.definition.model));
607
606
  const contextSnapshot = createContextSnapshot(session.messages.length - 1, // Exclude the newly added user message (it will be passed separately via prompt)
608
607
  new Date().toISOString(), previousContext, context_size);
609
608
  session.context.push(contextSnapshot);
@@ -1215,13 +1214,10 @@ export class AgentAcpAdapter {
1215
1214
  }
1216
1215
  }
1217
1216
  // Calculate context size with LLM-reported tokens from this turn
1218
- // Exclude tool results - they're only sent during the turn they were received,
1219
- // not in subsequent turns (only messages are sent)
1220
1217
  const context_size = calculateContextSize(contextMessages, this.agent.definition.systemPrompt ?? undefined, turnTokenUsage.inputTokens, // Final LLM-reported tokens from this turn
1221
1218
  this.currentToolOverheadTokens, // Include tool overhead
1222
1219
  this.currentMcpOverheadTokens, // Include MCP overhead
1223
- getModelContextWindow(this.agent.definition.model), // Model context window for UI
1224
- true);
1220
+ getModelContextWindow(this.agent.definition.model));
1225
1221
  const contextSnapshot = createContextSnapshot(session.messages.length, new Date().toISOString(), previousContext, context_size);
1226
1222
  session.context.push(contextSnapshot);
1227
1223
  await this.saveSessionToDisk(params.sessionId, session);
@@ -163,6 +163,7 @@ export class HookExecutor {
163
163
  currentPercentage: percentage,
164
164
  callback: hook.callback,
165
165
  triggeredAt,
166
+ toolCallId: toolResponse.toolCallId,
166
167
  }, notifications);
167
168
  try {
168
169
  // Load and execute callback
@@ -200,6 +201,7 @@ export class HookExecutor {
200
201
  callback: hook.callback,
201
202
  metadata: result.metadata,
202
203
  completedAt: Date.now(),
204
+ toolCallId: toolResponse.toolCallId,
203
205
  }, notifications);
204
206
  return response;
205
207
  }
@@ -210,6 +212,7 @@ export class HookExecutor {
210
212
  callback: hook.callback,
211
213
  metadata: { action: "no_action_needed" },
212
214
  completedAt: Date.now(),
215
+ toolCallId: toolResponse.toolCallId,
213
216
  }, notifications);
214
217
  return { notifications };
215
218
  }
@@ -221,6 +224,7 @@ export class HookExecutor {
221
224
  callback: hook.callback,
222
225
  error: error instanceof Error ? error.message : String(error),
223
226
  completedAt: Date.now(),
227
+ toolCallId: toolResponse.toolCallId,
224
228
  }, notifications);
225
229
  logger.error("Tool response hook execution failed", {
226
230
  callback: hook.callback,
@@ -24,14 +24,36 @@ export const compactionTool = async (ctx) => {
24
24
  });
25
25
  // Build the conversation history to compact
26
26
  const messagesToCompact = ctx.session.messages;
27
- // Convert session messages to text for context
27
+ // Convert session messages to text for context, including tool calls and results
28
28
  const conversationText = messagesToCompact
29
29
  .map((msg) => {
30
- const textContent = msg.content
31
- .filter((block) => block.type === "text")
32
- .map((block) => block.text)
33
- .join("\n");
34
- return `${msg.role.toUpperCase()}:\n${textContent}`;
30
+ const parts = [];
31
+ for (const block of msg.content) {
32
+ if (block.type === "text") {
33
+ parts.push(block.text);
34
+ }
35
+ else if (block.type === "tool_call") {
36
+ // Include tool call info
37
+ parts.push(`[Tool: ${block.title}]`);
38
+ if (block.rawInput) {
39
+ parts.push(`Input: ${JSON.stringify(block.rawInput, null, 2)}`);
40
+ }
41
+ if (block.rawOutput) {
42
+ // Summarize large outputs to avoid overwhelming the compaction LLM
43
+ const outputStr = JSON.stringify(block.rawOutput);
44
+ if (outputStr.length > 2000) {
45
+ parts.push(`Output: [Large output - ${outputStr.length} chars]`);
46
+ }
47
+ else {
48
+ parts.push(`Output: ${outputStr}`);
49
+ }
50
+ }
51
+ if (block.error) {
52
+ parts.push(`Error: ${block.error}`);
53
+ }
54
+ }
55
+ }
56
+ return `${msg.role.toUpperCase()}:\n${parts.join("\n")}`;
35
57
  })
36
58
  .join("\n\n---\n\n");
37
59
  // Create system prompt for compaction
@@ -56,7 +56,19 @@ export const toolResponseCompactor = async (ctx) => {
56
56
  }
57
57
  // Response would exceed threshold, need to compact or truncate
58
58
  // Determine target size: fit within available space, but cap at compactionLimit for truncation
59
- const targetSize = Math.min(availableSpace, compactionLimit);
59
+ // IMPORTANT: If context is already over threshold, availableSpace will be negative
60
+ // In that case, use a minimum reasonable target size (e.g., 10% of the output or 1000 tokens)
61
+ const minTargetSize = Math.max(Math.floor(outputTokens * 0.1), 1000);
62
+ const targetSize = availableSpace > 0
63
+ ? Math.min(availableSpace, compactionLimit)
64
+ : minTargetSize;
65
+ logger.info("Calculated target size for compaction", {
66
+ availableSpace,
67
+ compactionLimit,
68
+ minTargetSize,
69
+ targetSize,
70
+ contextAlreadyOverThreshold: availableSpace <= 0,
71
+ });
60
72
  // Case 2: Huge response, must truncate (too large for LLM compaction)
61
73
  if (outputTokens >= compactionLimit) {
62
74
  logger.warn("Tool response exceeds compaction capacity, truncating", {
@@ -168,16 +168,19 @@ export type HookNotification = {
168
168
  currentPercentage: number;
169
169
  callback: string;
170
170
  triggeredAt: number;
171
+ toolCallId?: string;
171
172
  } | {
172
173
  type: "hook_completed";
173
174
  hookType: HookType;
174
175
  callback: string;
175
176
  metadata?: HookResult["metadata"];
176
177
  completedAt: number;
178
+ toolCallId?: string;
177
179
  } | {
178
180
  type: "hook_error";
179
181
  hookType: HookType;
180
182
  callback: string;
181
183
  error: string;
182
184
  completedAt: number;
185
+ toolCallId?: string;
183
186
  };
@@ -472,6 +472,7 @@ export class LangchainAgent {
472
472
  }
473
473
  const agent = createAgent(agentConfig);
474
474
  // Build messages from context history if available, otherwise use just the prompt
475
+ // Type includes tool messages for sending tool results
475
476
  let messages;
476
477
  // Helper to convert content blocks to LangChain format
477
478
  // LangChain expects image_url type with data URL, not Claude's native image+source format
@@ -539,11 +540,62 @@ export class LangchainAgent {
539
540
  };
540
541
  if (req.contextMessages && req.contextMessages.length > 0) {
541
542
  // Use context messages (already resolved from context entries)
542
- // Convert to LangChain format
543
- messages = req.contextMessages.map((msg) => ({
544
- type: msg.role === "user" ? "human" : "ai",
545
- content: convertContentBlocks(msg.content),
546
- }));
543
+ // Convert to LangChain format, including tool calls and their results
544
+ messages = [];
545
+ for (const msg of req.contextMessages) {
546
+ if (msg.role === "user") {
547
+ messages.push({
548
+ type: "human",
549
+ content: convertContentBlocks(msg.content),
550
+ });
551
+ }
552
+ else if (msg.role === "assistant") {
553
+ // Check if message has tool calls
554
+ const toolCalls = msg.content.filter((block) => block.type === "tool_call");
555
+ const textBlocks = msg.content.filter((block) => block.type === "text" || block.type === "image");
556
+ if (toolCalls.length > 0) {
557
+ // Build AI message with tool_use blocks
558
+ const aiContent = [];
559
+ // Add any text content first
560
+ for (const block of textBlocks) {
561
+ if (block.type === "text") {
562
+ aiContent.push({ type: "text", text: block.text });
563
+ }
564
+ }
565
+ // Add tool_use blocks
566
+ for (const tc of toolCalls) {
567
+ if (tc.type === "tool_call") {
568
+ aiContent.push({
569
+ type: "tool_use",
570
+ id: tc.id,
571
+ name: tc.title,
572
+ input: tc.rawInput || {},
573
+ });
574
+ }
575
+ }
576
+ messages.push({ type: "ai", content: aiContent });
577
+ // Add tool result messages for each tool call that has output
578
+ for (const tc of toolCalls) {
579
+ if (tc.type === "tool_call" && tc.rawOutput) {
580
+ messages.push({
581
+ type: "tool",
582
+ tool_call_id: tc.id,
583
+ content: typeof tc.rawOutput === "string"
584
+ ? tc.rawOutput
585
+ : JSON.stringify(tc.rawOutput),
586
+ });
587
+ }
588
+ }
589
+ }
590
+ else {
591
+ // No tool calls - simple AI message
592
+ messages.push({
593
+ type: "ai",
594
+ content: convertContentBlocks(msg.content),
595
+ });
596
+ }
597
+ }
598
+ }
547
599
  // Add the current prompt as the final human message
548
600
  const promptContent = convertContentBlocks(req.prompt);
549
601
  messages.push({
@@ -1059,7 +1111,7 @@ const makeMcpToolsClient = (mcpConfigs) => {
1059
1111
  // Whether to throw on errors if a tool fails to load (optional, default: true)
1060
1112
  throwOnLoadError: true,
1061
1113
  // Whether to prefix tool names with the server name (optional, default: false)
1062
- prefixToolNameWithServerName: false,
1114
+ prefixToolNameWithServerName: true,
1063
1115
  // Optional additional prefix for tool names (optional, default: "")
1064
1116
  additionalToolNamePrefix: "",
1065
1117
  // Use standardized content block format in tool outputs
@@ -5,8 +5,8 @@ export declare function makeFilesystemTools(workingDirectory: string): readonly
5
5
  glob: z.ZodOptional<z.ZodString>;
6
6
  output_mode: z.ZodOptional<z.ZodEnum<{
7
7
  content: "content";
8
- files_with_matches: "files_with_matches";
9
8
  count: "count";
9
+ files_with_matches: "files_with_matches";
10
10
  }>>;
11
11
  "-B": z.ZodOptional<z.ZodNumber>;
12
12
  "-A": z.ZodOptional<z.ZodNumber>;
@@ -20,7 +20,7 @@ export declare function makeFilesystemTools(workingDirectory: string): readonly
20
20
  pattern: string;
21
21
  path?: string | undefined;
22
22
  glob?: string | undefined;
23
- output_mode?: "content" | "files_with_matches" | "count" | undefined;
23
+ output_mode?: "content" | "count" | "files_with_matches" | undefined;
24
24
  "-B"?: number | undefined;
25
25
  "-A"?: number | undefined;
26
26
  "-C"?: number | undefined;
@@ -33,7 +33,7 @@ export declare function makeFilesystemTools(workingDirectory: string): readonly
33
33
  pattern: string;
34
34
  path?: string | undefined;
35
35
  glob?: string | undefined;
36
- output_mode?: "content" | "files_with_matches" | "count" | undefined;
36
+ output_mode?: "content" | "count" | "files_with_matches" | undefined;
37
37
  "-B"?: number | undefined;
38
38
  "-A"?: number | undefined;
39
39
  "-C"?: number | undefined;