@townco/agent 0.1.82 → 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 +150 -49
- package/dist/acp-server/http.js +56 -1
- package/dist/acp-server/session-storage.d.ts +44 -12
- package/dist/acp-server/session-storage.js +153 -59
- package/dist/definition/index.d.ts +2 -2
- package/dist/definition/index.js +1 -1
- package/dist/runner/agent-runner.d.ts +4 -2
- package/dist/runner/hooks/executor.d.ts +1 -0
- package/dist/runner/hooks/executor.js +18 -2
- package/dist/runner/hooks/predefined/compaction-tool.js +3 -2
- package/dist/runner/hooks/predefined/tool-response-compactor.d.ts +0 -4
- package/dist/runner/hooks/predefined/tool-response-compactor.js +30 -16
- package/dist/runner/hooks/types.d.ts +4 -5
- package/dist/runner/langchain/index.d.ts +1 -0
- package/dist/runner/langchain/index.js +156 -33
- package/dist/runner/langchain/tools/artifacts.d.ts +68 -0
- package/dist/runner/langchain/tools/artifacts.js +466 -0
- package/dist/runner/langchain/tools/browser.js +15 -3
- package/dist/runner/langchain/tools/filesystem.d.ts +8 -4
- package/dist/runner/langchain/tools/filesystem.js +118 -82
- package/dist/runner/langchain/tools/generate_image.d.ts +19 -0
- package/dist/runner/langchain/tools/generate_image.js +54 -14
- package/dist/runner/langchain/tools/subagent.js +2 -2
- package/dist/runner/langchain/tools/todo.js +3 -0
- package/dist/runner/langchain/tools/web_search.js +6 -0
- package/dist/runner/session-context.d.ts +40 -0
- package/dist/runner/session-context.js +69 -0
- package/dist/runner/tools.d.ts +2 -2
- package/dist/runner/tools.js +2 -0
- package/dist/scaffold/project-scaffold.js +7 -3
- package/dist/telemetry/setup.js +1 -1
- 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/dist/utils/token-counter.js +2 -2
- package/package.json +10 -10
- package/templates/index.ts +1 -1
|
@@ -278,8 +278,10 @@ export class AgentAcpAdapter {
|
|
|
278
278
|
return response;
|
|
279
279
|
}
|
|
280
280
|
async newSession(params) {
|
|
281
|
-
//
|
|
282
|
-
|
|
281
|
+
// Use sessionId from params if provided (HTTP transport injects it),
|
|
282
|
+
// otherwise generate a unique session ID for this session
|
|
283
|
+
const sessionId = params.sessionId ??
|
|
284
|
+
Math.random().toString(36).substring(2);
|
|
283
285
|
// Extract configOverrides from _meta if provided (Town Hall comparison feature)
|
|
284
286
|
const configOverrides = params._meta?.configOverrides;
|
|
285
287
|
const sessionData = {
|
|
@@ -380,7 +382,7 @@ export class AgentAcpAdapter {
|
|
|
380
382
|
: {}),
|
|
381
383
|
...block._meta,
|
|
382
384
|
};
|
|
383
|
-
// Debug: log subagent data being replayed
|
|
385
|
+
// Debug: log subagent data and compaction metadata being replayed
|
|
384
386
|
logger.info("Replaying tool_call", {
|
|
385
387
|
toolCallId: block.id,
|
|
386
388
|
title: block.title,
|
|
@@ -389,6 +391,8 @@ export class AgentAcpAdapter {
|
|
|
389
391
|
hasSubagentSessionId: !!block.subagentSessionId,
|
|
390
392
|
hasSubagentMessages: !!block.subagentMessages,
|
|
391
393
|
subagentMessagesCount: block.subagentMessages?.length,
|
|
394
|
+
blockMeta: block._meta,
|
|
395
|
+
replayMeta,
|
|
392
396
|
});
|
|
393
397
|
this.connection.sessionUpdate({
|
|
394
398
|
sessionId: params.sessionId,
|
|
@@ -434,6 +438,7 @@ export class AgentAcpAdapter {
|
|
|
434
438
|
status: block.status,
|
|
435
439
|
rawOutput: block.rawOutput,
|
|
436
440
|
error: block.error,
|
|
441
|
+
_meta: replayMeta,
|
|
437
442
|
},
|
|
438
443
|
});
|
|
439
444
|
}
|
|
@@ -598,9 +603,8 @@ export class AgentAcpAdapter {
|
|
|
598
603
|
contextMessages.push(entry.message);
|
|
599
604
|
}
|
|
600
605
|
}
|
|
601
|
-
// Calculate context size -
|
|
602
|
-
const context_size = calculateContextSize(contextMessages, this.agent.definition.systemPrompt ?? undefined,
|
|
603
|
-
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
|
|
604
608
|
this.currentMcpOverheadTokens, // Include MCP overhead
|
|
605
609
|
getModelContextWindow(this.agent.definition.model));
|
|
606
610
|
const contextSnapshot = createContextSnapshot(session.messages.length - 1, // Exclude the newly added user message (it will be passed separately via prompt)
|
|
@@ -619,7 +623,7 @@ export class AgentAcpAdapter {
|
|
|
619
623
|
}
|
|
620
624
|
};
|
|
621
625
|
// Declare agentResponse and turnTokenUsage outside try block so they're accessible after catch
|
|
622
|
-
let
|
|
626
|
+
let _agentResponse;
|
|
623
627
|
// Track accumulated token usage during the turn
|
|
624
628
|
const turnTokenUsage = {
|
|
625
629
|
inputTokens: 0,
|
|
@@ -653,8 +657,8 @@ export class AgentAcpAdapter {
|
|
|
653
657
|
latestContextEntry: session.context.length > 0 &&
|
|
654
658
|
session.context[session.context.length - 1]
|
|
655
659
|
? {
|
|
656
|
-
messageCount: session.context[session.context.length - 1]
|
|
657
|
-
contextSize: session.context[session.context.length - 1]
|
|
660
|
+
messageCount: session.context[session.context.length - 1]?.messages.length,
|
|
661
|
+
contextSize: session.context[session.context.length - 1]?.context_size,
|
|
658
662
|
}
|
|
659
663
|
: null,
|
|
660
664
|
});
|
|
@@ -662,6 +666,8 @@ export class AgentAcpAdapter {
|
|
|
662
666
|
prompt: params.prompt,
|
|
663
667
|
sessionId: params.sessionId,
|
|
664
668
|
messageId,
|
|
669
|
+
// Pass agent directory for session-scoped file storage (only if defined)
|
|
670
|
+
...(this.agentDir ? { agentDir: this.agentDir } : {}),
|
|
665
671
|
// Pass resolved context messages to agent
|
|
666
672
|
contextMessages,
|
|
667
673
|
};
|
|
@@ -717,19 +723,8 @@ export class AgentAcpAdapter {
|
|
|
717
723
|
if (tokenUsage.inputTokens !== undefined &&
|
|
718
724
|
tokenUsage.inputTokens > 0) {
|
|
719
725
|
turnTokenUsage.inputTokens = tokenUsage.inputTokens;
|
|
720
|
-
//
|
|
721
|
-
|
|
722
|
-
const lastContext = session.context[session.context.length - 1];
|
|
723
|
-
if (lastContext) {
|
|
724
|
-
lastContext.context_size.llmReportedInputTokens =
|
|
725
|
-
tokenUsage.inputTokens;
|
|
726
|
-
logger.debug("Updated context entry with LLM-reported tokens", {
|
|
727
|
-
contextIndex: session.context.length - 1,
|
|
728
|
-
llmReportedTokens: tokenUsage.inputTokens,
|
|
729
|
-
estimatedTokens: lastContext.context_size.totalEstimated,
|
|
730
|
-
});
|
|
731
|
-
}
|
|
732
|
-
}
|
|
726
|
+
// Note: We no longer update context entries with LLM-reported tokens
|
|
727
|
+
// as they can cause mismatches when updated on mid-turn snapshots
|
|
733
728
|
}
|
|
734
729
|
turnTokenUsage.outputTokens += tokenUsage.outputTokens ?? 0;
|
|
735
730
|
turnTokenUsage.totalTokens += tokenUsage.totalTokens ?? 0;
|
|
@@ -906,19 +901,71 @@ export class AgentAcpAdapter {
|
|
|
906
901
|
// Get the raw output
|
|
907
902
|
let rawOutput = outputMsg.rawOutput || outputMsg.output;
|
|
908
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
|
+
}
|
|
909
959
|
if (rawOutput && !this.noSession) {
|
|
910
960
|
// Execute tool_response hooks if configured
|
|
911
961
|
const hooks = this.agent.definition.hooks ?? [];
|
|
912
962
|
if (hooks.some((h) => h.type === "tool_response")) {
|
|
913
963
|
const latestContext = session.context[session.context.length - 1];
|
|
914
|
-
// Use
|
|
915
|
-
const currentContextTokens =
|
|
916
|
-
// 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
|
|
917
967
|
if (latestContext?.context_size) {
|
|
918
|
-
const contextSizeForUI =
|
|
919
|
-
...latestContext.context_size,
|
|
920
|
-
totalEstimated: currentContextTokens,
|
|
921
|
-
};
|
|
968
|
+
const contextSizeForUI = latestContext.context_size;
|
|
922
969
|
this.connection.sessionUpdate({
|
|
923
970
|
sessionId: params.sessionId,
|
|
924
971
|
update: {
|
|
@@ -960,6 +1007,12 @@ export class AgentAcpAdapter {
|
|
|
960
1007
|
});
|
|
961
1008
|
// Note: Notifications are now sent in real-time via the callback
|
|
962
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);
|
|
963
1016
|
// Apply modifications if hook returned them
|
|
964
1017
|
if (hookResult.modifiedOutput) {
|
|
965
1018
|
rawOutput = hookResult.modifiedOutput;
|
|
@@ -970,6 +1023,49 @@ export class AgentAcpAdapter {
|
|
|
970
1023
|
});
|
|
971
1024
|
}
|
|
972
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
|
+
}
|
|
973
1069
|
}
|
|
974
1070
|
}
|
|
975
1071
|
// Store the (potentially modified) output
|
|
@@ -983,6 +1079,22 @@ export class AgentAcpAdapter {
|
|
|
983
1079
|
}
|
|
984
1080
|
toolCallBlock._meta.truncationWarning = truncationWarning;
|
|
985
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
|
+
}
|
|
986
1098
|
// Note: content blocks are handled by the transport for display
|
|
987
1099
|
// We store the raw output here for session persistence
|
|
988
1100
|
// Create mid-turn context snapshot after tool completes
|
|
@@ -1058,8 +1170,7 @@ export class AgentAcpAdapter {
|
|
|
1058
1170
|
}
|
|
1059
1171
|
}
|
|
1060
1172
|
// Calculate context size - tool result is now in the message, but hasn't been sent to LLM yet
|
|
1061
|
-
const context_size = calculateContextSize(contextMessages, this.agent.definition.systemPrompt ?? undefined,
|
|
1062
|
-
this.currentToolOverheadTokens, // Include tool overhead
|
|
1173
|
+
const context_size = calculateContextSize(contextMessages, this.agent.definition.systemPrompt ?? undefined, this.currentToolOverheadTokens, // Include tool overhead
|
|
1063
1174
|
this.currentMcpOverheadTokens, // Include MCP overhead
|
|
1064
1175
|
getModelContextWindow(this.agent.definition.model));
|
|
1065
1176
|
// Create snapshot with a pointer to the partial message (not a full copy!)
|
|
@@ -1154,7 +1265,7 @@ export class AgentAcpAdapter {
|
|
|
1154
1265
|
iterResult = await generator.next();
|
|
1155
1266
|
}
|
|
1156
1267
|
// Capture the return value (PromptResponse with tokenUsage)
|
|
1157
|
-
|
|
1268
|
+
_agentResponse = iterResult.value;
|
|
1158
1269
|
// Flush any remaining pending text
|
|
1159
1270
|
flushPendingText();
|
|
1160
1271
|
}
|
|
@@ -1213,9 +1324,8 @@ export class AgentAcpAdapter {
|
|
|
1213
1324
|
contextMessages.push(entry.message);
|
|
1214
1325
|
}
|
|
1215
1326
|
}
|
|
1216
|
-
// Calculate context size
|
|
1217
|
-
const context_size = calculateContextSize(contextMessages, this.agent.definition.systemPrompt ?? undefined,
|
|
1218
|
-
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
|
|
1219
1329
|
this.currentMcpOverheadTokens, // Include MCP overhead
|
|
1220
1330
|
getModelContextWindow(this.agent.definition.model));
|
|
1221
1331
|
const contextSnapshot = createContextSnapshot(session.messages.length, new Date().toISOString(), previousContext, context_size);
|
|
@@ -1293,28 +1403,19 @@ export class AgentAcpAdapter {
|
|
|
1293
1403
|
context: session.context,
|
|
1294
1404
|
requestParams: session.requestParams,
|
|
1295
1405
|
};
|
|
1296
|
-
// Get
|
|
1406
|
+
// Get input token count from latest context entry
|
|
1297
1407
|
const latestContext = session.context.length > 0
|
|
1298
1408
|
? session.context[session.context.length - 1]
|
|
1299
1409
|
: undefined;
|
|
1300
|
-
// Use
|
|
1301
|
-
|
|
1302
|
-
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;
|
|
1303
1412
|
logger.debug("Using tokens for hook execution", {
|
|
1304
|
-
llmReported: latestContext?.context_size.llmReportedInputTokens,
|
|
1305
1413
|
estimated: latestContext?.context_size.totalEstimated,
|
|
1306
1414
|
used: actualInputTokens,
|
|
1307
1415
|
});
|
|
1308
|
-
// Send the context size to the UI
|
|
1309
|
-
// This ensures the UI percentage matches what the hook sees
|
|
1416
|
+
// Send the context size to the UI
|
|
1310
1417
|
if (latestContext?.context_size) {
|
|
1311
|
-
|
|
1312
|
-
// so UI can calculate the same percentage
|
|
1313
|
-
const contextSizeForUI = {
|
|
1314
|
-
...latestContext.context_size,
|
|
1315
|
-
// Override with the max value we computed (what hooks actually use)
|
|
1316
|
-
totalEstimated: actualInputTokens,
|
|
1317
|
-
};
|
|
1418
|
+
const contextSizeForUI = latestContext.context_size;
|
|
1318
1419
|
this.connection.sessionUpdate({
|
|
1319
1420
|
sessionId,
|
|
1320
1421
|
update: {
|
package/dist/acp-server/http.js
CHANGED
|
@@ -241,7 +241,7 @@ export function makeHttpTransport(agent, agentDir, agentName) {
|
|
|
241
241
|
}
|
|
242
242
|
// Regular session update - send via PubSub
|
|
243
243
|
const channel = safeChannelName("notifications", msgSessionId);
|
|
244
|
-
const { payload,
|
|
244
|
+
const { payload, compressedSize } = compressIfNeeded(rawMsg);
|
|
245
245
|
if (compressedSize <= 7500) {
|
|
246
246
|
const escapedPayload = payload.replace(/'/g, "''");
|
|
247
247
|
try {
|
|
@@ -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
|
}
|
|
@@ -147,7 +148,8 @@ export interface StoredSession {
|
|
|
147
148
|
}
|
|
148
149
|
/**
|
|
149
150
|
* File-based session storage
|
|
150
|
-
* Stores sessions in agents/<agent-name>/.sessions/<session_id
|
|
151
|
+
* Stores sessions in agents/<agent-name>/.sessions/<session_id>/session.json
|
|
152
|
+
* (Legacy: agents/<agent-name>/.sessions/<session_id>.json)
|
|
151
153
|
*/
|
|
152
154
|
export declare class SessionStorage {
|
|
153
155
|
private sessionsDir;
|
|
@@ -159,13 +161,18 @@ export declare class SessionStorage {
|
|
|
159
161
|
*/
|
|
160
162
|
constructor(agentDir: string, agentName: string);
|
|
161
163
|
/**
|
|
162
|
-
* Ensure the
|
|
164
|
+
* Ensure the session directory exists
|
|
163
165
|
*/
|
|
164
|
-
private
|
|
166
|
+
private ensureSessionDir;
|
|
165
167
|
/**
|
|
166
168
|
* Get the file path for a session
|
|
167
169
|
*/
|
|
168
170
|
private getSessionPath;
|
|
171
|
+
/**
|
|
172
|
+
* Get the legacy file path for a session (for backwards compatibility)
|
|
173
|
+
* Legacy format: .sessions/<session_id>.json (flat file, not directory)
|
|
174
|
+
*/
|
|
175
|
+
private getLegacySessionPath;
|
|
169
176
|
/**
|
|
170
177
|
* Save a session to disk
|
|
171
178
|
* Uses atomic write (write to temp file, then rename)
|
|
@@ -177,18 +184,19 @@ export declare class SessionStorage {
|
|
|
177
184
|
loadSession(sessionId: string): Promise<StoredSession | null>;
|
|
178
185
|
/**
|
|
179
186
|
* Synchronous session loading (for internal use)
|
|
187
|
+
* Checks new location first, falls back to legacy location
|
|
180
188
|
*/
|
|
181
189
|
private loadSessionSync;
|
|
182
190
|
/**
|
|
183
|
-
* Check if a session exists
|
|
191
|
+
* Check if a session exists (checks both new and legacy locations)
|
|
184
192
|
*/
|
|
185
193
|
sessionExists(sessionId: string): boolean;
|
|
186
194
|
/**
|
|
187
|
-
* Delete a session
|
|
195
|
+
* Delete a session (deletes entire session directory or legacy file)
|
|
188
196
|
*/
|
|
189
197
|
deleteSession(sessionId: string): Promise<boolean>;
|
|
190
198
|
/**
|
|
191
|
-
* List all session IDs
|
|
199
|
+
* List all session IDs (checks both new and legacy locations)
|
|
192
200
|
*/
|
|
193
201
|
listSessions(): Promise<string[]>;
|
|
194
202
|
/**
|
|
@@ -205,4 +213,28 @@ export declare class SessionStorage {
|
|
|
205
213
|
messageCount: number;
|
|
206
214
|
firstUserMessage?: string;
|
|
207
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;
|
|
208
240
|
}
|