@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.
@@ -17,7 +17,7 @@ export interface SubagentModeExtension {
17
17
  */
18
18
  export interface CitationSource {
19
19
  id: string;
20
- url: string;
20
+ url?: string | undefined;
21
21
  title: string;
22
22
  snippet?: string | undefined;
23
23
  favicon?: string | undefined;
@@ -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
- const results = outputContent.results ?? outputContent.documents;
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", {
@@ -164,7 +164,7 @@ export interface StoredSession {
164
164
  */
165
165
  export interface PersistedCitationSource {
166
166
  id: string;
167
- url: string;
167
+ url?: string | undefined;
168
168
  title: string;
169
169
  snippet?: string | undefined;
170
170
  favicon?: string | undefined;
@@ -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
- const model = new ChatAnthropic({
71
- model: ctx.model,
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
- const results = actualOutput.results ?? actualOutput.documents;
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
- const result = await originalTool.invoke(input);
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
- // Wrap the subagent tool to re-number citation sources
903
- // This ensures sources from subagents get unique IDs that don't conflict
904
- // with the parent agent's own sources or other subagent calls
905
- // Each subagent call gets a unique ID range (1000+, 2000+, etc.)
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