@townco/agent 0.1.50 → 0.1.52

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.
Files changed (60) hide show
  1. package/dist/acp-server/adapter.d.ts +10 -0
  2. package/dist/acp-server/adapter.js +287 -80
  3. package/dist/acp-server/cli.d.ts +1 -3
  4. package/dist/acp-server/http.js +8 -1
  5. package/dist/acp-server/index.js +5 -0
  6. package/dist/acp-server/session-storage.d.ts +17 -3
  7. package/dist/acp-server/session-storage.js +9 -0
  8. package/dist/bin.js +0 -0
  9. package/dist/check-jaeger.d.ts +5 -0
  10. package/dist/check-jaeger.js +82 -0
  11. package/dist/definition/index.d.ts +16 -4
  12. package/dist/definition/index.js +17 -4
  13. package/dist/index.js +1 -1
  14. package/dist/run-subagents.d.ts +9 -0
  15. package/dist/run-subagents.js +110 -0
  16. package/dist/runner/agent-runner.d.ts +10 -2
  17. package/dist/runner/agent-runner.js +4 -0
  18. package/dist/runner/hooks/executor.d.ts +17 -0
  19. package/dist/runner/hooks/executor.js +66 -0
  20. package/dist/runner/hooks/predefined/compaction-tool.js +9 -1
  21. package/dist/runner/hooks/predefined/tool-response-compactor.d.ts +6 -0
  22. package/dist/runner/hooks/predefined/tool-response-compactor.js +461 -0
  23. package/dist/runner/hooks/registry.js +2 -0
  24. package/dist/runner/hooks/types.d.ts +39 -3
  25. package/dist/runner/hooks/types.js +9 -4
  26. package/dist/runner/index.d.ts +1 -3
  27. package/dist/runner/langchain/custom-stream-types.d.ts +36 -0
  28. package/dist/runner/langchain/custom-stream-types.js +23 -0
  29. package/dist/runner/langchain/index.js +102 -76
  30. package/dist/runner/langchain/otel-callbacks.js +67 -1
  31. package/dist/runner/langchain/tools/bash.d.ts +14 -0
  32. package/dist/runner/langchain/tools/bash.js +135 -0
  33. package/dist/scaffold/link-local.d.ts +1 -0
  34. package/dist/scaffold/link-local.js +54 -0
  35. package/dist/scaffold/project-scaffold.js +1 -0
  36. package/dist/telemetry/setup.d.ts +3 -1
  37. package/dist/telemetry/setup.js +33 -3
  38. package/dist/templates/index.d.ts +7 -0
  39. package/dist/test-telemetry.d.ts +5 -0
  40. package/dist/test-telemetry.js +88 -0
  41. package/dist/tsconfig.tsbuildinfo +1 -1
  42. package/dist/utils/context-size-calculator.d.ts +29 -0
  43. package/dist/utils/context-size-calculator.js +78 -0
  44. package/dist/utils/index.d.ts +2 -0
  45. package/dist/utils/index.js +2 -0
  46. package/dist/utils/token-counter.d.ts +19 -0
  47. package/dist/utils/token-counter.js +44 -0
  48. package/index.ts +1 -1
  49. package/package.json +7 -6
  50. package/templates/index.ts +18 -6
  51. package/dist/definition/mcp.d.ts +0 -0
  52. package/dist/definition/mcp.js +0 -0
  53. package/dist/definition/tools/todo.d.ts +0 -49
  54. package/dist/definition/tools/todo.js +0 -80
  55. package/dist/definition/tools/web_search.d.ts +0 -4
  56. package/dist/definition/tools/web_search.js +0 -26
  57. package/dist/dev-agent/index.d.ts +0 -2
  58. package/dist/dev-agent/index.js +0 -18
  59. package/dist/example.d.ts +0 -2
  60. package/dist/example.js +0 -19
@@ -19,7 +19,17 @@ export declare class AgentAcpAdapter implements acp.Agent {
19
19
  private storage;
20
20
  private noSession;
21
21
  private agentDir;
22
+ private agentName;
23
+ private agentDisplayName;
24
+ private agentVersion;
25
+ private agentDescription;
26
+ private agentSuggestedPrompts;
22
27
  constructor(agent: AgentRunner, connection: acp.AgentSideConnection, agentDir?: string, agentName?: string);
28
+ /**
29
+ * Helper to save session to disk
30
+ * Call this after any modification to session.messages or session.context
31
+ */
32
+ private saveSessionToDisk;
23
33
  initialize(_params: acp.InitializeRequest): Promise<acp.InitializeResponse>;
24
34
  newSession(params: acp.NewSessionRequest): Promise<acp.NewSessionResponse>;
25
35
  loadSession(params: acp.LoadSessionRequest): Promise<acp.LoadSessionResponse>;
@@ -1,6 +1,8 @@
1
1
  import * as acp from "@agentclientprotocol/sdk";
2
2
  import { createLogger } from "@townco/core";
3
3
  import { HookExecutor, loadHookCallback } from "../runner/hooks";
4
+ import { calculateContextSize, } from "../utils/context-size-calculator.js";
5
+ import { countToolResultTokens } from "../utils/token-counter.js";
4
6
  import { SessionStorage, } from "./session-storage.js";
5
7
  const logger = createLogger("adapter");
6
8
  /**
@@ -12,7 +14,7 @@ export const SUBAGENT_MODE_KEY = "town.com/isSubagent";
12
14
  * Create a context snapshot based on the previous context
13
15
  * Preserves full messages from previous context and adds new pointers
14
16
  */
15
- function createContextSnapshot(messageCount, timestamp, previousContext, inputTokens) {
17
+ function createContextSnapshot(messageCount, timestamp, previousContext, context_size) {
16
18
  const messages = [];
17
19
  if (previousContext) {
18
20
  // Start with all messages from previous context
@@ -49,7 +51,14 @@ function createContextSnapshot(messageCount, timestamp, previousContext, inputTo
49
51
  timestamp,
50
52
  messages,
51
53
  compactedUpTo: previousContext?.compactedUpTo,
52
- inputTokens,
54
+ context_size: context_size || {
55
+ systemPromptTokens: 0,
56
+ userMessagesTokens: 0,
57
+ assistantMessagesTokens: 0,
58
+ toolInputTokens: 0,
59
+ toolResultsTokens: 0,
60
+ totalEstimated: 0,
61
+ },
53
62
  };
54
63
  }
55
64
  /**
@@ -89,11 +98,21 @@ export class AgentAcpAdapter {
89
98
  storage;
90
99
  noSession;
91
100
  agentDir;
101
+ agentName;
102
+ agentDisplayName;
103
+ agentVersion;
104
+ agentDescription;
105
+ agentSuggestedPrompts;
92
106
  constructor(agent, connection, agentDir, agentName) {
93
107
  this.connection = connection;
94
108
  this.sessions = new Map();
95
109
  this.agent = agent;
96
110
  this.agentDir = agentDir;
111
+ this.agentName = agentName;
112
+ this.agentDisplayName = agent.definition.displayName;
113
+ this.agentVersion = agent.definition.version;
114
+ this.agentDescription = agent.definition.description;
115
+ this.agentSuggestedPrompts = agent.definition.suggestedPrompts;
97
116
  this.noSession = process.env.TOWN_NO_SESSION === "true";
98
117
  this.storage =
99
118
  agentDir && agentName && !this.noSession
@@ -102,18 +121,65 @@ export class AgentAcpAdapter {
102
121
  logger.info("Initialized with", {
103
122
  agentDir,
104
123
  agentName,
124
+ agentDisplayName: this.agentDisplayName,
125
+ agentVersion: this.agentVersion,
126
+ agentDescription: this.agentDescription,
127
+ suggestedPrompts: this.agentSuggestedPrompts,
105
128
  noSession: this.noSession,
106
129
  hasStorage: this.storage !== null,
107
130
  sessionStoragePath: this.storage ? `${agentDir}/.sessions` : null,
108
131
  });
109
132
  }
133
+ /**
134
+ * Helper to save session to disk
135
+ * Call this after any modification to session.messages or session.context
136
+ */
137
+ async saveSessionToDisk(sessionId, session) {
138
+ if (!this.noSession && this.storage) {
139
+ try {
140
+ await this.storage.saveSession(sessionId, session.messages, session.context);
141
+ logger.debug("Saved session to disk", {
142
+ sessionId,
143
+ messageCount: session.messages.length,
144
+ contextCount: session.context.length,
145
+ });
146
+ }
147
+ catch (error) {
148
+ logger.error(`Failed to save session ${sessionId}`, {
149
+ error: error instanceof Error ? error.message : String(error),
150
+ });
151
+ }
152
+ }
153
+ }
110
154
  async initialize(_params) {
111
- return {
155
+ const response = {
112
156
  protocolVersion: acp.PROTOCOL_VERSION,
113
157
  agentCapabilities: {
114
158
  loadSession: !this.noSession && this.storage !== null,
115
159
  },
116
160
  };
161
+ if (this.agentName) {
162
+ response.agentInfo = {
163
+ name: this.agentName,
164
+ version: this.agentVersion ?? "0.0.0",
165
+ // title is the ACP field for human-readable display name
166
+ ...(this.agentDisplayName ? { title: this.agentDisplayName } : {}),
167
+ };
168
+ }
169
+ // Pass description and suggestedPrompts via _meta extension point
170
+ // since Implementation doesn't support these fields
171
+ if (this.agentDescription || this.agentSuggestedPrompts) {
172
+ response._meta = {
173
+ ...response._meta,
174
+ ...(this.agentDescription
175
+ ? { agentDescription: this.agentDescription }
176
+ : {}),
177
+ ...(this.agentSuggestedPrompts
178
+ ? { suggestedPrompts: this.agentSuggestedPrompts }
179
+ : {}),
180
+ };
181
+ }
182
+ return response;
117
183
  }
118
184
  async newSession(params) {
119
185
  const sessionId = Math.random().toString(36).substring(2);
@@ -216,6 +282,37 @@ export class AgentAcpAdapter {
216
282
  }
217
283
  }
218
284
  }
285
+ // After replay completes, send the latest context size to the UI
286
+ const latestContext = storedSession.context.length > 0
287
+ ? storedSession.context[storedSession.context.length - 1]
288
+ : undefined;
289
+ if (latestContext?.context_size) {
290
+ logger.info("Sending context size to UI after session replay", {
291
+ sessionId: params.sessionId,
292
+ contextSize: latestContext.context_size,
293
+ });
294
+ this.connection.sessionUpdate({
295
+ sessionId: params.sessionId,
296
+ update: {
297
+ sessionUpdate: "agent_message_chunk",
298
+ content: {
299
+ type: "text",
300
+ text: "",
301
+ },
302
+ _meta: {
303
+ context_size: latestContext.context_size,
304
+ isReplay: true,
305
+ },
306
+ },
307
+ });
308
+ }
309
+ else {
310
+ logger.warn("No context size available after session replay", {
311
+ sessionId: params.sessionId,
312
+ hasLatestContext: !!latestContext,
313
+ contextLength: storedSession.context.length,
314
+ });
315
+ }
219
316
  return {};
220
317
  }
221
318
  async authenticate(_params) {
@@ -261,12 +358,49 @@ export class AgentAcpAdapter {
261
358
  timestamp: new Date().toISOString(),
262
359
  };
263
360
  session.messages.push(userMessage);
361
+ logger.debug("User message added to session", {
362
+ sessionId: params.sessionId,
363
+ messageCount: session.messages.length,
364
+ });
365
+ // Save immediately after user message, even before context calculation
366
+ // This ensures the session file exists if anything crashes below
367
+ await this.saveSessionToDisk(params.sessionId, session);
368
+ logger.debug("Session saved after user message", {
369
+ sessionId: params.sessionId,
370
+ });
264
371
  // Create context snapshot based on previous context
372
+ logger.debug("Starting context snapshot creation", {
373
+ sessionId: params.sessionId,
374
+ });
265
375
  const previousContext = session.context.length > 0
266
376
  ? session.context[session.context.length - 1]
267
377
  : undefined;
268
- const contextSnapshot = createContextSnapshot(session.messages.length, new Date().toISOString(), previousContext, previousContext?.inputTokens ?? 0);
378
+ // Calculate context size for this snapshot
379
+ // Build message pointers for the new context (previous messages + new user message)
380
+ const messageEntries = previousContext
381
+ ? [
382
+ ...previousContext.messages,
383
+ { type: "pointer", index: session.messages.length - 1 },
384
+ ]
385
+ : [{ type: "pointer", index: 0 }];
386
+ // Resolve message entries to actual messages
387
+ const contextMessages = [];
388
+ for (const entry of messageEntries) {
389
+ if (entry.type === "pointer") {
390
+ const message = session.messages[entry.index];
391
+ if (message) {
392
+ contextMessages.push(message);
393
+ }
394
+ }
395
+ else if (entry.type === "full") {
396
+ contextMessages.push(entry.message);
397
+ }
398
+ }
399
+ // Calculate context size - no LLM call yet, so only estimated values
400
+ const context_size = calculateContextSize(contextMessages, this.agent.definition.systemPrompt ?? undefined, undefined);
401
+ const contextSnapshot = createContextSnapshot(session.messages.length, new Date().toISOString(), previousContext, context_size);
269
402
  session.context.push(contextSnapshot);
403
+ await this.saveSessionToDisk(params.sessionId, session);
270
404
  }
271
405
  // Build ordered content blocks for the assistant response
272
406
  const contentBlocks = [];
@@ -293,23 +427,24 @@ export class AgentAcpAdapter {
293
427
  if (turnStartContextEntries.length > 0) {
294
428
  logger.info(`Appending ${turnStartContextEntries.length} new context entries from turn_start hooks`);
295
429
  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
- });
306
- }
307
- }
430
+ await this.saveSessionToDisk(params.sessionId, session);
308
431
  }
309
432
  // Resolve context to messages for agent invocation
310
433
  const contextMessages = this.noSession
311
434
  ? []
312
435
  : resolveContextToMessages(session.context, session.messages);
436
+ logger.debug("Resolved context messages for agent invocation", {
437
+ sessionId: params.sessionId,
438
+ contextMessageCount: contextMessages.length,
439
+ totalSessionMessages: session.messages.length,
440
+ latestContextEntry: session.context.length > 0 &&
441
+ session.context[session.context.length - 1]
442
+ ? {
443
+ messageCount: session.context[session.context.length - 1].messages.length,
444
+ contextSize: session.context[session.context.length - 1].context_size,
445
+ }
446
+ : null,
447
+ });
313
448
  const invokeParams = {
314
449
  prompt: params.prompt,
315
450
  sessionId: params.sessionId,
@@ -340,6 +475,19 @@ export class AgentAcpAdapter {
340
475
  if (tokenUsage.inputTokens !== undefined &&
341
476
  tokenUsage.inputTokens > 0) {
342
477
  turnTokenUsage.inputTokens = tokenUsage.inputTokens;
478
+ // Update the LAST context entry with LLM-reported tokens
479
+ if (!this.noSession && session.context.length > 0) {
480
+ const lastContext = session.context[session.context.length - 1];
481
+ if (lastContext) {
482
+ lastContext.context_size.llmReportedInputTokens =
483
+ tokenUsage.inputTokens;
484
+ logger.debug("Updated context entry with LLM-reported tokens", {
485
+ contextIndex: session.context.length - 1,
486
+ llmReportedTokens: tokenUsage.inputTokens,
487
+ estimatedTokens: lastContext.context_size.totalEstimated,
488
+ });
489
+ }
490
+ }
343
491
  }
344
492
  turnTokenUsage.outputTokens += tokenUsage.outputTokens ?? 0;
345
493
  turnTokenUsage.totalTokens += tokenUsage.totalTokens ?? 0;
@@ -409,12 +557,52 @@ export class AgentAcpAdapter {
409
557
  const outputMsg = msg;
410
558
  const toolCallBlock = contentBlocks.find((block) => block.type === "tool_call" && block.id === outputMsg.toolCallId);
411
559
  if (toolCallBlock) {
412
- // Store both rawOutput and output from tool_output
413
- if (outputMsg.rawOutput) {
414
- toolCallBlock.rawOutput = outputMsg.rawOutput;
560
+ // Get the raw output
561
+ let rawOutput = outputMsg.rawOutput || outputMsg.output;
562
+ let truncationWarning;
563
+ if (rawOutput && !this.noSession) {
564
+ // Execute tool_response hooks if configured
565
+ const hooks = this.agent.definition.hooks ?? [];
566
+ if (hooks.some((h) => h.type === "tool_response")) {
567
+ const latestContext = session.context[session.context.length - 1];
568
+ const currentContextTokens = latestContext?.context_size.llmReportedInputTokens ??
569
+ latestContext?.context_size.totalEstimated ??
570
+ 0;
571
+ const outputTokens = countToolResultTokens(rawOutput);
572
+ const hookExecutor = new HookExecutor(hooks, this.agent.definition.model, (callbackRef) => loadHookCallback(callbackRef, this.agentDir));
573
+ const hookResult = await hookExecutor.executeToolResponseHooks({
574
+ messages: session.messages,
575
+ context: session.context,
576
+ requestParams: session.requestParams,
577
+ }, currentContextTokens, {
578
+ toolCallId: outputMsg.toolCallId,
579
+ toolName: toolCallBlock.title || "unknown",
580
+ toolInput: toolCallBlock.rawInput || {},
581
+ rawOutput,
582
+ outputTokens,
583
+ });
584
+ // Apply modifications if hook returned them
585
+ if (hookResult.modifiedOutput) {
586
+ rawOutput = hookResult.modifiedOutput;
587
+ logger.info("Tool response modified by hook", {
588
+ toolCallId: outputMsg.toolCallId,
589
+ originalTokens: outputTokens,
590
+ finalTokens: countToolResultTokens(rawOutput),
591
+ });
592
+ }
593
+ truncationWarning = hookResult.truncationWarning;
594
+ }
595
+ }
596
+ // Store the (potentially modified) output
597
+ if (rawOutput) {
598
+ toolCallBlock.rawOutput = rawOutput;
415
599
  }
416
- else if (outputMsg.output) {
417
- toolCallBlock.rawOutput = outputMsg.output;
600
+ // Store truncation warning if present (for UI display)
601
+ if (truncationWarning) {
602
+ if (!toolCallBlock._meta) {
603
+ toolCallBlock._meta = {};
604
+ }
605
+ toolCallBlock._meta.truncationWarning = truncationWarning;
418
606
  }
419
607
  // Note: content blocks are handled by the transport for display
420
608
  // We store the raw output here for session persistence
@@ -445,22 +633,48 @@ export class AgentAcpAdapter {
445
633
  const latestContext = session.context.length > 0
446
634
  ? session.context[session.context.length - 1]
447
635
  : undefined;
636
+ // Build message entries for the new context
637
+ // Check if we already have a pointer to this message (during mid-turn updates)
638
+ const existingMessages = latestContext?.messages ?? [];
639
+ const lastEntry = existingMessages[existingMessages.length - 1];
640
+ const alreadyHasPointer = lastEntry?.type === "pointer" &&
641
+ lastEntry.index === partialMessageIndex;
642
+ const messageEntries = alreadyHasPointer
643
+ ? existingMessages // Don't add duplicate pointer
644
+ : [
645
+ ...existingMessages,
646
+ { type: "pointer", index: partialMessageIndex },
647
+ ];
648
+ // Resolve message entries to actual messages
649
+ const contextMessages = [];
650
+ for (const entry of messageEntries) {
651
+ if (entry.type === "pointer") {
652
+ const message = session.messages[entry.index];
653
+ if (message) {
654
+ contextMessages.push(message);
655
+ }
656
+ }
657
+ else if (entry.type === "full") {
658
+ contextMessages.push(entry.message);
659
+ }
660
+ }
661
+ // Calculate context size - tool result is now in the message, but hasn't been sent to LLM yet
662
+ const context_size = calculateContextSize(contextMessages, this.agent.definition.systemPrompt ?? undefined, undefined);
448
663
  // Create snapshot with a pointer to the partial message (not a full copy!)
449
664
  const midTurnSnapshot = {
450
665
  timestamp: new Date().toISOString(),
451
- messages: [
452
- ...(latestContext?.messages ?? []),
453
- { type: "pointer", index: partialMessageIndex },
454
- ],
666
+ messages: messageEntries,
455
667
  compactedUpTo: latestContext?.compactedUpTo,
456
- inputTokens: turnTokenUsage.inputTokens || latestContext?.inputTokens || 0,
668
+ context_size,
457
669
  };
458
670
  session.context.push(midTurnSnapshot);
671
+ await this.saveSessionToDisk(params.sessionId, session);
459
672
  logger.debug("Created mid-turn context snapshot after tool output", {
460
673
  toolCallId: outputMsg.toolCallId,
461
674
  contentBlocks: contentBlocks.length,
462
675
  partialMessageIndex,
463
- inputTokens: midTurnSnapshot.inputTokens,
676
+ totalEstimated: midTurnSnapshot.context_size.totalEstimated,
677
+ toolResultsTokens: midTurnSnapshot.context_size.toolResultsTokens,
464
678
  });
465
679
  // Execute hooks mid-turn to check if compaction is needed
466
680
  const midTurnContextEntries = await this.executeHooksIfConfigured(session, params.sessionId, "mid_turn");
@@ -470,23 +684,7 @@ export class AgentAcpAdapter {
470
684
  toolCallId: outputMsg.toolCallId,
471
685
  });
472
686
  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
- }
687
+ await this.saveSessionToDisk(params.sessionId, session);
490
688
  }
491
689
  }
492
690
  }
@@ -500,32 +698,23 @@ export class AgentAcpAdapter {
500
698
  msg._meta &&
501
699
  typeof msg._meta === "object" &&
502
700
  "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) {
701
+ // Get latest context entry and send its full context_size breakdown to GUI
702
+ const latestContext = session.context.length > 0
703
+ ? session.context[session.context.length - 1]
704
+ : undefined;
705
+ if (latestContext?.context_size) {
518
706
  enhancedMsg = {
519
707
  ...msg,
520
708
  _meta: {
521
709
  ...msg._meta,
522
- contextInputTokens,
710
+ context_size: latestContext.context_size,
523
711
  },
524
712
  };
525
- logger.debug("Sending contextInputTokens to GUI", {
526
- contextInputTokens,
527
- turnTokens: turnTokenUsage.inputTokens,
528
- previousContextTokens: session.context[session.context.length - 1]?.inputTokens,
713
+ }
714
+ else {
715
+ logger.warn("⚠️ No context_size to send to GUI", {
716
+ hasLatestContext: !!latestContext,
717
+ contextLength: session.context.length,
529
718
  });
530
719
  }
531
720
  }
@@ -568,22 +757,32 @@ export class AgentAcpAdapter {
568
757
  const previousContext = session.context.length > 0
569
758
  ? session.context[session.context.length - 1]
570
759
  : undefined;
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);
575
- session.context.push(contextSnapshot);
576
- }
577
- // Save session to disk if storage is configured and session persistence is enabled
578
- if (!this.noSession && this.storage) {
579
- try {
580
- await this.storage.saveSession(params.sessionId, session.messages, session.context);
581
- }
582
- catch (error) {
583
- logger.error(`Failed to save session ${params.sessionId}`, {
584
- error: error instanceof Error ? error.message : String(error),
585
- });
760
+ // Calculate final context size
761
+ // Build message pointers for the new context
762
+ const messageEntries = previousContext
763
+ ? [
764
+ ...previousContext.messages,
765
+ { type: "pointer", index: session.messages.length - 1 },
766
+ ]
767
+ : [{ type: "pointer", index: session.messages.length - 1 }];
768
+ // Resolve message entries to actual messages
769
+ const contextMessages = [];
770
+ for (const entry of messageEntries) {
771
+ if (entry.type === "pointer") {
772
+ const message = session.messages[entry.index];
773
+ if (message) {
774
+ contextMessages.push(message);
775
+ }
776
+ }
777
+ else if (entry.type === "full") {
778
+ contextMessages.push(entry.message);
779
+ }
586
780
  }
781
+ // Calculate context size with LLM-reported tokens from this turn
782
+ const context_size = calculateContextSize(contextMessages, this.agent.definition.systemPrompt ?? undefined, turnTokenUsage.inputTokens);
783
+ const contextSnapshot = createContextSnapshot(session.messages.length, new Date().toISOString(), previousContext, context_size);
784
+ session.context.push(contextSnapshot);
785
+ await this.saveSessionToDisk(params.sessionId, session);
587
786
  }
588
787
  session.pendingPrompt = null;
589
788
  return {
@@ -616,7 +815,15 @@ export class AgentAcpAdapter {
616
815
  const latestContext = session.context.length > 0
617
816
  ? session.context[session.context.length - 1]
618
817
  : undefined;
619
- const actualInputTokens = latestContext?.inputTokens ?? 0;
818
+ // Prefer LLM-reported tokens (most accurate), fall back to our estimate
819
+ const actualInputTokens = latestContext?.context_size.llmReportedInputTokens ??
820
+ latestContext?.context_size.totalEstimated ??
821
+ 0;
822
+ logger.debug("Using tokens for hook execution", {
823
+ llmReported: latestContext?.context_size.llmReportedInputTokens,
824
+ estimated: latestContext?.context_size.totalEstimated,
825
+ used: actualInputTokens,
826
+ });
620
827
  const hookResult = await hookExecutor.executeHooks(readonlySession, actualInputTokens);
621
828
  // Send hook notifications to client
622
829
  for (const notification of hookResult.notifications) {
@@ -1,5 +1,3 @@
1
1
  import type { AgentDefinition } from "../definition";
2
2
  import { type AgentRunner } from "../runner";
3
- export declare function makeStdioTransport(
4
- agent: AgentRunner | AgentDefinition,
5
- ): void;
3
+ export declare function makeStdioTransport(agent: AgentRunner | AgentDefinition): void;
@@ -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);
@@ -1,2 +1,7 @@
1
+ import { initializeOpenTelemetryFromEnv } from "../telemetry/setup.js";
2
+ // Initialize OpenTelemetry when this module is imported (if enabled)
3
+ if (process.env.ENABLE_TELEMETRY === "true") {
4
+ initializeOpenTelemetryFromEnv();
5
+ }
1
6
  export { makeStdioTransport } from "./cli";
2
7
  export { makeHttpTransport } from "./http";
@@ -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
  /**
@@ -51,10 +57,18 @@ export interface ContextEntry {
51
57
  */
52
58
  compactedUpTo?: number | undefined;
53
59
  /**
54
- * Actual input token count from API for this context.
55
- * Used for accurate context size tracking and compaction decisions.
60
+ * Complete breakdown of context size at this snapshot.
61
+ * Calculated by counting ALL tokens in the messages referenced by this context entry.
56
62
  */
57
- inputTokens?: number | undefined;
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
+ };
58
72
  }
59
73
  /**
60
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(),
package/dist/bin.js CHANGED
File without changes
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Quick script to verify Jaeger connectivity
3
+ * Run with: bun check-jaeger.ts
4
+ */
5
+ export {};