@townco/agent 0.1.142 → 0.1.144
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 +1 -1
- package/dist/acp-server/adapter.js +9 -1
- package/dist/acp-server/session-storage.d.ts +1 -1
- package/dist/acp-server/session-storage.js +1 -1
- package/dist/runner/hooks/predefined/compaction-tool.js +3 -5
- package/dist/runner/langchain/index.js +75 -61
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +7 -7
|
@@ -465,7 +465,15 @@ export class AgentAcpAdapter {
|
|
|
465
465
|
return citationSource;
|
|
466
466
|
};
|
|
467
467
|
// Check if this is a search results array (library__search_keyword)
|
|
468
|
-
|
|
468
|
+
// Results may be at top level or nested in structuredContent (MCP tool wrapper)
|
|
469
|
+
const structuredContent = typeof outputContent.structuredContent === "object" &&
|
|
470
|
+
outputContent.structuredContent !== null
|
|
471
|
+
? outputContent.structuredContent
|
|
472
|
+
: null;
|
|
473
|
+
const results = outputContent.results ??
|
|
474
|
+
outputContent.documents ??
|
|
475
|
+
structuredContent?.results ??
|
|
476
|
+
structuredContent?.documents;
|
|
469
477
|
if (Array.isArray(results)) {
|
|
470
478
|
// Handle array of search results
|
|
471
479
|
logger.debug("Processing library search results array", {
|
|
@@ -168,7 +168,7 @@ const sessionMetadataSchema = z.object({
|
|
|
168
168
|
// Citation schemas - matches SourceSchema from packages/ui/src/core/schemas/source.ts
|
|
169
169
|
const persistedCitationSourceSchema = z.object({
|
|
170
170
|
id: z.string(),
|
|
171
|
-
url: z.string(),
|
|
171
|
+
url: z.string().optional(), // Optional for backward compatibility with sessions that have missing URLs
|
|
172
172
|
title: z.string(),
|
|
173
173
|
snippet: z.string().optional(),
|
|
174
174
|
favicon: z.string().optional(),
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { ChatAnthropic } from "@langchain/anthropic";
|
|
2
1
|
import { HumanMessage, SystemMessage } from "@langchain/core/messages";
|
|
3
2
|
import { createLogger } from "../../../logger.js";
|
|
3
|
+
import { createModelFromString } from "../../langchain/model-factory.js";
|
|
4
4
|
import { createContextEntry, createFullMessageEntry, } from "../types";
|
|
5
5
|
import { applyTokenPadding } from "./token-utils.js";
|
|
6
6
|
const logger = createLogger("compaction-tool");
|
|
@@ -67,10 +67,8 @@ export const compactionTool = async (ctx) => {
|
|
|
67
67
|
const hasLibraryMcp = ctx.agent.mcps?.some((mcp) => typeof mcp === "string" ? mcp === "library" : mcp.name === "library");
|
|
68
68
|
try {
|
|
69
69
|
// Create the LLM client using the same model as the agent
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
temperature: 0,
|
|
73
|
-
});
|
|
70
|
+
// Use model factory to properly handle town- prefixed models (routes through shed proxy)
|
|
71
|
+
const model = await createModelFromString(ctx.model);
|
|
74
72
|
// Build the conversation history to compact
|
|
75
73
|
const messagesToCompact = ctx.session.messages;
|
|
76
74
|
// Convert session messages to text for context, including tool calls and results
|
|
@@ -185,7 +185,15 @@ function extractSourcesBeforeCompaction(toolName, rawOutput) {
|
|
|
185
185
|
};
|
|
186
186
|
};
|
|
187
187
|
// Check for results array (library__search_keyword, library__semantic_search)
|
|
188
|
-
|
|
188
|
+
// Results may be at top level or nested in structuredContent (MCP tool wrapper)
|
|
189
|
+
const structuredContent = typeof actualOutput.structuredContent === "object" &&
|
|
190
|
+
actualOutput.structuredContent !== null
|
|
191
|
+
? actualOutput.structuredContent
|
|
192
|
+
: null;
|
|
193
|
+
const results = actualOutput.results ??
|
|
194
|
+
actualOutput.documents ??
|
|
195
|
+
structuredContent?.results ??
|
|
196
|
+
structuredContent?.documents;
|
|
189
197
|
if (Array.isArray(results)) {
|
|
190
198
|
for (const result of results) {
|
|
191
199
|
if (result && typeof result === "object") {
|
|
@@ -202,6 +210,32 @@ function extractSourcesBeforeCompaction(toolName, rawOutput) {
|
|
|
202
210
|
sources.push(source);
|
|
203
211
|
}
|
|
204
212
|
}
|
|
213
|
+
// Handle subagent tool outputs (SubagentResult format: { text, sources })
|
|
214
|
+
const isSubagentTool = toolName === "subagent" || toolName === "Task";
|
|
215
|
+
if (isSubagentTool && Array.isArray(actualOutput.sources)) {
|
|
216
|
+
for (const source of actualOutput.sources) {
|
|
217
|
+
if (source &&
|
|
218
|
+
typeof source === "object" &&
|
|
219
|
+
typeof source.url === "string" &&
|
|
220
|
+
source.url) {
|
|
221
|
+
sourceCounter++;
|
|
222
|
+
sources.push({
|
|
223
|
+
id: typeof source.id === "string" ? source.id : String(sourceCounter),
|
|
224
|
+
url: source.url,
|
|
225
|
+
title: typeof source.title === "string" ? source.title : "Untitled",
|
|
226
|
+
snippet: typeof source.snippet === "string"
|
|
227
|
+
? source.snippet.slice(0, 200)
|
|
228
|
+
: undefined,
|
|
229
|
+
favicon: typeof source.favicon === "string"
|
|
230
|
+
? source.favicon
|
|
231
|
+
: getFaviconFromUrl(source.url),
|
|
232
|
+
sourceName: typeof source.sourceName === "string"
|
|
233
|
+
? source.sourceName
|
|
234
|
+
: getSourceNameFromUrl(source.url),
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
205
239
|
return sources;
|
|
206
240
|
}
|
|
207
241
|
function stableStringify(value) {
|
|
@@ -742,7 +776,41 @@ export class LangchainAgent {
|
|
|
742
776
|
const toolCallId = hasInflightToolCompaction
|
|
743
777
|
? await consumeToolCallId(originalTool.name, input)
|
|
744
778
|
: `unknown_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
|
|
745
|
-
|
|
779
|
+
let result = await originalTool.invoke(input);
|
|
780
|
+
// Apply subagent source renumbering BEFORE extraction
|
|
781
|
+
// This ensures pre-extracted sources have the same IDs as the text references
|
|
782
|
+
const isSubagentToolResult = originalTool.name === SUBAGENT_TOOL_NAME &&
|
|
783
|
+
result &&
|
|
784
|
+
typeof result === "object" &&
|
|
785
|
+
"sources" in result &&
|
|
786
|
+
Array.isArray(result.sources) &&
|
|
787
|
+
result.sources.length > 0;
|
|
788
|
+
if (isSubagentToolResult) {
|
|
789
|
+
const subagentResult = result;
|
|
790
|
+
subagentCallCounter++;
|
|
791
|
+
const baseOffset = subagentCallCounter * 1000;
|
|
792
|
+
let sourceIndex = 0;
|
|
793
|
+
// Create ID mapping and re-number sources with offset
|
|
794
|
+
const idMapping = new Map();
|
|
795
|
+
const renumberedSources = subagentResult.sources.map((source) => {
|
|
796
|
+
sourceIndex++;
|
|
797
|
+
const newId = String(baseOffset + sourceIndex);
|
|
798
|
+
idMapping.set(source.id, newId);
|
|
799
|
+
return { ...source, id: newId };
|
|
800
|
+
});
|
|
801
|
+
// Update citation references in the text [[oldId]] -> [[newId]]
|
|
802
|
+
let updatedText = subagentResult.text;
|
|
803
|
+
for (const [oldId, newId] of idMapping) {
|
|
804
|
+
const pattern = new RegExp(`\\[\\[${oldId}\\]\\]`, "g");
|
|
805
|
+
updatedText = updatedText.replace(pattern, `[[${newId}]]`);
|
|
806
|
+
}
|
|
807
|
+
_logger.info("Re-numbered subagent citation sources (in-flight)", {
|
|
808
|
+
subagentCall: subagentCallCounter,
|
|
809
|
+
originalCount: subagentResult.sources.length,
|
|
810
|
+
idRange: `${baseOffset + 1}-${baseOffset + sourceIndex}`,
|
|
811
|
+
});
|
|
812
|
+
result = { text: updatedText, sources: renumberedSources };
|
|
813
|
+
}
|
|
746
814
|
if (!inflightHookExecutor || !hasInflightToolCompaction) {
|
|
747
815
|
return result;
|
|
748
816
|
}
|
|
@@ -756,6 +824,7 @@ export class LangchainAgent {
|
|
|
756
824
|
const outputTokens = await countToolResultTokens(rawOutput);
|
|
757
825
|
// Extract citation sources BEFORE compaction to preserve URLs
|
|
758
826
|
// Compaction LLM may remove URLs as "unnecessary" during summarization
|
|
827
|
+
// NOTE: For subagent tools, sources are already renumbered above
|
|
759
828
|
const preExtractedSources = extractSourcesBeforeCompaction(originalTool.name, rawOutput);
|
|
760
829
|
// Include current prompt as the last user message for better context.
|
|
761
830
|
const nowIso = new Date().toISOString();
|
|
@@ -899,65 +968,10 @@ export class LangchainAgent {
|
|
|
899
968
|
allowedToolNames.has(t.name));
|
|
900
969
|
});
|
|
901
970
|
}
|
|
902
|
-
//
|
|
903
|
-
//
|
|
904
|
-
//
|
|
905
|
-
//
|
|
906
|
-
finalTools = finalTools.map((t) => {
|
|
907
|
-
if (t.name !== SUBAGENT_TOOL_NAME) {
|
|
908
|
-
return t;
|
|
909
|
-
}
|
|
910
|
-
const wrappedFunc = async (input) => {
|
|
911
|
-
const result = (await t.invoke(input));
|
|
912
|
-
// Check if result has sources to re-number
|
|
913
|
-
if (!result ||
|
|
914
|
-
typeof result !== "object" ||
|
|
915
|
-
!Array.isArray(result.sources) ||
|
|
916
|
-
result.sources.length === 0) {
|
|
917
|
-
return result;
|
|
918
|
-
}
|
|
919
|
-
// Increment subagent call counter and calculate base offset
|
|
920
|
-
subagentCallCounter++;
|
|
921
|
-
const baseOffset = subagentCallCounter * 1000;
|
|
922
|
-
let sourceIndex = 0;
|
|
923
|
-
// Create ID mapping and re-number sources with offset
|
|
924
|
-
const idMapping = new Map();
|
|
925
|
-
const renumberedSources = result.sources.map((source) => {
|
|
926
|
-
sourceIndex++;
|
|
927
|
-
const newId = String(baseOffset + sourceIndex);
|
|
928
|
-
idMapping.set(source.id, newId);
|
|
929
|
-
return { ...source, id: newId };
|
|
930
|
-
});
|
|
931
|
-
// Update citation references in the text [[oldId]] -> [[newId]]
|
|
932
|
-
let updatedText = result.text;
|
|
933
|
-
for (const [oldId, newId] of idMapping) {
|
|
934
|
-
const pattern = new RegExp(`\\[\\[${oldId}\\]\\]`, "g");
|
|
935
|
-
updatedText = updatedText.replace(pattern, `[[${newId}]]`);
|
|
936
|
-
}
|
|
937
|
-
_logger.info("Re-numbered subagent citation sources", {
|
|
938
|
-
subagentCall: subagentCallCounter,
|
|
939
|
-
originalCount: result.sources.length,
|
|
940
|
-
idRange: `${baseOffset + 1}-${baseOffset + sourceIndex}`,
|
|
941
|
-
});
|
|
942
|
-
return { text: updatedText, sources: renumberedSources };
|
|
943
|
-
};
|
|
944
|
-
// Create new tool with wrapped function
|
|
945
|
-
// biome-ignore lint/suspicious/noExplicitAny: Need to pass function with dynamic signature
|
|
946
|
-
const wrappedTool = tool(wrappedFunc, {
|
|
947
|
-
name: t.name,
|
|
948
|
-
description: t.description,
|
|
949
|
-
// biome-ignore lint/suspicious/noExplicitAny: Accessing internal schema property
|
|
950
|
-
schema: t.schema,
|
|
951
|
-
});
|
|
952
|
-
// Preserve metadata
|
|
953
|
-
// biome-ignore lint/suspicious/noExplicitAny: Need to add custom properties to LangChain tool
|
|
954
|
-
wrappedTool.prettyName = t.prettyName;
|
|
955
|
-
// biome-ignore lint/suspicious/noExplicitAny: Need to add custom properties to LangChain tool
|
|
956
|
-
wrappedTool.icon = t.icon;
|
|
957
|
-
// biome-ignore lint/suspicious/noExplicitAny: Need to preserve subagentConfigs for metadata
|
|
958
|
-
wrappedTool.subagentConfigs = t.subagentConfigs;
|
|
959
|
-
return wrappedTool;
|
|
960
|
-
});
|
|
971
|
+
// NOTE: Subagent source renumbering now happens earlier in the wrappedTools
|
|
972
|
+
// wrapper (around line 1050) to ensure pre-extracted sources have matching IDs.
|
|
973
|
+
// This ensures that when sources are extracted before compaction, they already
|
|
974
|
+
// have the renumbered IDs (1001+, 2001+, etc.) that match the text references.
|
|
961
975
|
// Create the model instance using the factory
|
|
962
976
|
// This detects the provider from the model string:
|
|
963
977
|
// - "gemini-2.0-flash" → Google Generative AI
|