@townco/agent 0.1.83 → 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 +140 -43
- package/dist/acp-server/http.js +55 -0
- package/dist/acp-server/session-storage.d.ts +31 -6
- package/dist/acp-server/session-storage.js +60 -1
- package/dist/definition/index.d.ts +2 -2
- package/dist/definition/index.js +1 -1
- package/dist/runner/agent-runner.d.ts +1 -1
- package/dist/runner/hooks/executor.d.ts +1 -0
- package/dist/runner/hooks/executor.js +17 -1
- package/dist/runner/hooks/predefined/tool-response-compactor.d.ts +0 -4
- package/dist/runner/hooks/predefined/tool-response-compactor.js +28 -14
- package/dist/runner/hooks/types.d.ts +4 -5
- package/dist/runner/langchain/index.js +62 -11
- package/dist/runner/langchain/tools/artifacts.js +6 -9
- 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/package.json +6 -6
- package/templates/index.ts +1 -1
|
@@ -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) {
|
|
@@ -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
|
});
|
|
@@ -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
|
|
@@ -2,7 +2,7 @@ import { mkdir } from "node:fs/promises";
|
|
|
2
2
|
import * as path from "node:path";
|
|
3
3
|
import { MultiServerMCPClient } from "@langchain/mcp-adapters";
|
|
4
4
|
import { context, propagation, trace } from "@opentelemetry/api";
|
|
5
|
-
import {
|
|
5
|
+
import { ensureAuthenticated } from "@townco/core/auth";
|
|
6
6
|
import { AIMessageChunk, createAgent, ToolMessage, tool, } from "langchain";
|
|
7
7
|
import { z } from "zod";
|
|
8
8
|
import { SUBAGENT_MODE_KEY } from "../../acp-server/adapter";
|
|
@@ -356,7 +356,8 @@ export class LangchainAgent {
|
|
|
356
356
|
// MCP tools - calculate overhead separately
|
|
357
357
|
let mcpOverheadTokens = 0;
|
|
358
358
|
if ((this.definition.mcps?.length ?? 0) > 0) {
|
|
359
|
-
const
|
|
359
|
+
const client = await makeMcpToolsClient(this.definition.mcps);
|
|
360
|
+
const mcpTools = await client.getTools();
|
|
360
361
|
const mcpToolMetadata = mcpTools.map(extractToolMetadata);
|
|
361
362
|
mcpOverheadTokens = estimateAllToolsOverhead(mcpToolMetadata);
|
|
362
363
|
enabledTools.push(...mcpTools);
|
|
@@ -447,9 +448,25 @@ export class LangchainAgent {
|
|
|
447
448
|
reduction: `${((1 - compactedTokens / outputTokens) * 100).toFixed(1)}%`,
|
|
448
449
|
totalCumulativeTokens: cumulativeToolOutputTokens,
|
|
449
450
|
});
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
451
|
+
// Include compaction metadata in the output for the adapter to extract
|
|
452
|
+
// Also include original content so adapter can store it
|
|
453
|
+
const originalContentStr = typeof rawOutput === "object" &&
|
|
454
|
+
rawOutput !== null &&
|
|
455
|
+
"content" in rawOutput
|
|
456
|
+
? String(rawOutput.content)
|
|
457
|
+
: JSON.stringify(rawOutput);
|
|
458
|
+
const outputWithMeta = {
|
|
459
|
+
...modifiedOutput,
|
|
460
|
+
_compactionMeta: {
|
|
461
|
+
action: hookResult.metadata.action,
|
|
462
|
+
originalTokens: hookResult.metadata.originalTokens,
|
|
463
|
+
finalTokens: hookResult.metadata.finalTokens,
|
|
464
|
+
tokensSaved: hookResult.metadata.tokensSaved,
|
|
465
|
+
originalContent: originalContentStr,
|
|
466
|
+
},
|
|
467
|
+
};
|
|
468
|
+
// Always return JSON string to preserve metadata
|
|
469
|
+
return JSON.stringify(outputWithMeta);
|
|
453
470
|
}
|
|
454
471
|
// No compaction happened, count original size
|
|
455
472
|
cumulativeToolOutputTokens += outputTokens;
|
|
@@ -1037,6 +1054,40 @@ export class LangchainAgent {
|
|
|
1037
1054
|
_meta: { messageId: req.messageId },
|
|
1038
1055
|
});
|
|
1039
1056
|
// Buffer tool output separately
|
|
1057
|
+
// Check if the content contains compaction metadata and extract it
|
|
1058
|
+
let rawOutput = {
|
|
1059
|
+
content: aiMessage.content,
|
|
1060
|
+
};
|
|
1061
|
+
let compactionMeta;
|
|
1062
|
+
try {
|
|
1063
|
+
const parsed = JSON.parse(aiMessage.content);
|
|
1064
|
+
if (typeof parsed === "object" &&
|
|
1065
|
+
parsed !== null &&
|
|
1066
|
+
"_compactionMeta" in parsed) {
|
|
1067
|
+
// Extract compaction metadata to top level of rawOutput
|
|
1068
|
+
const { _compactionMeta, ...contentWithoutMeta } = parsed;
|
|
1069
|
+
compactionMeta = _compactionMeta;
|
|
1070
|
+
rawOutput = {
|
|
1071
|
+
content: JSON.stringify(contentWithoutMeta),
|
|
1072
|
+
_compactionMeta,
|
|
1073
|
+
};
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
1076
|
+
catch {
|
|
1077
|
+
// Not valid JSON, use original content
|
|
1078
|
+
}
|
|
1079
|
+
// For content display, use cleaned version if compaction occurred
|
|
1080
|
+
let displayContent = aiMessage.content;
|
|
1081
|
+
if (compactionMeta) {
|
|
1082
|
+
try {
|
|
1083
|
+
const parsed = JSON.parse(aiMessage.content);
|
|
1084
|
+
const { _compactionMeta: _, ...cleanParsed } = parsed;
|
|
1085
|
+
displayContent = JSON.stringify(cleanParsed);
|
|
1086
|
+
}
|
|
1087
|
+
catch {
|
|
1088
|
+
// Keep original if parsing fails
|
|
1089
|
+
}
|
|
1090
|
+
}
|
|
1040
1091
|
pendingToolCallNotifications.push({
|
|
1041
1092
|
sessionUpdate: "tool_output",
|
|
1042
1093
|
toolCallId: aiMessage.tool_call_id,
|
|
@@ -1045,11 +1096,11 @@ export class LangchainAgent {
|
|
|
1045
1096
|
type: "content",
|
|
1046
1097
|
content: {
|
|
1047
1098
|
type: "text",
|
|
1048
|
-
text:
|
|
1099
|
+
text: displayContent,
|
|
1049
1100
|
},
|
|
1050
1101
|
},
|
|
1051
1102
|
],
|
|
1052
|
-
rawOutput
|
|
1103
|
+
rawOutput,
|
|
1053
1104
|
_meta: { messageId: req.messageId },
|
|
1054
1105
|
});
|
|
1055
1106
|
// Flush tool outputs after buffering
|
|
@@ -1119,11 +1170,11 @@ const modelRequestSchema = z.object({
|
|
|
1119
1170
|
messages: z.array(z.any()),
|
|
1120
1171
|
}),
|
|
1121
1172
|
});
|
|
1122
|
-
const makeMcpToolsClient = (mcpConfigs) => {
|
|
1123
|
-
const mcpServers = mcpConfigs
|
|
1173
|
+
const makeMcpToolsClient = async (mcpConfigs) => {
|
|
1174
|
+
const mcpServers = await Promise.all((mcpConfigs ?? []).map(async (config) => {
|
|
1124
1175
|
if (typeof config === "string") {
|
|
1125
1176
|
// String configs use the centralized MCP proxy with auth
|
|
1126
|
-
const shedAuth =
|
|
1177
|
+
const shedAuth = await ensureAuthenticated();
|
|
1127
1178
|
if (!shedAuth) {
|
|
1128
1179
|
throw new Error("Not logged in. Run 'town login' or set SHED_API_KEY to use cloud MCP servers.");
|
|
1129
1180
|
}
|
|
@@ -1155,7 +1206,7 @@ const makeMcpToolsClient = (mcpConfigs) => {
|
|
|
1155
1206
|
args: config.args ?? [],
|
|
1156
1207
|
},
|
|
1157
1208
|
];
|
|
1158
|
-
});
|
|
1209
|
+
}));
|
|
1159
1210
|
const client = new MultiServerMCPClient({
|
|
1160
1211
|
// Global tool configuration options
|
|
1161
1212
|
// Whether to throw on errors if a tool fails to load (optional, default: true)
|
|
@@ -177,9 +177,6 @@ async function deleteFromSupabase(storageKey) {
|
|
|
177
177
|
throw new Error(`Failed to delete from Supabase Storage: ${error.message}`);
|
|
178
178
|
}
|
|
179
179
|
}
|
|
180
|
-
/**
|
|
181
|
-
* List files in Supabase Storage with optional prefix and recursion
|
|
182
|
-
*/
|
|
183
180
|
async function listFilesInSupabase(sessionId, relativePath, recursive = false) {
|
|
184
181
|
const supabase = getSupabaseClient();
|
|
185
182
|
const bucket = getBucketName();
|
|
@@ -426,9 +423,6 @@ const artifactsUrl = tool(async ({ session_id, path, expires_in = 3600, }) => {
|
|
|
426
423
|
.describe("Expiration time in seconds (1-31536000). Default: 3600 (1 hour)"),
|
|
427
424
|
}),
|
|
428
425
|
});
|
|
429
|
-
// ============================================================================
|
|
430
|
-
// Tool Metadata
|
|
431
|
-
// ============================================================================
|
|
432
426
|
// Add metadata for UI display
|
|
433
427
|
artifactsCp.prettyName = "Copy Artifact";
|
|
434
428
|
artifactsCp.icon = "Upload";
|
|
@@ -437,20 +431,23 @@ artifactsCp.verbiage = {
|
|
|
437
431
|
past: "Copied artifact to {destination}",
|
|
438
432
|
paramKey: "destination",
|
|
439
433
|
};
|
|
440
|
-
artifactsDel.prettyName =
|
|
434
|
+
artifactsDel.prettyName =
|
|
435
|
+
"Delete Artifact";
|
|
441
436
|
artifactsDel.icon = "Trash";
|
|
442
437
|
artifactsDel.verbiage = {
|
|
443
438
|
active: "Deleting artifact {path}",
|
|
444
439
|
past: "Deleted artifact {path}",
|
|
445
440
|
paramKey: "path",
|
|
446
441
|
};
|
|
447
|
-
artifactsLs.prettyName =
|
|
442
|
+
artifactsLs.prettyName =
|
|
443
|
+
"List Artifacts";
|
|
448
444
|
artifactsLs.icon = "List";
|
|
449
445
|
artifactsLs.verbiage = {
|
|
450
446
|
active: "Listing artifacts",
|
|
451
447
|
past: "Listed artifacts",
|
|
452
448
|
};
|
|
453
|
-
artifactsUrl.prettyName =
|
|
449
|
+
artifactsUrl.prettyName =
|
|
450
|
+
"Generate Artifact URL";
|
|
454
451
|
artifactsUrl.icon = "Link";
|
|
455
452
|
artifactsUrl.verbiage = {
|
|
456
453
|
active: "Generating URL for {path}",
|