@townco/agent 0.1.77 → 0.1.79

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.
@@ -1,7 +1,7 @@
1
1
  import * as acp from "@agentclientprotocol/sdk";
2
2
  import { context, trace } from "@opentelemetry/api";
3
3
  import { createLogger } from "../logger.js";
4
- import { HookExecutor, loadHookCallback } from "../runner/hooks";
4
+ import { getModelContextWindow, HookExecutor, loadHookCallback, } from "../runner/hooks";
5
5
  import { telemetry } from "../telemetry/index.js";
6
6
  import { calculateContextSize, } from "../utils/context-size-calculator.js";
7
7
  import { countToolResultTokens } from "../utils/token-counter.js";
@@ -278,16 +278,18 @@ export class AgentAcpAdapter {
278
278
  return response;
279
279
  }
280
280
  async newSession(params) {
281
+ // Generate a unique session ID for this session
281
282
  const sessionId = Math.random().toString(36).substring(2);
282
283
  // Extract configOverrides from _meta if provided (Town Hall comparison feature)
283
284
  const configOverrides = params._meta?.configOverrides;
284
- this.sessions.set(sessionId, {
285
+ const sessionData = {
285
286
  pendingPrompt: null,
286
287
  messages: [],
287
288
  context: [],
288
289
  requestParams: params,
289
290
  configOverrides,
290
- });
291
+ };
292
+ this.sessions.set(sessionId, sessionData);
291
293
  // Note: Initial message is sent by the HTTP transport when SSE connection is established
292
294
  // This ensures the message is delivered after the client is ready to receive it
293
295
  return {
@@ -599,7 +601,9 @@ export class AgentAcpAdapter {
599
601
  // Calculate context size - no LLM call yet, so only estimated values
600
602
  const context_size = calculateContextSize(contextMessages, this.agent.definition.systemPrompt ?? undefined, undefined, // No LLM-reported tokens yet
601
603
  this.currentToolOverheadTokens, // Include tool overhead
602
- this.currentMcpOverheadTokens);
604
+ this.currentMcpOverheadTokens, // Include MCP overhead
605
+ getModelContextWindow(this.agent.definition.model), // Model context window for UI
606
+ true);
603
607
  const contextSnapshot = createContextSnapshot(session.messages.length - 1, // Exclude the newly added user message (it will be passed separately via prompt)
604
608
  new Date().toISOString(), previousContext, context_size);
605
609
  session.context.push(contextSnapshot);
@@ -826,6 +830,11 @@ export class AgentAcpAdapter {
826
830
  toolCallBlock.status =
827
831
  updateMsg.status;
828
832
  }
833
+ // Update rawInput if provided (for preliminary -> full tool call flow)
834
+ if (updateMsg.rawInput &&
835
+ Object.keys(updateMsg.rawInput).length > 0) {
836
+ toolCallBlock.rawInput = updateMsg.rawInput;
837
+ }
829
838
  if (updateMsg.rawOutput) {
830
839
  toolCallBlock.rawOutput = updateMsg.rawOutput;
831
840
  }
@@ -903,11 +912,42 @@ export class AgentAcpAdapter {
903
912
  const hooks = this.agent.definition.hooks ?? [];
904
913
  if (hooks.some((h) => h.type === "tool_response")) {
905
914
  const latestContext = session.context[session.context.length - 1];
906
- const currentContextTokens = latestContext?.context_size.llmReportedInputTokens ??
907
- latestContext?.context_size.totalEstimated ??
908
- 0;
915
+ // Use max of estimated and LLM-reported tokens (same logic as UI)
916
+ const currentContextTokens = Math.max(latestContext?.context_size.totalEstimated ?? 0, latestContext?.context_size.llmReportedInputTokens ?? 0);
917
+ // Send the context size to the UI so it shows the same value we're using for hook evaluation
918
+ if (latestContext?.context_size) {
919
+ const contextSizeForUI = {
920
+ ...latestContext.context_size,
921
+ totalEstimated: currentContextTokens,
922
+ };
923
+ this.connection.sessionUpdate({
924
+ sessionId: params.sessionId,
925
+ update: {
926
+ sessionUpdate: "agent_message_chunk",
927
+ content: {
928
+ type: "text",
929
+ text: "",
930
+ },
931
+ _meta: {
932
+ context_size: contextSizeForUI,
933
+ },
934
+ },
935
+ });
936
+ }
909
937
  const outputTokens = countToolResultTokens(rawOutput);
910
- const hookExecutor = new HookExecutor(hooks, this.agent.definition.model, (callbackRef) => loadHookCallback(callbackRef, this.agentDir));
938
+ // Create notification callback to stream hook events in real-time
939
+ const sendHookNotification = (notification) => {
940
+ this.connection.sessionUpdate({
941
+ sessionId: params.sessionId,
942
+ update: {
943
+ sessionUpdate: "hook_notification",
944
+ id: `hook_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`,
945
+ notification,
946
+ messageId,
947
+ },
948
+ });
949
+ };
950
+ const hookExecutor = new HookExecutor(hooks, this.agent.definition.model, (callbackRef) => loadHookCallback(callbackRef, this.agentDir), sendHookNotification);
911
951
  const hookResult = await hookExecutor.executeToolResponseHooks({
912
952
  messages: session.messages,
913
953
  context: session.context,
@@ -919,6 +959,8 @@ export class AgentAcpAdapter {
919
959
  rawOutput,
920
960
  outputTokens,
921
961
  });
962
+ // Note: Notifications are now sent in real-time via the callback
963
+ // The hookResult.notifications array is kept for backwards compatibility
922
964
  // Apply modifications if hook returned them
923
965
  if (hookResult.modifiedOutput) {
924
966
  rawOutput = hookResult.modifiedOutput;
@@ -1019,7 +1061,8 @@ export class AgentAcpAdapter {
1019
1061
  // Calculate context size - tool result is now in the message, but hasn't been sent to LLM yet
1020
1062
  const context_size = calculateContextSize(contextMessages, this.agent.definition.systemPrompt ?? undefined, undefined, // Tool result hasn't been sent to LLM yet, so no new LLM-reported tokens
1021
1063
  this.currentToolOverheadTokens, // Include tool overhead
1022
- this.currentMcpOverheadTokens);
1064
+ this.currentMcpOverheadTokens, // Include MCP overhead
1065
+ getModelContextWindow(this.agent.definition.model));
1023
1066
  // Create snapshot with a pointer to the partial message (not a full copy!)
1024
1067
  const midTurnSnapshot = {
1025
1068
  timestamp: new Date().toISOString(),
@@ -1172,12 +1215,26 @@ export class AgentAcpAdapter {
1172
1215
  }
1173
1216
  }
1174
1217
  // 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)
1175
1220
  const context_size = calculateContextSize(contextMessages, this.agent.definition.systemPrompt ?? undefined, turnTokenUsage.inputTokens, // Final LLM-reported tokens from this turn
1176
1221
  this.currentToolOverheadTokens, // Include tool overhead
1177
- this.currentMcpOverheadTokens);
1222
+ this.currentMcpOverheadTokens, // Include MCP overhead
1223
+ getModelContextWindow(this.agent.definition.model), // Model context window for UI
1224
+ true);
1178
1225
  const contextSnapshot = createContextSnapshot(session.messages.length, new Date().toISOString(), previousContext, context_size);
1179
1226
  session.context.push(contextSnapshot);
1180
1227
  await this.saveSessionToDisk(params.sessionId, session);
1228
+ // Send final context_size to UI (with tool results excluded)
1229
+ // This ensures the UI shows the correct context size at turn-end
1230
+ this.connection.sessionUpdate({
1231
+ sessionId: params.sessionId,
1232
+ update: {
1233
+ sessionUpdate: "agent_message_chunk",
1234
+ content: { type: "text", text: "" },
1235
+ _meta: { context_size },
1236
+ },
1237
+ });
1181
1238
  }
1182
1239
  session.pendingPrompt = null;
1183
1240
  return {
@@ -1222,7 +1279,18 @@ export class AgentAcpAdapter {
1222
1279
  contextEntries: session.context.length,
1223
1280
  totalMessages: session.messages.length,
1224
1281
  });
1225
- const hookExecutor = new HookExecutor(hooks, this.agent.definition.model, (callbackRef) => loadHookCallback(callbackRef, this.agentDir));
1282
+ // Create notification callback to stream hook events in real-time
1283
+ const sendHookNotification = (notification) => {
1284
+ this.connection.sessionUpdate({
1285
+ sessionId,
1286
+ update: {
1287
+ sessionUpdate: "hook_notification",
1288
+ id: `hook_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`,
1289
+ notification,
1290
+ },
1291
+ });
1292
+ };
1293
+ const hookExecutor = new HookExecutor(hooks, this.agent.definition.model, (callbackRef) => loadHookCallback(callbackRef, this.agentDir), sendHookNotification);
1226
1294
  // Create read-only session view for hooks
1227
1295
  const readonlySession = {
1228
1296
  messages: session.messages,
@@ -1233,23 +1301,45 @@ export class AgentAcpAdapter {
1233
1301
  const latestContext = session.context.length > 0
1234
1302
  ? session.context[session.context.length - 1]
1235
1303
  : undefined;
1236
- // Prefer LLM-reported tokens (most accurate), fall back to our estimate
1237
- const actualInputTokens = latestContext?.context_size.llmReportedInputTokens ??
1238
- latestContext?.context_size.totalEstimated ??
1239
- 0;
1304
+ // Use max of estimated and LLM-reported tokens (same logic as UI)
1305
+ // This is conservative - we want to trigger compaction earlier rather than later
1306
+ const actualInputTokens = Math.max(latestContext?.context_size.totalEstimated ?? 0, latestContext?.context_size.llmReportedInputTokens ?? 0);
1240
1307
  logger.debug("Using tokens for hook execution", {
1241
1308
  llmReported: latestContext?.context_size.llmReportedInputTokens,
1242
1309
  estimated: latestContext?.context_size.totalEstimated,
1243
1310
  used: actualInputTokens,
1244
1311
  });
1245
- const hookResult = await hookExecutor.executeHooks(readonlySession, actualInputTokens);
1246
- // Send hook notifications to client
1247
- for (const notification of hookResult.notifications) {
1312
+ // Send the context size to the UI so it shows the same value we're using for hook evaluation
1313
+ // This ensures the UI percentage matches what the hook sees
1314
+ if (latestContext?.context_size) {
1315
+ // Create an updated context_size with the actualInputTokens we computed
1316
+ // so UI can calculate the same percentage
1317
+ const contextSizeForUI = {
1318
+ ...latestContext.context_size,
1319
+ // Override with the max value we computed (what hooks actually use)
1320
+ totalEstimated: actualInputTokens,
1321
+ };
1248
1322
  this.connection.sessionUpdate({
1249
1323
  sessionId,
1250
- update: notification,
1324
+ update: {
1325
+ sessionUpdate: "agent_message_chunk",
1326
+ content: {
1327
+ type: "text",
1328
+ text: "",
1329
+ },
1330
+ _meta: {
1331
+ context_size: contextSizeForUI,
1332
+ },
1333
+ },
1334
+ });
1335
+ logger.debug("Sent context_size update to UI before hook execution", {
1336
+ actualInputTokens,
1337
+ modelContextWindow: contextSizeForUI.modelContextWindow,
1251
1338
  });
1252
1339
  }
1340
+ const hookResult = await hookExecutor.executeHooks(readonlySession, actualInputTokens);
1341
+ // Note: Notifications are now sent in real-time via the callback
1342
+ // The hookResult.notifications array is kept for backwards compatibility
1253
1343
  // Return new context entries (will be appended by caller)
1254
1344
  return hookResult.newContextEntries;
1255
1345
  }