@townco/agent 0.1.50 → 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.
- package/dist/acp-server/adapter.d.ts +10 -0
- package/dist/acp-server/adapter.js +287 -80
- package/dist/acp-server/http.js +8 -1
- package/dist/acp-server/session-storage.d.ts +17 -3
- package/dist/acp-server/session-storage.js +9 -0
- package/dist/definition/index.d.ts +16 -4
- package/dist/definition/index.js +17 -4
- package/dist/index.js +1 -1
- package/dist/runner/agent-runner.d.ts +10 -2
- package/dist/runner/agent-runner.js +4 -0
- package/dist/runner/hooks/executor.d.ts +17 -0
- package/dist/runner/hooks/executor.js +66 -0
- package/dist/runner/hooks/predefined/compaction-tool.js +9 -1
- package/dist/runner/hooks/predefined/tool-response-compactor.d.ts +6 -0
- package/dist/runner/hooks/predefined/tool-response-compactor.js +461 -0
- package/dist/runner/hooks/registry.js +2 -0
- package/dist/runner/hooks/types.d.ts +39 -3
- package/dist/runner/hooks/types.js +9 -4
- package/dist/runner/langchain/index.js +95 -76
- package/dist/scaffold/link-local.d.ts +1 -0
- package/dist/scaffold/link-local.js +54 -0
- package/dist/scaffold/project-scaffold.js +1 -0
- package/dist/templates/index.d.ts +7 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/utils/context-size-calculator.d.ts +29 -0
- package/dist/utils/context-size-calculator.js +78 -0
- package/dist/utils/index.d.ts +2 -0
- package/dist/utils/index.js +2 -0
- package/dist/utils/token-counter.d.ts +19 -0
- package/dist/utils/token-counter.js +44 -0
- package/index.ts +1 -1
- package/package.json +7 -6
- package/templates/index.ts +18 -6
|
@@ -3,7 +3,7 @@ import type { SessionMessage } from "../agent-runner";
|
|
|
3
3
|
/**
|
|
4
4
|
* Hook types supported by the agent system
|
|
5
5
|
*/
|
|
6
|
-
export type HookType = "context_size";
|
|
6
|
+
export type HookType = "context_size" | "tool_response";
|
|
7
7
|
/**
|
|
8
8
|
* Settings for context_size hook
|
|
9
9
|
*/
|
|
@@ -13,6 +13,24 @@ export interface ContextSizeSettings {
|
|
|
13
13
|
*/
|
|
14
14
|
threshold: number;
|
|
15
15
|
}
|
|
16
|
+
/**
|
|
17
|
+
* Settings for tool_response hook
|
|
18
|
+
*/
|
|
19
|
+
export interface ToolResponseSettings {
|
|
20
|
+
/**
|
|
21
|
+
* Maximum % of main model context that tool response + current context can reach
|
|
22
|
+
* If adding the tool response would exceed this, compaction is triggered
|
|
23
|
+
* Default: 80
|
|
24
|
+
*/
|
|
25
|
+
maxContextThreshold?: number | undefined;
|
|
26
|
+
/**
|
|
27
|
+
* Maximum % of compaction model context (Haiku: 200k) that a tool response can be
|
|
28
|
+
* to attempt LLM-based compaction. Larger responses are truncated instead.
|
|
29
|
+
* The truncation limit is also this percentage.
|
|
30
|
+
* Default: 30
|
|
31
|
+
*/
|
|
32
|
+
responseTruncationThreshold?: number | undefined;
|
|
33
|
+
}
|
|
16
34
|
/**
|
|
17
35
|
* Hook configuration in agent definition
|
|
18
36
|
*/
|
|
@@ -24,7 +42,7 @@ export interface HookConfig {
|
|
|
24
42
|
/**
|
|
25
43
|
* Optional hook-specific settings
|
|
26
44
|
*/
|
|
27
|
-
setting?: ContextSizeSettings | undefined;
|
|
45
|
+
setting?: ContextSizeSettings | ToolResponseSettings | undefined;
|
|
28
46
|
/**
|
|
29
47
|
* Callback reference - either a predefined hook name or a file path
|
|
30
48
|
* Examples: "compaction_tool" or "./hooks/my_compaction_tool.ts"
|
|
@@ -72,6 +90,16 @@ export interface HookContext {
|
|
|
72
90
|
* The model being used
|
|
73
91
|
*/
|
|
74
92
|
model: string;
|
|
93
|
+
/**
|
|
94
|
+
* Tool response data (only for tool_response hooks)
|
|
95
|
+
*/
|
|
96
|
+
toolResponse?: {
|
|
97
|
+
toolCallId: string;
|
|
98
|
+
toolName: string;
|
|
99
|
+
toolInput: Record<string, unknown>;
|
|
100
|
+
rawOutput: Record<string, unknown>;
|
|
101
|
+
outputTokens: number;
|
|
102
|
+
};
|
|
75
103
|
}
|
|
76
104
|
/**
|
|
77
105
|
* Result returned by hook callbacks
|
|
@@ -106,7 +134,15 @@ export declare function createContextEntry(messages: Array<{
|
|
|
106
134
|
} | {
|
|
107
135
|
type: "full";
|
|
108
136
|
message: SessionMessage;
|
|
109
|
-
}>, timestamp?: string, compactedUpTo?: number,
|
|
137
|
+
}>, timestamp?: string, compactedUpTo?: number, context_size?: {
|
|
138
|
+
systemPromptTokens: number;
|
|
139
|
+
userMessagesTokens: number;
|
|
140
|
+
assistantMessagesTokens: number;
|
|
141
|
+
toolInputTokens: number;
|
|
142
|
+
toolResultsTokens: number;
|
|
143
|
+
totalEstimated: number;
|
|
144
|
+
llmReportedInputTokens?: number | undefined;
|
|
145
|
+
}): ContextEntry;
|
|
110
146
|
/**
|
|
111
147
|
* Helper function to create a full message entry for context
|
|
112
148
|
* Use this when hooks need to inject new messages into context
|
|
@@ -2,17 +2,22 @@
|
|
|
2
2
|
* Helper function to create a new context entry
|
|
3
3
|
* Use this when hooks want to create a new context snapshot
|
|
4
4
|
*/
|
|
5
|
-
export function createContextEntry(messages, timestamp, compactedUpTo,
|
|
5
|
+
export function createContextEntry(messages, timestamp, compactedUpTo, context_size) {
|
|
6
6
|
const entry = {
|
|
7
7
|
timestamp: timestamp || new Date().toISOString(),
|
|
8
8
|
messages,
|
|
9
|
+
context_size: context_size || {
|
|
10
|
+
systemPromptTokens: 0,
|
|
11
|
+
userMessagesTokens: 0,
|
|
12
|
+
assistantMessagesTokens: 0,
|
|
13
|
+
toolInputTokens: 0,
|
|
14
|
+
toolResultsTokens: 0,
|
|
15
|
+
totalEstimated: 0,
|
|
16
|
+
},
|
|
9
17
|
};
|
|
10
18
|
if (compactedUpTo !== undefined) {
|
|
11
19
|
entry.compactedUpTo = compactedUpTo;
|
|
12
20
|
}
|
|
13
|
-
if (inputTokens !== undefined) {
|
|
14
|
-
entry.inputTokens = inputTokens;
|
|
15
|
-
}
|
|
16
21
|
return entry;
|
|
17
22
|
}
|
|
18
23
|
/**
|
|
@@ -161,11 +161,103 @@ export class LangchainAgent {
|
|
|
161
161
|
if ((this.definition.mcps?.length ?? 0) > 0) {
|
|
162
162
|
enabledTools.push(...(await makeMcpToolsClient(this.definition.mcps).getTools()));
|
|
163
163
|
}
|
|
164
|
+
// Wrap tools with response compaction if hook is configured
|
|
165
|
+
const hooks = this.definition.hooks ?? [];
|
|
166
|
+
const hasToolResponseHook = hooks.some((h) => h.type === "tool_response");
|
|
167
|
+
const noSession = req.sessionMeta?.[SUBAGENT_MODE_KEY] === true; // Subagents don't have session storage
|
|
168
|
+
// Track cumulative tool output tokens in this turn for proper context calculation
|
|
169
|
+
let cumulativeToolOutputTokens = 0;
|
|
170
|
+
let wrappedTools = enabledTools;
|
|
171
|
+
if (hasToolResponseHook && !noSession) {
|
|
172
|
+
const { countToolResultTokens } = await import("../../utils/token-counter.js");
|
|
173
|
+
const { toolResponseCompactor } = await import("../hooks/predefined/tool-response-compactor.js");
|
|
174
|
+
wrappedTools = enabledTools.map((originalTool) => {
|
|
175
|
+
const wrappedFunc = async (input) => {
|
|
176
|
+
// Execute the original tool
|
|
177
|
+
const result = await originalTool.invoke(input);
|
|
178
|
+
// Check if result should be compacted
|
|
179
|
+
const resultStr = typeof result === "string" ? result : JSON.stringify(result);
|
|
180
|
+
const rawOutput = { content: resultStr };
|
|
181
|
+
const outputTokens = countToolResultTokens(rawOutput);
|
|
182
|
+
// Skip compaction for small results (under 10k tokens)
|
|
183
|
+
if (outputTokens < 10000) {
|
|
184
|
+
// Still track this in cumulative total
|
|
185
|
+
cumulativeToolOutputTokens += outputTokens;
|
|
186
|
+
return result;
|
|
187
|
+
}
|
|
188
|
+
_logger.info("Tool wrapper: compacting large tool result", {
|
|
189
|
+
toolName: originalTool.name,
|
|
190
|
+
originalTokens: outputTokens,
|
|
191
|
+
cumulativeToolOutputTokens,
|
|
192
|
+
});
|
|
193
|
+
// Calculate current context including all tool outputs so far in this turn
|
|
194
|
+
// This ensures we account for multiple large tool calls in the same turn
|
|
195
|
+
const baseContextTokens = turnTokenUsage.inputTokens || 10000;
|
|
196
|
+
const currentTokens = baseContextTokens + cumulativeToolOutputTokens;
|
|
197
|
+
const maxTokens = 200000; // Claude's limit
|
|
198
|
+
// Build proper hook context with all required fields
|
|
199
|
+
const hookContext = {
|
|
200
|
+
session: {
|
|
201
|
+
messages: req.contextMessages || [],
|
|
202
|
+
context: [],
|
|
203
|
+
requestParams: {
|
|
204
|
+
hookSettings: hooks.find((h) => h.type === "tool_response")
|
|
205
|
+
?.setting,
|
|
206
|
+
},
|
|
207
|
+
},
|
|
208
|
+
currentTokens,
|
|
209
|
+
maxTokens,
|
|
210
|
+
percentage: (currentTokens / maxTokens) * 100,
|
|
211
|
+
model: this.definition.model,
|
|
212
|
+
toolResponse: {
|
|
213
|
+
toolCallId: "pending",
|
|
214
|
+
toolName: originalTool.name,
|
|
215
|
+
toolInput: input,
|
|
216
|
+
rawOutput,
|
|
217
|
+
outputTokens,
|
|
218
|
+
},
|
|
219
|
+
};
|
|
220
|
+
// Call the tool response compactor directly
|
|
221
|
+
const hookResult = await toolResponseCompactor(hookContext);
|
|
222
|
+
// Extract modified output from metadata
|
|
223
|
+
if (hookResult?.metadata?.modifiedOutput) {
|
|
224
|
+
const modifiedOutput = hookResult.metadata
|
|
225
|
+
.modifiedOutput;
|
|
226
|
+
const compactedTokens = countToolResultTokens(modifiedOutput);
|
|
227
|
+
// Update cumulative total with the compacted size (not original!)
|
|
228
|
+
cumulativeToolOutputTokens += compactedTokens;
|
|
229
|
+
_logger.info("Tool wrapper: compaction complete", {
|
|
230
|
+
toolName: originalTool.name,
|
|
231
|
+
originalTokens: outputTokens,
|
|
232
|
+
compactedTokens,
|
|
233
|
+
reduction: `${((1 - compactedTokens / outputTokens) * 100).toFixed(1)}%`,
|
|
234
|
+
totalCumulativeTokens: cumulativeToolOutputTokens,
|
|
235
|
+
});
|
|
236
|
+
return typeof result === "string"
|
|
237
|
+
? modifiedOutput.content
|
|
238
|
+
: JSON.stringify(modifiedOutput);
|
|
239
|
+
}
|
|
240
|
+
// No compaction happened, count original size
|
|
241
|
+
cumulativeToolOutputTokens += outputTokens;
|
|
242
|
+
return result;
|
|
243
|
+
};
|
|
244
|
+
// Create new tool with wrapped function
|
|
245
|
+
const wrappedTool = tool(wrappedFunc, {
|
|
246
|
+
name: originalTool.name,
|
|
247
|
+
description: originalTool.description,
|
|
248
|
+
schema: originalTool.schema,
|
|
249
|
+
});
|
|
250
|
+
// Preserve metadata
|
|
251
|
+
wrappedTool.prettyName = originalTool.prettyName;
|
|
252
|
+
wrappedTool.icon = originalTool.icon;
|
|
253
|
+
return wrappedTool;
|
|
254
|
+
});
|
|
255
|
+
}
|
|
164
256
|
// Filter tools if running in subagent mode
|
|
165
257
|
const isSubagent = req.sessionMeta?.[SUBAGENT_MODE_KEY] === true;
|
|
166
258
|
const finalTools = isSubagent
|
|
167
|
-
?
|
|
168
|
-
:
|
|
259
|
+
? wrappedTools.filter((t) => t.name !== TODO_WRITE_TOOL_NAME && t.name !== TASK_TOOL_NAME)
|
|
260
|
+
: wrappedTools;
|
|
169
261
|
// Create the model instance using the factory
|
|
170
262
|
// This detects the provider from the model string:
|
|
171
263
|
// - "gemini-2.0-flash" → Google Generative AI
|
|
@@ -187,79 +279,6 @@ export class LangchainAgent {
|
|
|
187
279
|
const agent = createAgent(agentConfig);
|
|
188
280
|
// Add logging callbacks for model requests
|
|
189
281
|
const provider = detectProvider(this.definition.model);
|
|
190
|
-
const loggingCallback = {
|
|
191
|
-
handleChatModelStart: async (_llm, messages, runId, parentRunId, extraParams) => {
|
|
192
|
-
_logger.info("Model request started", {
|
|
193
|
-
provider,
|
|
194
|
-
model: this.definition.model,
|
|
195
|
-
runId,
|
|
196
|
-
parentRunId,
|
|
197
|
-
messageCount: messages.length,
|
|
198
|
-
extraParams,
|
|
199
|
-
});
|
|
200
|
-
},
|
|
201
|
-
handleLLMEnd: async (output, runId, parentRunId, tags, extraParams) => {
|
|
202
|
-
// Extract token usage from output
|
|
203
|
-
const llmResult = output;
|
|
204
|
-
_logger.info("Model request completed", {
|
|
205
|
-
provider,
|
|
206
|
-
model: this.definition.model,
|
|
207
|
-
runId,
|
|
208
|
-
parentRunId,
|
|
209
|
-
tags,
|
|
210
|
-
tokenUsage: llmResult.llmOutput?.tokenUsage,
|
|
211
|
-
generationCount: llmResult.generations?.length,
|
|
212
|
-
extraParams,
|
|
213
|
-
});
|
|
214
|
-
},
|
|
215
|
-
handleLLMError: async (error, runId, parentRunId, tags) => {
|
|
216
|
-
_logger.error("Model request failed", {
|
|
217
|
-
provider,
|
|
218
|
-
model: this.definition.model,
|
|
219
|
-
runId,
|
|
220
|
-
parentRunId,
|
|
221
|
-
tags,
|
|
222
|
-
error: error.message,
|
|
223
|
-
stack: error.stack,
|
|
224
|
-
});
|
|
225
|
-
},
|
|
226
|
-
handleToolStart: async (_tool, input, runId, parentRunId, tags, metadata, runName) => {
|
|
227
|
-
if (process.env.DEBUG_TELEMETRY === "true") {
|
|
228
|
-
console.log(`[handleToolStart] runId=${runId}, runName=${runName}, parentRunId=${parentRunId}`);
|
|
229
|
-
console.log(`[handleToolStart] Active context span:`, trace.getSpan(context.active())?.spanContext());
|
|
230
|
-
}
|
|
231
|
-
_logger.info("Tool started", {
|
|
232
|
-
runId,
|
|
233
|
-
parentRunId,
|
|
234
|
-
runName,
|
|
235
|
-
tags,
|
|
236
|
-
metadata,
|
|
237
|
-
input: input.substring(0, 200), // Truncate for logging
|
|
238
|
-
});
|
|
239
|
-
},
|
|
240
|
-
handleToolEnd: async (_output, runId, parentRunId, tags) => {
|
|
241
|
-
if (process.env.DEBUG_TELEMETRY === "true") {
|
|
242
|
-
console.log(`[handleToolEnd] runId=${runId}, parentRunId=${parentRunId}`);
|
|
243
|
-
}
|
|
244
|
-
_logger.info("Tool completed", {
|
|
245
|
-
runId,
|
|
246
|
-
parentRunId,
|
|
247
|
-
tags,
|
|
248
|
-
});
|
|
249
|
-
},
|
|
250
|
-
handleToolError: async (error, runId, parentRunId, tags) => {
|
|
251
|
-
if (process.env.DEBUG_TELEMETRY === "true") {
|
|
252
|
-
console.log(`[handleToolError] runId=${runId}, error=${error.message}`);
|
|
253
|
-
}
|
|
254
|
-
_logger.error("Tool failed", {
|
|
255
|
-
runId,
|
|
256
|
-
parentRunId,
|
|
257
|
-
tags,
|
|
258
|
-
error: error.message,
|
|
259
|
-
stack: error.stack,
|
|
260
|
-
});
|
|
261
|
-
},
|
|
262
|
-
};
|
|
263
282
|
// Build messages from context history if available, otherwise use just the prompt
|
|
264
283
|
let messages;
|
|
265
284
|
if (req.contextMessages && req.contextMessages.length > 0) {
|
|
@@ -303,7 +322,7 @@ export class LangchainAgent {
|
|
|
303
322
|
const stream = context.with(invocationContext, () => agent.stream({ messages }, {
|
|
304
323
|
streamMode: ["updates", "messages"],
|
|
305
324
|
recursionLimit: 200,
|
|
306
|
-
callbacks: [
|
|
325
|
+
callbacks: [otelCallbacks],
|
|
307
326
|
}));
|
|
308
327
|
for await (const streamItem of await stream) {
|
|
309
328
|
const [streamMode, chunk] = streamItem;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function linkLocalPackages(projectPath: string): Promise<void>;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { exists } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { $ } from "bun";
|
|
4
|
+
const PACKAGE_PATHS = {
|
|
5
|
+
"@townco/ui": "packages/ui",
|
|
6
|
+
"@townco/core": "packages/core",
|
|
7
|
+
"@townco/tsconfig": "packages/tsconfig",
|
|
8
|
+
"@townco/tui-template": "apps/tui",
|
|
9
|
+
"@townco/gui-template": "apps/gui",
|
|
10
|
+
"@townco/secret": "packages/secret",
|
|
11
|
+
"@townco/agent": "packages/agent",
|
|
12
|
+
"@townco/cli": "apps/cli",
|
|
13
|
+
};
|
|
14
|
+
async function getMonorepoRoot() {
|
|
15
|
+
try {
|
|
16
|
+
// 1. Get git repo root
|
|
17
|
+
const result = await $ `git rev-parse --show-toplevel`.quiet();
|
|
18
|
+
const repoRoot = result.text().trim();
|
|
19
|
+
// 2. Check package.json name === "town"
|
|
20
|
+
const pkgJsonPath = join(repoRoot, "package.json");
|
|
21
|
+
const pkgJson = await Bun.file(pkgJsonPath).json();
|
|
22
|
+
if (pkgJson.name !== "town")
|
|
23
|
+
return null;
|
|
24
|
+
// 3. Check packages/agent and packages/ui exist
|
|
25
|
+
const agentExists = await exists(join(repoRoot, "packages/agent"));
|
|
26
|
+
const uiExists = await exists(join(repoRoot, "packages/ui"));
|
|
27
|
+
if (!agentExists || !uiExists)
|
|
28
|
+
return null;
|
|
29
|
+
return repoRoot;
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
export async function linkLocalPackages(projectPath) {
|
|
36
|
+
const repoRoot = await getMonorepoRoot();
|
|
37
|
+
if (!repoRoot)
|
|
38
|
+
return; // Not in monorepo, no-op
|
|
39
|
+
console.log("Detected town monorepo, linking local packages...");
|
|
40
|
+
// 1. Register each local package globally
|
|
41
|
+
for (const [, localPath] of Object.entries(PACKAGE_PATHS)) {
|
|
42
|
+
const pkgPath = join(repoRoot, localPath);
|
|
43
|
+
await $ `bun link`.cwd(pkgPath).quiet();
|
|
44
|
+
}
|
|
45
|
+
// 2. Parse project's package.json for @townco/* deps
|
|
46
|
+
const pkgJson = await Bun.file(join(projectPath, "package.json")).json();
|
|
47
|
+
const deps = { ...pkgJson.dependencies, ...pkgJson.devDependencies };
|
|
48
|
+
const towncoPackages = Object.keys(deps).filter((name) => name.startsWith("@townco/"));
|
|
49
|
+
// 3. Link each package in the project
|
|
50
|
+
for (const pkgName of towncoPackages) {
|
|
51
|
+
await $ `bun link ${pkgName}`.cwd(projectPath);
|
|
52
|
+
console.log(`Linked ${pkgName}`);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
@@ -21,6 +21,7 @@ function generateProjectPackageJson() {
|
|
|
21
21
|
"@radix-ui/react-select": "^2.2.6",
|
|
22
22
|
"@radix-ui/react-slot": "^1.2.4",
|
|
23
23
|
"@radix-ui/react-tabs": "^1.1.13",
|
|
24
|
+
"@townco/core": "~0.0.23",
|
|
24
25
|
"@townco/ui": "^0.1.0",
|
|
25
26
|
"@townco/agent": "^0.1.20",
|
|
26
27
|
"class-variance-authority": "^0.7.1",
|
|
@@ -23,6 +23,13 @@ export interface TemplateVars {
|
|
|
23
23
|
threshold: number;
|
|
24
24
|
} | undefined;
|
|
25
25
|
callback: string;
|
|
26
|
+
} | {
|
|
27
|
+
type: "tool_response";
|
|
28
|
+
setting?: {
|
|
29
|
+
maxContextThreshold?: number | undefined;
|
|
30
|
+
responseTruncationThreshold?: number | undefined;
|
|
31
|
+
} | undefined;
|
|
32
|
+
callback: string;
|
|
26
33
|
}> | undefined;
|
|
27
34
|
}
|
|
28
35
|
export declare function getTemplateVars(name: string, definition: AgentDefinition): TemplateVars;
|