@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.
- package/dist/acp-server/adapter.d.ts +5 -0
- package/dist/acp-server/adapter.js +219 -48
- package/dist/acp-server/session-storage.d.ts +5 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.js +9 -0
- package/dist/runner/agent-runner.d.ts +3 -0
- package/dist/runner/hooks/executor.d.ts +1 -1
- package/dist/runner/hooks/executor.js +8 -62
- package/dist/runner/hooks/predefined/compaction-tool.js +14 -4
- package/dist/runner/hooks/types.d.ts +1 -1
- package/dist/runner/hooks/types.js +4 -1
- package/dist/runner/langchain/index.d.ts +1 -0
- package/dist/runner/langchain/index.js +504 -321
- package/dist/runner/langchain/model-factory.js +1 -1
- package/dist/runner/langchain/otel-callbacks.d.ts +18 -0
- package/dist/runner/langchain/otel-callbacks.js +123 -0
- package/dist/runner/langchain/tools/subagent.js +21 -1
- package/dist/telemetry/index.d.ts +83 -0
- package/dist/telemetry/index.js +172 -0
- package/dist/telemetry/setup.d.ts +22 -0
- package/dist/telemetry/setup.js +141 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/index.ts +15 -0
- package/package.json +23 -7
|
@@ -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 {
|
|
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
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
|
107
|
-
const percentage = (currentTokens / maxTokens) * 100;
|
|
53
|
+
const percentage = (actualInputTokens / maxTokens) * 100;
|
|
108
54
|
// Default threshold is 95%
|
|
109
55
|
const threshold = hook.setting?.threshold ?? 95;
|
|
110
56
|
// Check if threshold exceeded
|
|
111
57
|
if (percentage < threshold) {
|
|
112
|
-
logger.info(`Context hook not triggered: ${
|
|
58
|
+
logger.info(`Context hook not triggered: ${actualInputTokens} tokens (${percentage.toFixed(1)}%) < threshold ${threshold}%`);
|
|
113
59
|
return null; // No action needed
|
|
114
60
|
}
|
|
115
|
-
logger.info(`Context hook triggered: ${
|
|
61
|
+
logger.info(`Context hook triggered: ${actualInputTokens} tokens (${percentage.toFixed(1)}%) exceeds threshold ${threshold}%`);
|
|
116
62
|
const notifications = [];
|
|
117
63
|
// Notify that hook is triggered
|
|
118
64
|
notifications.push({
|
|
@@ -127,7 +73,7 @@ export class HookExecutor {
|
|
|
127
73
|
const callback = await this.loadCallback(hook.callback);
|
|
128
74
|
const hookContext = {
|
|
129
75
|
session,
|
|
130
|
-
currentTokens,
|
|
76
|
+
currentTokens: actualInputTokens,
|
|
131
77
|
maxTokens,
|
|
132
78
|
percentage,
|
|
133
79
|
model: this.model,
|
|
@@ -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" &&
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|