@townco/agent 0.1.48 → 0.1.50

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.
@@ -26,5 +26,10 @@ export declare class AgentAcpAdapter implements acp.Agent {
26
26
  authenticate(_params: acp.AuthenticateRequest): Promise<acp.AuthenticateResponse | undefined>;
27
27
  setSessionMode(_params: acp.SetSessionModeRequest): Promise<acp.SetSessionModeResponse>;
28
28
  prompt(params: acp.PromptRequest): Promise<acp.PromptResponse>;
29
+ /**
30
+ * Execute hooks if configured for this agent
31
+ * Returns new context entries that should be appended to session.context
32
+ */
33
+ private executeHooksIfConfigured;
29
34
  cancel(params: acp.CancelNotification): Promise<void>;
30
35
  }
@@ -12,7 +12,7 @@ export const SUBAGENT_MODE_KEY = "town.com/isSubagent";
12
12
  * Create a context snapshot based on the previous context
13
13
  * Preserves full messages from previous context and adds new pointers
14
14
  */
15
- function createContextSnapshot(messageCount, timestamp, previousContext) {
15
+ function createContextSnapshot(messageCount, timestamp, previousContext, inputTokens) {
16
16
  const messages = [];
17
17
  if (previousContext) {
18
18
  // Start with all messages from previous context
@@ -45,7 +45,12 @@ function createContextSnapshot(messageCount, timestamp, previousContext) {
45
45
  messages.push({ type: "pointer", index: i });
46
46
  }
47
47
  }
48
- return { timestamp, messages };
48
+ return {
49
+ timestamp,
50
+ messages,
51
+ compactedUpTo: previousContext?.compactedUpTo,
52
+ inputTokens,
53
+ };
49
54
  }
50
55
  /**
51
56
  * Resolve context entries to session messages
@@ -260,7 +265,7 @@ export class AgentAcpAdapter {
260
265
  const previousContext = session.context.length > 0
261
266
  ? session.context[session.context.length - 1]
262
267
  : undefined;
263
- const contextSnapshot = createContextSnapshot(session.messages.length, new Date().toISOString(), previousContext);
268
+ const contextSnapshot = createContextSnapshot(session.messages.length, new Date().toISOString(), previousContext, previousContext?.inputTokens ?? 0);
264
269
  session.context.push(contextSnapshot);
265
270
  }
266
271
  // Build ordered content blocks for the assistant response
@@ -273,48 +278,31 @@ export class AgentAcpAdapter {
273
278
  pendingText = "";
274
279
  }
275
280
  };
281
+ // Declare agentResponse and turnTokenUsage outside try block so they're accessible after catch
282
+ let agentResponse;
283
+ // Track accumulated token usage during the turn
284
+ const turnTokenUsage = {
285
+ inputTokens: 0,
286
+ outputTokens: 0,
287
+ totalTokens: 0,
288
+ };
276
289
  try {
277
- // Execute hooks before agent invocation
278
- const hooks = this.agent.definition.hooks;
279
- logger.info("Checking hooks", {
280
- noSession: this.noSession,
281
- hasHooks: !!hooks,
282
- hooksLength: hooks?.length ?? 0,
283
- contextEntries: session.context.length,
284
- totalMessages: session.messages.length,
285
- });
286
- if (!this.noSession && hooks && hooks.length > 0) {
287
- logger.info("Executing hooks before agent invocation");
288
- const hookExecutor = new HookExecutor(hooks, this.agent.definition.model, (callbackRef) => loadHookCallback(callbackRef, this.agentDir));
289
- // Create read-only session view for hooks
290
- const readonlySession = {
291
- messages: session.messages,
292
- context: session.context,
293
- requestParams: session.requestParams,
294
- };
295
- const hookResult = await hookExecutor.executeHooks(readonlySession);
296
- // Send hook notifications to client
297
- for (const notification of hookResult.notifications) {
298
- this.connection.sessionUpdate({
299
- sessionId: params.sessionId,
300
- update: notification,
301
- });
302
- }
303
- // Append new context entries returned by hooks
304
- if (hookResult.newContextEntries.length > 0) {
305
- logger.info(`Appending ${hookResult.newContextEntries.length} new context entries from hooks`);
306
- session.context.push(...hookResult.newContextEntries);
307
- // Save session immediately after hooks to persist compacted context
308
- if (this.storage) {
309
- try {
310
- await this.storage.saveSession(params.sessionId, session.messages, session.context);
311
- logger.info("Session saved after hook execution with new context entries");
312
- }
313
- catch (error) {
314
- logger.error(`Failed to save session ${params.sessionId} after hook execution`, {
315
- error: error instanceof Error ? error.message : String(error),
316
- });
317
- }
290
+ // Execute hooks before agent invocation (turn start)
291
+ const turnStartContextEntries = await this.executeHooksIfConfigured(session, params.sessionId, "turn_start");
292
+ // Append new context entries returned by hooks (e.g., compaction)
293
+ if (turnStartContextEntries.length > 0) {
294
+ logger.info(`Appending ${turnStartContextEntries.length} new context entries from turn_start hooks`);
295
+ session.context.push(...turnStartContextEntries);
296
+ // Save session immediately after hooks to persist compacted context
297
+ if (this.storage) {
298
+ try {
299
+ await this.storage.saveSession(params.sessionId, session.messages, session.context);
300
+ logger.info("Session saved after turn_start hook execution with new context entries");
301
+ }
302
+ catch (error) {
303
+ logger.error(`Failed to save session ${params.sessionId} after turn_start hook execution`, {
304
+ error: error instanceof Error ? error.message : String(error),
305
+ });
318
306
  }
319
307
  }
320
308
  }
@@ -333,7 +321,30 @@ export class AgentAcpAdapter {
333
321
  if (session.requestParams._meta) {
334
322
  invokeParams.sessionMeta = session.requestParams._meta;
335
323
  }
336
- for await (const msg of this.agent.invoke(invokeParams)) {
324
+ const generator = this.agent.invoke(invokeParams);
325
+ // Manually iterate to capture the return value
326
+ let iterResult = await generator.next();
327
+ while (!iterResult.done) {
328
+ const msg = iterResult.value;
329
+ // Extract and accumulate token usage from message chunks
330
+ if ("sessionUpdate" in msg &&
331
+ msg.sessionUpdate === "agent_message_chunk" &&
332
+ "_meta" in msg &&
333
+ msg._meta &&
334
+ typeof msg._meta === "object" &&
335
+ "tokenUsage" in msg._meta) {
336
+ const tokenUsage = msg._meta.tokenUsage;
337
+ if (tokenUsage) {
338
+ // Only update inputTokens if we receive a positive value
339
+ // (subsequent messages may have inputTokens: 0 for output-only chunks)
340
+ if (tokenUsage.inputTokens !== undefined &&
341
+ tokenUsage.inputTokens > 0) {
342
+ turnTokenUsage.inputTokens = tokenUsage.inputTokens;
343
+ }
344
+ turnTokenUsage.outputTokens += tokenUsage.outputTokens ?? 0;
345
+ turnTokenUsage.totalTokens += tokenUsage.totalTokens ?? 0;
346
+ }
347
+ }
337
348
  // Accumulate text content from message chunks
338
349
  if ("sessionUpdate" in msg &&
339
350
  msg.sessionUpdate === "agent_message_chunk") {
@@ -407,15 +418,125 @@ export class AgentAcpAdapter {
407
418
  }
408
419
  // Note: content blocks are handled by the transport for display
409
420
  // We store the raw output here for session persistence
421
+ // Create mid-turn context snapshot after tool completes
422
+ if (!this.noSession) {
423
+ flushPendingText(); // Ensure all text is captured
424
+ // Update or create the partial assistant message in the messages array
425
+ const partialAssistantMessage = {
426
+ role: "assistant",
427
+ content: [...contentBlocks], // Clone current content blocks
428
+ timestamp: new Date().toISOString(),
429
+ };
430
+ // Check if we already have a partial assistant message in messages
431
+ const lastMessage = session.messages[session.messages.length - 1];
432
+ let partialMessageIndex;
433
+ if (lastMessage && lastMessage.role === "assistant") {
434
+ // Update existing partial message
435
+ session.messages[session.messages.length - 1] =
436
+ partialAssistantMessage;
437
+ partialMessageIndex = session.messages.length - 1;
438
+ }
439
+ else {
440
+ // Add new partial message
441
+ session.messages.push(partialAssistantMessage);
442
+ partialMessageIndex = session.messages.length - 1;
443
+ }
444
+ // Get the latest context
445
+ const latestContext = session.context.length > 0
446
+ ? session.context[session.context.length - 1]
447
+ : undefined;
448
+ // Create snapshot with a pointer to the partial message (not a full copy!)
449
+ const midTurnSnapshot = {
450
+ timestamp: new Date().toISOString(),
451
+ messages: [
452
+ ...(latestContext?.messages ?? []),
453
+ { type: "pointer", index: partialMessageIndex },
454
+ ],
455
+ compactedUpTo: latestContext?.compactedUpTo,
456
+ inputTokens: turnTokenUsage.inputTokens || latestContext?.inputTokens || 0,
457
+ };
458
+ session.context.push(midTurnSnapshot);
459
+ logger.debug("Created mid-turn context snapshot after tool output", {
460
+ toolCallId: outputMsg.toolCallId,
461
+ contentBlocks: contentBlocks.length,
462
+ partialMessageIndex,
463
+ inputTokens: midTurnSnapshot.inputTokens,
464
+ });
465
+ // Execute hooks mid-turn to check if compaction is needed
466
+ const midTurnContextEntries = await this.executeHooksIfConfigured(session, params.sessionId, "mid_turn");
467
+ // Append new context entries returned by hooks (e.g., compaction)
468
+ if (midTurnContextEntries.length > 0) {
469
+ logger.info(`Appending ${midTurnContextEntries.length} new context entries from mid_turn hooks`, {
470
+ toolCallId: outputMsg.toolCallId,
471
+ });
472
+ session.context.push(...midTurnContextEntries);
473
+ // Save session immediately after mid-turn compaction
474
+ if (this.storage) {
475
+ try {
476
+ await this.storage.saveSession(params.sessionId, session.messages, session.context);
477
+ logger.info("Session saved after mid_turn hook execution with new context entries", {
478
+ toolCallId: outputMsg.toolCallId,
479
+ });
480
+ }
481
+ catch (error) {
482
+ logger.error(`Failed to save session ${params.sessionId} after mid_turn hook execution`, {
483
+ toolCallId: outputMsg.toolCallId,
484
+ error: error instanceof Error
485
+ ? error.message
486
+ : String(error),
487
+ });
488
+ }
489
+ }
490
+ }
491
+ }
410
492
  }
411
493
  }
412
494
  // The agent may emit extended types (like tool_output) that aren't in ACP SDK yet
413
495
  // The http transport will handle routing these appropriately
496
+ // Add context input tokens to messages with token usage metadata
497
+ let enhancedMsg = msg;
498
+ if (!this.noSession &&
499
+ "_meta" in msg &&
500
+ msg._meta &&
501
+ typeof msg._meta === "object" &&
502
+ "tokenUsage" in msg._meta) {
503
+ // Use accumulated turn tokens if available, otherwise fall back to previous context
504
+ let contextInputTokens;
505
+ if (turnTokenUsage.inputTokens > 0) {
506
+ // Use current turn's accumulated tokens (most up-to-date)
507
+ contextInputTokens = turnTokenUsage.inputTokens;
508
+ }
509
+ else {
510
+ // Fall back to previous context's tokens (for start of turn)
511
+ const latestContext = session.context.length > 0
512
+ ? session.context[session.context.length - 1]
513
+ : undefined;
514
+ contextInputTokens = latestContext?.inputTokens;
515
+ }
516
+ // Add context tokens to _meta only if defined
517
+ if (contextInputTokens !== undefined) {
518
+ enhancedMsg = {
519
+ ...msg,
520
+ _meta: {
521
+ ...msg._meta,
522
+ contextInputTokens,
523
+ },
524
+ };
525
+ logger.debug("Sending contextInputTokens to GUI", {
526
+ contextInputTokens,
527
+ turnTokens: turnTokenUsage.inputTokens,
528
+ previousContextTokens: session.context[session.context.length - 1]?.inputTokens,
529
+ });
530
+ }
531
+ }
414
532
  this.connection.sessionUpdate({
415
533
  sessionId: params.sessionId,
416
- update: msg,
534
+ update: enhancedMsg,
417
535
  });
536
+ iterResult = await generator.next();
418
537
  }
538
+ // Capture the return value (PromptResponse with tokenUsage)
539
+ agentResponse = iterResult.value;
419
540
  // Flush any remaining pending text
420
541
  flushPendingText();
421
542
  }
@@ -433,12 +554,24 @@ export class AgentAcpAdapter {
433
554
  content: contentBlocks,
434
555
  timestamp: new Date().toISOString(),
435
556
  };
436
- session.messages.push(assistantMessage);
557
+ // Check if we already have a partial assistant message from mid-turn updates
558
+ const lastMessage = session.messages[session.messages.length - 1];
559
+ if (lastMessage && lastMessage.role === "assistant") {
560
+ // Update the existing message instead of adding a duplicate
561
+ session.messages[session.messages.length - 1] = assistantMessage;
562
+ }
563
+ else {
564
+ // Add new message (no mid-turn updates occurred)
565
+ session.messages.push(assistantMessage);
566
+ }
437
567
  // Create context snapshot based on previous context
438
568
  const previousContext = session.context.length > 0
439
569
  ? session.context[session.context.length - 1]
440
570
  : undefined;
441
- const contextSnapshot = createContextSnapshot(session.messages.length, new Date().toISOString(), previousContext);
571
+ const contextSnapshot = createContextSnapshot(session.messages.length, new Date().toISOString(), previousContext,
572
+ // Use accumulated turn tokens (latest API call's input tokens)
573
+ // not agentResponse.tokenUsage which may sum all calls in the turn
574
+ turnTokenUsage.inputTokens || previousContext?.inputTokens || 0);
442
575
  session.context.push(contextSnapshot);
443
576
  }
444
577
  // Save session to disk if storage is configured and session persistence is enabled
@@ -457,6 +590,44 @@ export class AgentAcpAdapter {
457
590
  stopReason: "end_turn",
458
591
  };
459
592
  }
593
+ /**
594
+ * Execute hooks if configured for this agent
595
+ * Returns new context entries that should be appended to session.context
596
+ */
597
+ async executeHooksIfConfigured(session, sessionId, executionPoint) {
598
+ // Check if hooks are configured and session persistence is enabled
599
+ const hooks = this.agent.definition.hooks;
600
+ if (this.noSession || !hooks || hooks.length === 0) {
601
+ return [];
602
+ }
603
+ logger.info(`Executing hooks at ${executionPoint}`, {
604
+ hooksLength: hooks.length,
605
+ contextEntries: session.context.length,
606
+ totalMessages: session.messages.length,
607
+ });
608
+ const hookExecutor = new HookExecutor(hooks, this.agent.definition.model, (callbackRef) => loadHookCallback(callbackRef, this.agentDir));
609
+ // Create read-only session view for hooks
610
+ const readonlySession = {
611
+ messages: session.messages,
612
+ context: session.context,
613
+ requestParams: session.requestParams,
614
+ };
615
+ // Get actual input token count from latest context entry
616
+ const latestContext = session.context.length > 0
617
+ ? session.context[session.context.length - 1]
618
+ : undefined;
619
+ const actualInputTokens = latestContext?.inputTokens ?? 0;
620
+ const hookResult = await hookExecutor.executeHooks(readonlySession, actualInputTokens);
621
+ // Send hook notifications to client
622
+ for (const notification of hookResult.notifications) {
623
+ this.connection.sessionUpdate({
624
+ sessionId,
625
+ update: notification,
626
+ });
627
+ }
628
+ // Return new context entries (will be appended by caller)
629
+ return hookResult.newContextEntries;
630
+ }
460
631
  async cancel(params) {
461
632
  this.sessions.get(params.sessionId)?.pendingPrompt?.abort();
462
633
  }
@@ -50,6 +50,11 @@ export interface ContextEntry {
50
50
  * compacted into the full message(s) in this entry
51
51
  */
52
52
  compactedUpTo?: number | undefined;
53
+ /**
54
+ * Actual input token count from API for this context.
55
+ * Used for accurate context size tracking and compaction decisions.
56
+ */
57
+ inputTokens?: number | undefined;
53
58
  }
54
59
  /**
55
60
  * Session metadata
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",
@@ -64,6 +64,7 @@ export interface AgentMessageChunkWithTokens {
64
64
  };
65
65
  _meta?: {
66
66
  tokenUsage?: TokenUsage;
67
+ contextInputTokens?: number;
67
68
  [key: string]: unknown;
68
69
  };
69
70
  }
@@ -97,6 +98,8 @@ export type ExtendedSessionUpdate = (SessionNotification["update"] & {
97
98
  rawOutput?: Record<string, unknown>;
98
99
  _meta?: {
99
100
  messageId?: string;
101
+ contextInputTokens?: number;
102
+ [key: string]: unknown;
100
103
  };
101
104
  } | AgentMessageChunkWithTokens | HookNotificationUpdate;
102
105
  /** Describes an object that can run an agent definition */
@@ -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
  }>;
@@ -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 currentTokens = estimateTokens(resolvedMessages);
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: ${currentTokens} tokens (${percentage.toFixed(1)}%) < threshold ${threshold}%`);
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: ${currentTokens} tokens (${percentage.toFixed(1)}%) exceeds threshold ${threshold}%`);
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,
@@ -78,26 +78,36 @@ Please provide your summary based on the conversation above, following this stru
78
78
  ? response.content
79
79
  : Array.isArray(response.content)
80
80
  ? response.content
81
- .filter((block) => typeof block === "object" && block !== null && "text" in block)
81
+ .filter((block) => typeof block === "object" &&
82
+ block !== null &&
83
+ "text" in block)
82
84
  .map((block) => block.text)
83
85
  .join("\n")
84
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;
85
91
  logger.info("Generated compaction summary", {
86
92
  originalMessages: messagesToCompact.length,
87
93
  summaryLength: summaryText.length,
88
- estimatedTokensSaved: Math.round(ctx.currentTokens * 0.7),
94
+ inputTokens: inputTokensUsed,
95
+ summaryTokens,
96
+ tokensSaved: inputTokensUsed - summaryTokens,
89
97
  });
90
98
  // Create a new context entry with the summary
91
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}`);
92
100
  // Set compactedUpTo to indicate all messages have been compacted into the summary
93
101
  const lastMessageIndex = messagesToCompact.length - 1;
94
- const newContextEntry = createContextEntry([summaryEntry], undefined, lastMessageIndex);
102
+ const newContextEntry = createContextEntry([summaryEntry], undefined, lastMessageIndex, summaryTokens);
95
103
  return {
96
104
  newContextEntry,
97
105
  metadata: {
98
106
  action: "compacted",
99
107
  messagesRemoved: messagesToCompact.length - 1,
100
- tokensSaved: Math.round(ctx.currentTokens * 0.7),
108
+ tokensBeforeCompaction: inputTokensUsed,
109
+ tokensSaved: inputTokensUsed - summaryTokens,
110
+ summaryTokens, // Token count of the summary itself
101
111
  summaryGenerated: true,
102
112
  },
103
113
  };
@@ -106,7 +106,7 @@ export declare function createContextEntry(messages: Array<{
106
106
  } | {
107
107
  type: "full";
108
108
  message: SessionMessage;
109
- }>, timestamp?: string, compactedUpTo?: number): ContextEntry;
109
+ }>, timestamp?: string, compactedUpTo?: number, inputTokens?: number): ContextEntry;
110
110
  /**
111
111
  * Helper function to create a full message entry for context
112
112
  * Use this when hooks need to inject new messages into context
@@ -2,7 +2,7 @@
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, inputTokens) {
6
6
  const entry = {
7
7
  timestamp: timestamp || new Date().toISOString(),
8
8
  messages,
@@ -10,6 +10,9 @@ export function createContextEntry(messages, timestamp, compactedUpTo) {
10
10
  if (compactedUpTo !== undefined) {
11
11
  entry.compactedUpTo = compactedUpTo;
12
12
  }
13
+ if (inputTokens !== undefined) {
14
+ entry.inputTokens = inputTokens;
15
+ }
13
16
  return entry;
14
17
  }
15
18
  /**
@@ -10,6 +10,7 @@ type MakeLazy<T> = T extends LangchainTool ? () => T : never;
10
10
  export declare const TOOL_REGISTRY: Record<BuiltInToolType, LangchainTool | LazyLangchainTool | LazyLangchainTools>;
11
11
  export declare class LangchainAgent implements AgentRunner {
12
12
  definition: CreateAgentRunnerParams;
13
+ private toolSpans;
13
14
  constructor(params: CreateAgentRunnerParams);
14
15
  invoke(req: InvokeRequest): AsyncGenerator<ExtendedSessionUpdate, PromptResponse, undefined>;
15
16
  }