@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.
- package/dist/acp-server/adapter.d.ts +15 -0
- package/dist/acp-server/adapter.js +445 -67
- package/dist/acp-server/http.js +8 -1
- package/dist/acp-server/session-storage.d.ts +19 -0
- 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.d.ts +2 -1
- package/dist/index.js +10 -1
- package/dist/runner/agent-runner.d.ts +13 -2
- package/dist/runner/agent-runner.js +4 -0
- package/dist/runner/hooks/executor.d.ts +18 -1
- package/dist/runner/hooks/executor.js +74 -62
- package/dist/runner/hooks/predefined/compaction-tool.js +19 -3
- 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 -1
- package/dist/runner/langchain/index.d.ts +1 -0
- package/dist/runner/langchain/index.js +523 -321
- package/dist/runner/langchain/model-factory.js +1 -1
- package/dist/runner/langchain/otel-callbacks.d.ts +18 -0
- package/dist/runner/langchain/otel-callbacks.js +123 -0
- package/dist/runner/langchain/tools/subagent.js +21 -1
- 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/telemetry/index.d.ts +83 -0
- package/dist/telemetry/index.js +172 -0
- package/dist/telemetry/setup.d.ts +22 -0
- package/dist/telemetry/setup.js +141 -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 +16 -1
- package/package.json +24 -7
- package/templates/index.ts +18 -6
package/dist/acp-server/http.js
CHANGED
|
@@ -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>;
|
package/dist/definition/index.js
CHANGED
|
@@ -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
|
-
.
|
|
60
|
-
|
|
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:
|
|
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
|
|
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: ${
|
|
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: ${
|
|
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
|
-
|
|
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
|
-
|
|
116
|
+
tokensBeforeCompaction: inputTokensUsed,
|
|
117
|
+
tokensSaved: inputTokensUsed - summaryTokens,
|
|
118
|
+
summaryTokens, // Token count of the summary itself
|
|
103
119
|
summaryGenerated: true,
|
|
104
120
|
},
|
|
105
121
|
};
|