@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.
- package/dist/acp-server/adapter.d.ts +10 -0
- package/dist/acp-server/adapter.js +287 -80
- package/dist/acp-server/cli.d.ts +1 -3
- package/dist/acp-server/http.js +8 -1
- package/dist/acp-server/index.js +5 -0
- package/dist/acp-server/session-storage.d.ts +17 -3
- package/dist/acp-server/session-storage.js +9 -0
- package/dist/bin.js +0 -0
- package/dist/check-jaeger.d.ts +5 -0
- package/dist/check-jaeger.js +82 -0
- package/dist/definition/index.d.ts +16 -4
- package/dist/definition/index.js +17 -4
- package/dist/index.js +1 -1
- package/dist/run-subagents.d.ts +9 -0
- package/dist/run-subagents.js +110 -0
- 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/index.d.ts +1 -3
- package/dist/runner/langchain/custom-stream-types.d.ts +36 -0
- package/dist/runner/langchain/custom-stream-types.js +23 -0
- package/dist/runner/langchain/index.js +102 -76
- package/dist/runner/langchain/otel-callbacks.js +67 -1
- package/dist/runner/langchain/tools/bash.d.ts +14 -0
- package/dist/runner/langchain/tools/bash.js +135 -0
- 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/setup.d.ts +3 -1
- package/dist/telemetry/setup.js +33 -3
- package/dist/templates/index.d.ts +7 -0
- package/dist/test-telemetry.d.ts +5 -0
- package/dist/test-telemetry.js +88 -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
- package/dist/definition/mcp.d.ts +0 -0
- package/dist/definition/mcp.js +0 -0
- package/dist/definition/tools/todo.d.ts +0 -49
- package/dist/definition/tools/todo.js +0 -80
- package/dist/definition/tools/web_search.d.ts +0 -4
- package/dist/definition/tools/web_search.js +0 -26
- package/dist/dev-agent/index.d.ts +0 -2
- package/dist/dev-agent/index.js +0 -18
- package/dist/example.d.ts +0 -2
- 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,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
413
|
-
|
|
414
|
-
|
|
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
|
-
|
|
417
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
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
|
-
|
|
710
|
+
context_size: latestContext.context_size,
|
|
523
711
|
},
|
|
524
712
|
};
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
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
|
-
|
|
572
|
-
//
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
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
|
-
|
|
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) {
|
package/dist/acp-server/cli.d.ts
CHANGED
|
@@ -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;
|
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);
|
package/dist/acp-server/index.js
CHANGED
|
@@ -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
|
-
*
|
|
55
|
-
*
|
|
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
|
-
|
|
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
|