@townco/agent 0.1.83 → 0.1.84
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.js +140 -43
- package/dist/acp-server/http.js +55 -0
- package/dist/acp-server/session-storage.d.ts +31 -6
- package/dist/acp-server/session-storage.js +60 -1
- package/dist/definition/index.d.ts +2 -2
- package/dist/definition/index.js +1 -1
- package/dist/runner/agent-runner.d.ts +1 -1
- package/dist/runner/hooks/executor.d.ts +1 -0
- package/dist/runner/hooks/executor.js +17 -1
- package/dist/runner/hooks/predefined/tool-response-compactor.d.ts +0 -4
- package/dist/runner/hooks/predefined/tool-response-compactor.js +28 -14
- package/dist/runner/hooks/types.d.ts +4 -5
- package/dist/runner/langchain/index.js +62 -11
- package/dist/runner/langchain/tools/artifacts.js +6 -9
- package/dist/templates/index.d.ts +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/utils/context-size-calculator.d.ts +1 -10
- package/dist/utils/context-size-calculator.js +1 -12
- package/package.json +6 -6
- package/templates/index.ts +1 -1
|
@@ -382,7 +382,7 @@ export class AgentAcpAdapter {
|
|
|
382
382
|
: {}),
|
|
383
383
|
...block._meta,
|
|
384
384
|
};
|
|
385
|
-
// Debug: log subagent data being replayed
|
|
385
|
+
// Debug: log subagent data and compaction metadata being replayed
|
|
386
386
|
logger.info("Replaying tool_call", {
|
|
387
387
|
toolCallId: block.id,
|
|
388
388
|
title: block.title,
|
|
@@ -391,6 +391,8 @@ export class AgentAcpAdapter {
|
|
|
391
391
|
hasSubagentSessionId: !!block.subagentSessionId,
|
|
392
392
|
hasSubagentMessages: !!block.subagentMessages,
|
|
393
393
|
subagentMessagesCount: block.subagentMessages?.length,
|
|
394
|
+
blockMeta: block._meta,
|
|
395
|
+
replayMeta,
|
|
394
396
|
});
|
|
395
397
|
this.connection.sessionUpdate({
|
|
396
398
|
sessionId: params.sessionId,
|
|
@@ -436,6 +438,7 @@ export class AgentAcpAdapter {
|
|
|
436
438
|
status: block.status,
|
|
437
439
|
rawOutput: block.rawOutput,
|
|
438
440
|
error: block.error,
|
|
441
|
+
_meta: replayMeta,
|
|
439
442
|
},
|
|
440
443
|
});
|
|
441
444
|
}
|
|
@@ -600,9 +603,8 @@ export class AgentAcpAdapter {
|
|
|
600
603
|
contextMessages.push(entry.message);
|
|
601
604
|
}
|
|
602
605
|
}
|
|
603
|
-
// Calculate context size -
|
|
604
|
-
const context_size = calculateContextSize(contextMessages, this.agent.definition.systemPrompt ?? undefined,
|
|
605
|
-
this.currentToolOverheadTokens, // Include tool overhead
|
|
606
|
+
// Calculate context size - only estimated values
|
|
607
|
+
const context_size = calculateContextSize(contextMessages, this.agent.definition.systemPrompt ?? undefined, this.currentToolOverheadTokens, // Include tool overhead
|
|
606
608
|
this.currentMcpOverheadTokens, // Include MCP overhead
|
|
607
609
|
getModelContextWindow(this.agent.definition.model));
|
|
608
610
|
const contextSnapshot = createContextSnapshot(session.messages.length - 1, // Exclude the newly added user message (it will be passed separately via prompt)
|
|
@@ -721,19 +723,8 @@ export class AgentAcpAdapter {
|
|
|
721
723
|
if (tokenUsage.inputTokens !== undefined &&
|
|
722
724
|
tokenUsage.inputTokens > 0) {
|
|
723
725
|
turnTokenUsage.inputTokens = tokenUsage.inputTokens;
|
|
724
|
-
//
|
|
725
|
-
|
|
726
|
-
const lastContext = session.context[session.context.length - 1];
|
|
727
|
-
if (lastContext) {
|
|
728
|
-
lastContext.context_size.llmReportedInputTokens =
|
|
729
|
-
tokenUsage.inputTokens;
|
|
730
|
-
logger.debug("Updated context entry with LLM-reported tokens", {
|
|
731
|
-
contextIndex: session.context.length - 1,
|
|
732
|
-
llmReportedTokens: tokenUsage.inputTokens,
|
|
733
|
-
estimatedTokens: lastContext.context_size.totalEstimated,
|
|
734
|
-
});
|
|
735
|
-
}
|
|
736
|
-
}
|
|
726
|
+
// Note: We no longer update context entries with LLM-reported tokens
|
|
727
|
+
// as they can cause mismatches when updated on mid-turn snapshots
|
|
737
728
|
}
|
|
738
729
|
turnTokenUsage.outputTokens += tokenUsage.outputTokens ?? 0;
|
|
739
730
|
turnTokenUsage.totalTokens += tokenUsage.totalTokens ?? 0;
|
|
@@ -910,19 +901,71 @@ export class AgentAcpAdapter {
|
|
|
910
901
|
// Get the raw output
|
|
911
902
|
let rawOutput = outputMsg.rawOutput || outputMsg.output;
|
|
912
903
|
let truncationWarning;
|
|
904
|
+
// Check for compaction metadata embedded by LangChain tool wrapper
|
|
905
|
+
// This happens when compaction runs in the wrapper (before reaching adapter)
|
|
906
|
+
if (rawOutput &&
|
|
907
|
+
typeof rawOutput === "object" &&
|
|
908
|
+
"_compactionMeta" in rawOutput) {
|
|
909
|
+
const compactionMeta = rawOutput._compactionMeta;
|
|
910
|
+
// Store in _meta for UI persistence
|
|
911
|
+
if (!toolCallBlock._meta) {
|
|
912
|
+
toolCallBlock._meta = {};
|
|
913
|
+
}
|
|
914
|
+
if (compactionMeta.action) {
|
|
915
|
+
toolCallBlock._meta.compactionAction = compactionMeta.action;
|
|
916
|
+
}
|
|
917
|
+
if (compactionMeta.originalTokens !== undefined) {
|
|
918
|
+
toolCallBlock._meta.originalTokens =
|
|
919
|
+
compactionMeta.originalTokens;
|
|
920
|
+
}
|
|
921
|
+
if (compactionMeta.finalTokens !== undefined) {
|
|
922
|
+
toolCallBlock._meta.finalTokens = compactionMeta.finalTokens;
|
|
923
|
+
}
|
|
924
|
+
// Store original content only if compaction or truncation actually occurred
|
|
925
|
+
const wasCompacted = compactionMeta.action === "compacted" ||
|
|
926
|
+
compactionMeta.action === "truncated" ||
|
|
927
|
+
compactionMeta.action === "compacted_then_truncated";
|
|
928
|
+
if (compactionMeta.originalContent &&
|
|
929
|
+
wasCompacted &&
|
|
930
|
+
this.storage) {
|
|
931
|
+
try {
|
|
932
|
+
const toolName = toolCallBlock.title || "unknown";
|
|
933
|
+
const originalContentPath = this.storage.saveToolOriginal(params.sessionId, toolName, outputMsg.toolCallId, compactionMeta.originalContent);
|
|
934
|
+
toolCallBlock._meta.originalContentPath = originalContentPath;
|
|
935
|
+
logger.info("Saved original content to artifacts", {
|
|
936
|
+
toolCallId: outputMsg.toolCallId,
|
|
937
|
+
toolName,
|
|
938
|
+
path: originalContentPath,
|
|
939
|
+
});
|
|
940
|
+
}
|
|
941
|
+
catch (error) {
|
|
942
|
+
logger.warn("Failed to save original tool content", {
|
|
943
|
+
toolCallId: outputMsg.toolCallId,
|
|
944
|
+
error: error instanceof Error ? error.message : String(error),
|
|
945
|
+
});
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
logger.info("Extracted compaction metadata from tool response wrapper", {
|
|
949
|
+
toolCallId: outputMsg.toolCallId,
|
|
950
|
+
action: compactionMeta.action,
|
|
951
|
+
originalTokens: compactionMeta.originalTokens,
|
|
952
|
+
finalTokens: compactionMeta.finalTokens,
|
|
953
|
+
hasOriginalContent: !!compactionMeta.originalContent,
|
|
954
|
+
});
|
|
955
|
+
// Remove the metadata from rawOutput to keep it clean
|
|
956
|
+
const { _compactionMeta: _, ...cleanOutput } = rawOutput;
|
|
957
|
+
rawOutput = cleanOutput;
|
|
958
|
+
}
|
|
913
959
|
if (rawOutput && !this.noSession) {
|
|
914
960
|
// Execute tool_response hooks if configured
|
|
915
961
|
const hooks = this.agent.definition.hooks ?? [];
|
|
916
962
|
if (hooks.some((h) => h.type === "tool_response")) {
|
|
917
963
|
const latestContext = session.context[session.context.length - 1];
|
|
918
|
-
// Use
|
|
919
|
-
const currentContextTokens =
|
|
920
|
-
// Send the context size to the UI
|
|
964
|
+
// Use estimated tokens for hook evaluation
|
|
965
|
+
const currentContextTokens = latestContext?.context_size.totalEstimated ?? 0;
|
|
966
|
+
// Send the context size to the UI
|
|
921
967
|
if (latestContext?.context_size) {
|
|
922
|
-
const contextSizeForUI =
|
|
923
|
-
...latestContext.context_size,
|
|
924
|
-
totalEstimated: currentContextTokens,
|
|
925
|
-
};
|
|
968
|
+
const contextSizeForUI = latestContext.context_size;
|
|
926
969
|
this.connection.sessionUpdate({
|
|
927
970
|
sessionId: params.sessionId,
|
|
928
971
|
update: {
|
|
@@ -964,6 +1007,12 @@ export class AgentAcpAdapter {
|
|
|
964
1007
|
});
|
|
965
1008
|
// Note: Notifications are now sent in real-time via the callback
|
|
966
1009
|
// The hookResult.notifications array is kept for backwards compatibility
|
|
1010
|
+
// Capture original content BEFORE any modification (needed for storage)
|
|
1011
|
+
const originalContentStr = typeof rawOutput === "object" &&
|
|
1012
|
+
rawOutput !== null &&
|
|
1013
|
+
"content" in rawOutput
|
|
1014
|
+
? String(rawOutput.content)
|
|
1015
|
+
: JSON.stringify(rawOutput);
|
|
967
1016
|
// Apply modifications if hook returned them
|
|
968
1017
|
if (hookResult.modifiedOutput) {
|
|
969
1018
|
rawOutput = hookResult.modifiedOutput;
|
|
@@ -974,6 +1023,49 @@ export class AgentAcpAdapter {
|
|
|
974
1023
|
});
|
|
975
1024
|
}
|
|
976
1025
|
truncationWarning = hookResult.truncationWarning;
|
|
1026
|
+
// Store hook result metadata in toolCallBlock._meta for persistence
|
|
1027
|
+
if (hookResult.metadata) {
|
|
1028
|
+
if (!toolCallBlock._meta) {
|
|
1029
|
+
toolCallBlock._meta = {};
|
|
1030
|
+
}
|
|
1031
|
+
// Store compaction action and stats
|
|
1032
|
+
if (hookResult.metadata.action) {
|
|
1033
|
+
toolCallBlock._meta.compactionAction = hookResult.metadata
|
|
1034
|
+
.action;
|
|
1035
|
+
}
|
|
1036
|
+
if (hookResult.metadata.originalTokens !== undefined) {
|
|
1037
|
+
toolCallBlock._meta.originalTokens = hookResult.metadata
|
|
1038
|
+
.originalTokens;
|
|
1039
|
+
}
|
|
1040
|
+
if (hookResult.metadata.finalTokens !== undefined) {
|
|
1041
|
+
toolCallBlock._meta.finalTokens = hookResult.metadata
|
|
1042
|
+
.finalTokens;
|
|
1043
|
+
}
|
|
1044
|
+
// Store original content if compaction occurred (action !== "none")
|
|
1045
|
+
if (hookResult.metadata.action &&
|
|
1046
|
+
hookResult.metadata.action !== "none" &&
|
|
1047
|
+
this.storage) {
|
|
1048
|
+
try {
|
|
1049
|
+
const toolName = toolCallBlock.title || "unknown";
|
|
1050
|
+
const originalContentPath = this.storage.saveToolOriginal(params.sessionId, toolName, outputMsg.toolCallId, originalContentStr);
|
|
1051
|
+
toolCallBlock._meta.originalContentPath =
|
|
1052
|
+
originalContentPath;
|
|
1053
|
+
logger.info("Saved original content to artifacts", {
|
|
1054
|
+
toolCallId: outputMsg.toolCallId,
|
|
1055
|
+
toolName,
|
|
1056
|
+
path: originalContentPath,
|
|
1057
|
+
});
|
|
1058
|
+
}
|
|
1059
|
+
catch (error) {
|
|
1060
|
+
logger.warn("Failed to save original tool content", {
|
|
1061
|
+
toolCallId: outputMsg.toolCallId,
|
|
1062
|
+
error: error instanceof Error
|
|
1063
|
+
? error.message
|
|
1064
|
+
: String(error),
|
|
1065
|
+
});
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
}
|
|
977
1069
|
}
|
|
978
1070
|
}
|
|
979
1071
|
// Store the (potentially modified) output
|
|
@@ -987,6 +1079,22 @@ export class AgentAcpAdapter {
|
|
|
987
1079
|
}
|
|
988
1080
|
toolCallBlock._meta.truncationWarning = truncationWarning;
|
|
989
1081
|
}
|
|
1082
|
+
// Send compaction metadata to the client if present
|
|
1083
|
+
// This is needed for live streaming (not just replay)
|
|
1084
|
+
if (toolCallBlock._meta?.compactionAction) {
|
|
1085
|
+
logger.info("Sending compaction metadata to client during live streaming", {
|
|
1086
|
+
toolCallId: outputMsg.toolCallId,
|
|
1087
|
+
_meta: toolCallBlock._meta,
|
|
1088
|
+
});
|
|
1089
|
+
this.connection.sessionUpdate({
|
|
1090
|
+
sessionId: params.sessionId,
|
|
1091
|
+
update: {
|
|
1092
|
+
sessionUpdate: "tool_call_update",
|
|
1093
|
+
toolCallId: outputMsg.toolCallId,
|
|
1094
|
+
_meta: toolCallBlock._meta,
|
|
1095
|
+
},
|
|
1096
|
+
});
|
|
1097
|
+
}
|
|
990
1098
|
// Note: content blocks are handled by the transport for display
|
|
991
1099
|
// We store the raw output here for session persistence
|
|
992
1100
|
// Create mid-turn context snapshot after tool completes
|
|
@@ -1062,8 +1170,7 @@ export class AgentAcpAdapter {
|
|
|
1062
1170
|
}
|
|
1063
1171
|
}
|
|
1064
1172
|
// Calculate context size - tool result is now in the message, but hasn't been sent to LLM yet
|
|
1065
|
-
const context_size = calculateContextSize(contextMessages, this.agent.definition.systemPrompt ?? undefined,
|
|
1066
|
-
this.currentToolOverheadTokens, // Include tool overhead
|
|
1173
|
+
const context_size = calculateContextSize(contextMessages, this.agent.definition.systemPrompt ?? undefined, this.currentToolOverheadTokens, // Include tool overhead
|
|
1067
1174
|
this.currentMcpOverheadTokens, // Include MCP overhead
|
|
1068
1175
|
getModelContextWindow(this.agent.definition.model));
|
|
1069
1176
|
// Create snapshot with a pointer to the partial message (not a full copy!)
|
|
@@ -1217,9 +1324,8 @@ export class AgentAcpAdapter {
|
|
|
1217
1324
|
contextMessages.push(entry.message);
|
|
1218
1325
|
}
|
|
1219
1326
|
}
|
|
1220
|
-
// Calculate context size
|
|
1221
|
-
const context_size = calculateContextSize(contextMessages, this.agent.definition.systemPrompt ?? undefined,
|
|
1222
|
-
this.currentToolOverheadTokens, // Include tool overhead
|
|
1327
|
+
// Calculate context size - only estimated values
|
|
1328
|
+
const context_size = calculateContextSize(contextMessages, this.agent.definition.systemPrompt ?? undefined, this.currentToolOverheadTokens, // Include tool overhead
|
|
1223
1329
|
this.currentMcpOverheadTokens, // Include MCP overhead
|
|
1224
1330
|
getModelContextWindow(this.agent.definition.model));
|
|
1225
1331
|
const contextSnapshot = createContextSnapshot(session.messages.length, new Date().toISOString(), previousContext, context_size);
|
|
@@ -1297,28 +1403,19 @@ export class AgentAcpAdapter {
|
|
|
1297
1403
|
context: session.context,
|
|
1298
1404
|
requestParams: session.requestParams,
|
|
1299
1405
|
};
|
|
1300
|
-
// Get
|
|
1406
|
+
// Get input token count from latest context entry
|
|
1301
1407
|
const latestContext = session.context.length > 0
|
|
1302
1408
|
? session.context[session.context.length - 1]
|
|
1303
1409
|
: undefined;
|
|
1304
|
-
// Use
|
|
1305
|
-
|
|
1306
|
-
const actualInputTokens = Math.max(latestContext?.context_size.totalEstimated ?? 0, latestContext?.context_size.llmReportedInputTokens ?? 0);
|
|
1410
|
+
// Use estimated tokens for hook evaluation
|
|
1411
|
+
const actualInputTokens = latestContext?.context_size.totalEstimated ?? 0;
|
|
1307
1412
|
logger.debug("Using tokens for hook execution", {
|
|
1308
|
-
llmReported: latestContext?.context_size.llmReportedInputTokens,
|
|
1309
1413
|
estimated: latestContext?.context_size.totalEstimated,
|
|
1310
1414
|
used: actualInputTokens,
|
|
1311
1415
|
});
|
|
1312
|
-
// Send the context size to the UI
|
|
1313
|
-
// This ensures the UI percentage matches what the hook sees
|
|
1416
|
+
// Send the context size to the UI
|
|
1314
1417
|
if (latestContext?.context_size) {
|
|
1315
|
-
|
|
1316
|
-
// so UI can calculate the same percentage
|
|
1317
|
-
const contextSizeForUI = {
|
|
1318
|
-
...latestContext.context_size,
|
|
1319
|
-
// Override with the max value we computed (what hooks actually use)
|
|
1320
|
-
totalEstimated: actualInputTokens,
|
|
1321
|
-
};
|
|
1418
|
+
const contextSizeForUI = latestContext.context_size;
|
|
1322
1419
|
this.connection.sessionUpdate({
|
|
1323
1420
|
sessionId,
|
|
1324
1421
|
update: {
|
package/dist/acp-server/http.js
CHANGED
|
@@ -455,6 +455,61 @@ export function makeHttpTransport(agent, agentDir, agentName) {
|
|
|
455
455
|
}, 500);
|
|
456
456
|
}
|
|
457
457
|
});
|
|
458
|
+
// Serve files from session artifacts folder
|
|
459
|
+
app.get("/sessions/:sessionId/artifacts/*", async (c) => {
|
|
460
|
+
if (!agentDir || !agentName) {
|
|
461
|
+
return c.json({ error: "Session storage not configured" }, 500);
|
|
462
|
+
}
|
|
463
|
+
const noSession = process.env.TOWN_NO_SESSION === "true";
|
|
464
|
+
if (noSession) {
|
|
465
|
+
return c.json({ error: "Sessions disabled" }, 500);
|
|
466
|
+
}
|
|
467
|
+
const sessionId = c.req.param("sessionId");
|
|
468
|
+
// Extract the filename from the wildcard path
|
|
469
|
+
const filename = c.req.path.replace(`/sessions/${sessionId}/artifacts/`, "");
|
|
470
|
+
if (!sessionId || !filename) {
|
|
471
|
+
return c.json({ error: "Session ID and filename required" }, 400);
|
|
472
|
+
}
|
|
473
|
+
// Build path to artifacts folder
|
|
474
|
+
const storage = new SessionStorage(agentDir, agentName);
|
|
475
|
+
const artifactsDir = storage.getArtifactsDir(sessionId);
|
|
476
|
+
const filePath = join(artifactsDir, filename);
|
|
477
|
+
// Security check: ensure the file is within the artifacts directory
|
|
478
|
+
const normalizedPath = resolve(filePath);
|
|
479
|
+
const normalizedArtifactsDir = resolve(artifactsDir);
|
|
480
|
+
if (!normalizedPath.startsWith(normalizedArtifactsDir)) {
|
|
481
|
+
logger.warn("Attempted to access file outside artifacts directory", {
|
|
482
|
+
filename,
|
|
483
|
+
filePath,
|
|
484
|
+
artifactsDir,
|
|
485
|
+
});
|
|
486
|
+
return c.json({ error: "Invalid path" }, 403);
|
|
487
|
+
}
|
|
488
|
+
try {
|
|
489
|
+
const file = Bun.file(filePath);
|
|
490
|
+
const exists = await file.exists();
|
|
491
|
+
if (!exists) {
|
|
492
|
+
logger.warn("Artifact file not found", {
|
|
493
|
+
sessionId,
|
|
494
|
+
filename,
|
|
495
|
+
filePath,
|
|
496
|
+
});
|
|
497
|
+
return c.json({ error: "File not found" }, 404);
|
|
498
|
+
}
|
|
499
|
+
const content = await file.text();
|
|
500
|
+
return c.json({ content });
|
|
501
|
+
}
|
|
502
|
+
catch (error) {
|
|
503
|
+
logger.error("Failed to load artifact", {
|
|
504
|
+
error,
|
|
505
|
+
sessionId,
|
|
506
|
+
filename,
|
|
507
|
+
});
|
|
508
|
+
return c.json({
|
|
509
|
+
error: error instanceof Error ? error.message : String(error),
|
|
510
|
+
}, 500);
|
|
511
|
+
}
|
|
512
|
+
});
|
|
458
513
|
// Serve static files from agent directory (for generated images, etc.)
|
|
459
514
|
if (agentDir) {
|
|
460
515
|
app.get("/static/*", async (c) => {
|
|
@@ -66,11 +66,13 @@ export interface ToolCallBlock {
|
|
|
66
66
|
startedAt?: number | undefined;
|
|
67
67
|
completedAt?: number | undefined;
|
|
68
68
|
_meta?: {
|
|
69
|
-
truncationWarning?: string;
|
|
70
|
-
compactionAction?: "compacted" | "truncated";
|
|
71
|
-
originalTokens?: number;
|
|
72
|
-
finalTokens?: number;
|
|
73
|
-
|
|
69
|
+
truncationWarning?: string | undefined;
|
|
70
|
+
compactionAction?: "compacted" | "truncated" | undefined;
|
|
71
|
+
originalTokens?: number | undefined;
|
|
72
|
+
finalTokens?: number | undefined;
|
|
73
|
+
originalContentPreview?: string | undefined;
|
|
74
|
+
originalContentPath?: string | undefined;
|
|
75
|
+
} | undefined;
|
|
74
76
|
/** Sub-agent HTTP port (for reference, not used in replay) */
|
|
75
77
|
subagentPort?: number | undefined;
|
|
76
78
|
/** Sub-agent session ID (for reference, not used in replay) */
|
|
@@ -124,7 +126,6 @@ export interface ContextEntry {
|
|
|
124
126
|
toolInputTokens: number;
|
|
125
127
|
toolResultsTokens: number;
|
|
126
128
|
totalEstimated: number;
|
|
127
|
-
llmReportedInputTokens?: number | undefined;
|
|
128
129
|
modelContextWindow?: number | undefined;
|
|
129
130
|
};
|
|
130
131
|
}
|
|
@@ -212,4 +213,28 @@ export declare class SessionStorage {
|
|
|
212
213
|
messageCount: number;
|
|
213
214
|
firstUserMessage?: string;
|
|
214
215
|
}>>;
|
|
216
|
+
/**
|
|
217
|
+
* Get the directory for storing large content files for a session (artifacts folder)
|
|
218
|
+
*/
|
|
219
|
+
getArtifactsDir(sessionId: string): string;
|
|
220
|
+
/**
|
|
221
|
+
* Get the file path for a tool's original content
|
|
222
|
+
* Follows the pattern: artifacts/tool-<ToolName>/<toolCallId>.original.txt
|
|
223
|
+
*/
|
|
224
|
+
private getToolOriginalPath;
|
|
225
|
+
/**
|
|
226
|
+
* Save original tool response to a separate file
|
|
227
|
+
* Follows the pattern: artifacts/tool-<ToolName>/<toolCallId>.original.txt
|
|
228
|
+
* @param toolName - The name of the tool (e.g., "Read", "Grep")
|
|
229
|
+
* @returns Relative file path (for storage in _meta.originalContentPath)
|
|
230
|
+
*/
|
|
231
|
+
saveToolOriginal(sessionId: string, toolName: string, toolCallId: string, content: string): string;
|
|
232
|
+
/**
|
|
233
|
+
* Load original tool response from separate file
|
|
234
|
+
*/
|
|
235
|
+
loadToolOriginal(sessionId: string, toolName: string, toolCallId: string): string | null;
|
|
236
|
+
/**
|
|
237
|
+
* Check if original content exists for a tool call
|
|
238
|
+
*/
|
|
239
|
+
hasToolOriginal(sessionId: string, toolName: string, toolCallId: string): boolean;
|
|
215
240
|
}
|
|
@@ -78,6 +78,16 @@ const toolCallBlockSchema = z.object({
|
|
|
78
78
|
subagentPort: z.number().optional(),
|
|
79
79
|
subagentSessionId: z.string().optional(),
|
|
80
80
|
subagentMessages: z.array(subagentMessageSchema).optional(),
|
|
81
|
+
_meta: z
|
|
82
|
+
.object({
|
|
83
|
+
truncationWarning: z.string().optional(),
|
|
84
|
+
compactionAction: z.enum(["compacted", "truncated"]).optional(),
|
|
85
|
+
originalTokens: z.number().optional(),
|
|
86
|
+
finalTokens: z.number().optional(),
|
|
87
|
+
originalContentPreview: z.string().optional(),
|
|
88
|
+
originalContentPath: z.string().optional(),
|
|
89
|
+
})
|
|
90
|
+
.optional(),
|
|
81
91
|
});
|
|
82
92
|
const contentBlockSchema = z.discriminatedUnion("type", [
|
|
83
93
|
textBlockSchema,
|
|
@@ -114,7 +124,6 @@ const contextEntrySchema = z.object({
|
|
|
114
124
|
toolInputTokens: z.number(),
|
|
115
125
|
toolResultsTokens: z.number(),
|
|
116
126
|
totalEstimated: z.number(),
|
|
117
|
-
llmReportedInputTokens: z.number().optional(),
|
|
118
127
|
modelContextWindow: z.number().optional(),
|
|
119
128
|
}),
|
|
120
129
|
});
|
|
@@ -345,4 +354,54 @@ export class SessionStorage {
|
|
|
345
354
|
sessions.sort((a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime());
|
|
346
355
|
return sessions;
|
|
347
356
|
}
|
|
357
|
+
/**
|
|
358
|
+
* Get the directory for storing large content files for a session (artifacts folder)
|
|
359
|
+
*/
|
|
360
|
+
getArtifactsDir(sessionId) {
|
|
361
|
+
return join(this.sessionsDir, sessionId, "artifacts");
|
|
362
|
+
}
|
|
363
|
+
/**
|
|
364
|
+
* Get the file path for a tool's original content
|
|
365
|
+
* Follows the pattern: artifacts/tool-<ToolName>/<toolCallId>.original.txt
|
|
366
|
+
*/
|
|
367
|
+
getToolOriginalPath(sessionId, toolName, toolCallId) {
|
|
368
|
+
return join(this.getArtifactsDir(sessionId), `tool-${toolName}`, `${toolCallId}.original.txt`);
|
|
369
|
+
}
|
|
370
|
+
/**
|
|
371
|
+
* Save original tool response to a separate file
|
|
372
|
+
* Follows the pattern: artifacts/tool-<ToolName>/<toolCallId>.original.txt
|
|
373
|
+
* @param toolName - The name of the tool (e.g., "Read", "Grep")
|
|
374
|
+
* @returns Relative file path (for storage in _meta.originalContentPath)
|
|
375
|
+
*/
|
|
376
|
+
saveToolOriginal(sessionId, toolName, toolCallId, content) {
|
|
377
|
+
const toolDir = join(this.getArtifactsDir(sessionId), `tool-${toolName}`);
|
|
378
|
+
if (!existsSync(toolDir)) {
|
|
379
|
+
mkdirSync(toolDir, { recursive: true });
|
|
380
|
+
}
|
|
381
|
+
const filePath = this.getToolOriginalPath(sessionId, toolName, toolCallId);
|
|
382
|
+
writeFileSync(filePath, content, "utf-8");
|
|
383
|
+
// Return relative path for storage in metadata
|
|
384
|
+
return `${sessionId}/artifacts/tool-${toolName}/${toolCallId}.original.txt`;
|
|
385
|
+
}
|
|
386
|
+
/**
|
|
387
|
+
* Load original tool response from separate file
|
|
388
|
+
*/
|
|
389
|
+
loadToolOriginal(sessionId, toolName, toolCallId) {
|
|
390
|
+
const filePath = this.getToolOriginalPath(sessionId, toolName, toolCallId);
|
|
391
|
+
if (!existsSync(filePath)) {
|
|
392
|
+
return null;
|
|
393
|
+
}
|
|
394
|
+
try {
|
|
395
|
+
return readFileSync(filePath, "utf-8");
|
|
396
|
+
}
|
|
397
|
+
catch {
|
|
398
|
+
return null;
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
/**
|
|
402
|
+
* Check if original content exists for a tool call
|
|
403
|
+
*/
|
|
404
|
+
hasToolOriginal(sessionId, toolName, toolCallId) {
|
|
405
|
+
return existsSync(this.getToolOriginalPath(sessionId, toolName, toolCallId));
|
|
406
|
+
}
|
|
348
407
|
}
|
|
@@ -21,7 +21,7 @@ export declare const HookConfigSchema: z.ZodObject<{
|
|
|
21
21
|
setting: z.ZodOptional<z.ZodUnion<readonly [z.ZodObject<{
|
|
22
22
|
threshold: z.ZodNumber;
|
|
23
23
|
}, z.core.$strip>, z.ZodObject<{
|
|
24
|
-
|
|
24
|
+
maxTokensSize: z.ZodOptional<z.ZodNumber>;
|
|
25
25
|
responseTruncationThreshold: z.ZodOptional<z.ZodNumber>;
|
|
26
26
|
}, z.core.$strip>]>>;
|
|
27
27
|
callback: z.ZodString;
|
|
@@ -78,7 +78,7 @@ export declare const AgentDefinitionSchema: z.ZodObject<{
|
|
|
78
78
|
setting: z.ZodOptional<z.ZodUnion<readonly [z.ZodObject<{
|
|
79
79
|
threshold: z.ZodNumber;
|
|
80
80
|
}, z.core.$strip>, z.ZodObject<{
|
|
81
|
-
|
|
81
|
+
maxTokensSize: z.ZodOptional<z.ZodNumber>;
|
|
82
82
|
responseTruncationThreshold: z.ZodOptional<z.ZodNumber>;
|
|
83
83
|
}, z.core.$strip>]>>;
|
|
84
84
|
callback: z.ZodString;
|
package/dist/definition/index.js
CHANGED
|
@@ -64,7 +64,7 @@ export const HookConfigSchema = z.object({
|
|
|
64
64
|
}),
|
|
65
65
|
// For tool_response hooks
|
|
66
66
|
z.object({
|
|
67
|
-
|
|
67
|
+
maxTokensSize: z.number().min(0).optional(),
|
|
68
68
|
responseTruncationThreshold: z.number().min(0).max(100).optional(),
|
|
69
69
|
}),
|
|
70
70
|
])
|
|
@@ -48,7 +48,7 @@ export declare const zAgentRunnerParams: z.ZodObject<{
|
|
|
48
48
|
setting: z.ZodOptional<z.ZodUnion<readonly [z.ZodObject<{
|
|
49
49
|
threshold: z.ZodNumber;
|
|
50
50
|
}, z.core.$strip>, z.ZodObject<{
|
|
51
|
-
|
|
51
|
+
maxTokensSize: z.ZodOptional<z.ZodNumber>;
|
|
52
52
|
responseTruncationThreshold: z.ZodOptional<z.ZodNumber>;
|
|
53
53
|
}, z.core.$strip>]>>;
|
|
54
54
|
callback: z.ZodString;
|
|
@@ -137,6 +137,9 @@ export class HookExecutor {
|
|
|
137
137
|
if (result.truncationWarning !== undefined) {
|
|
138
138
|
response.truncationWarning = result.truncationWarning;
|
|
139
139
|
}
|
|
140
|
+
if (result.metadata !== undefined) {
|
|
141
|
+
response.metadata = result.metadata;
|
|
142
|
+
}
|
|
140
143
|
return response;
|
|
141
144
|
}
|
|
142
145
|
}
|
|
@@ -152,7 +155,11 @@ export class HookExecutor {
|
|
|
152
155
|
const notifications = [];
|
|
153
156
|
// Get threshold from settings
|
|
154
157
|
const settings = hook.setting;
|
|
155
|
-
|
|
158
|
+
// For notifications, calculate a percentage based on maxTokensSize relative to maxTokens
|
|
159
|
+
// Default to 20000 tokens, which we'll convert to a percentage for display
|
|
160
|
+
const maxTokensSize = settings?.maxTokensSize ?? 20000;
|
|
161
|
+
// Calculate approximate percentage: maxTokensSize / maxTokens * 100
|
|
162
|
+
const threshold = Math.min(100, Math.round((maxTokensSize / maxTokens) * 100));
|
|
156
163
|
// Capture start time and emit hook_triggered BEFORE callback runs
|
|
157
164
|
// This allows the UI to show the loading state immediately
|
|
158
165
|
const triggeredAt = Date.now();
|
|
@@ -185,6 +192,13 @@ export class HookExecutor {
|
|
|
185
192
|
toolResponse,
|
|
186
193
|
};
|
|
187
194
|
const result = await callback(hookContext);
|
|
195
|
+
logger.info("Hook callback result", {
|
|
196
|
+
hasMetadata: !!result.metadata,
|
|
197
|
+
metadataAction: result.metadata?.action,
|
|
198
|
+
hasModifiedOutput: !!result.metadata?.modifiedOutput,
|
|
199
|
+
modifiedOutputType: typeof result.metadata?.modifiedOutput,
|
|
200
|
+
toolCallId: toolResponse.toolCallId,
|
|
201
|
+
});
|
|
188
202
|
// Extract modified output and warnings from metadata
|
|
189
203
|
if (result.metadata?.modifiedOutput) {
|
|
190
204
|
// Hook took action - emit completed notification
|
|
@@ -194,6 +208,8 @@ export class HookExecutor {
|
|
|
194
208
|
response.truncationWarning = result.metadata
|
|
195
209
|
.truncationWarning;
|
|
196
210
|
}
|
|
211
|
+
// Include full metadata for persistence
|
|
212
|
+
response.metadata = result.metadata;
|
|
197
213
|
// Notify completion
|
|
198
214
|
this.emitNotification({
|
|
199
215
|
type: "hook_completed",
|