@townco/agent 0.1.138 → 0.1.140
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.
|
@@ -1736,55 +1736,89 @@ export class AgentAcpAdapter {
|
|
|
1736
1736
|
finalTokens: compactionMeta.finalTokens,
|
|
1737
1737
|
hasOriginalContent: !!compactionMeta.originalContent,
|
|
1738
1738
|
});
|
|
1739
|
+
// Save pre-extracted sources before removing metadata
|
|
1740
|
+
const preExtractedSources = compactionMeta.preExtractedSources;
|
|
1739
1741
|
// Remove the metadata from rawOutput to keep it clean
|
|
1740
1742
|
const { _compactionMeta: _, ...cleanOutput } = rawOutput;
|
|
1741
1743
|
rawOutput = cleanOutput;
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
session.sources.push(...
|
|
1744
|
+
// If we have pre-extracted sources from the runner (before compaction),
|
|
1745
|
+
// use them directly instead of trying to extract from compacted output
|
|
1746
|
+
if (preExtractedSources &&
|
|
1747
|
+
preExtractedSources.length > 0 &&
|
|
1748
|
+
!this.noSession) {
|
|
1749
|
+
const sourcesForSession = preExtractedSources.map((s) => ({
|
|
1750
|
+
id: s.id,
|
|
1751
|
+
url: s.url,
|
|
1752
|
+
title: s.title,
|
|
1753
|
+
toolCallId: outputMsg.toolCallId,
|
|
1754
|
+
...(s.snippet && { snippet: s.snippet }),
|
|
1755
|
+
...(s.favicon && { favicon: s.favicon }),
|
|
1756
|
+
...(s.sourceName && { sourceName: s.sourceName }),
|
|
1757
|
+
}));
|
|
1758
|
+
session.sources.push(...sourcesForSession);
|
|
1757
1759
|
this.connection.sessionUpdate({
|
|
1758
1760
|
sessionId: params.sessionId,
|
|
1759
1761
|
update: {
|
|
1760
1762
|
sessionUpdate: "sources",
|
|
1761
|
-
sources:
|
|
1763
|
+
sources: sourcesForSession,
|
|
1762
1764
|
},
|
|
1763
1765
|
});
|
|
1764
|
-
logger.info("
|
|
1766
|
+
logger.info("Using pre-extracted sources from runner (preserved before compaction)", {
|
|
1765
1767
|
toolCallId: outputMsg.toolCallId,
|
|
1766
|
-
|
|
1768
|
+
toolName: toolCallBlock.title,
|
|
1769
|
+
sourcesCount: sourcesForSession.length,
|
|
1767
1770
|
});
|
|
1768
1771
|
}
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
if
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1772
|
+
}
|
|
1773
|
+
if (rawOutput && !this.noSession) {
|
|
1774
|
+
// Check if we already have sources from pre-extraction
|
|
1775
|
+
const alreadyHasSources = session.sources.some((s) => s.toolCallId === outputMsg.toolCallId);
|
|
1776
|
+
if (!alreadyHasSources) {
|
|
1777
|
+
// IMPORTANT: Extract citations BEFORE hook execution
|
|
1778
|
+
// Hooks may compact/modify rawOutput, losing citation data
|
|
1779
|
+
// We extract from the original output to preserve citation sources
|
|
1780
|
+
const rawOutputForCitations = rawOutput;
|
|
1781
|
+
logger.debug("Extracting citations BEFORE compaction hooks", {
|
|
1782
|
+
toolCallId: outputMsg.toolCallId,
|
|
1783
|
+
toolName: toolCallBlock.title,
|
|
1784
|
+
rawOutputType: typeof rawOutputForCitations,
|
|
1785
|
+
});
|
|
1786
|
+
// Handle subagent tool outputs first
|
|
1787
|
+
const subagentResult = this.extractSubagentSources(rawOutput, outputMsg.toolCallId);
|
|
1788
|
+
if (subagentResult && subagentResult.sources.length > 0) {
|
|
1789
|
+
session.sources.push(...subagentResult.sources);
|
|
1775
1790
|
this.connection.sessionUpdate({
|
|
1776
1791
|
sessionId: params.sessionId,
|
|
1777
1792
|
update: {
|
|
1778
1793
|
sessionUpdate: "sources",
|
|
1779
|
-
sources:
|
|
1794
|
+
sources: subagentResult.sources,
|
|
1780
1795
|
},
|
|
1781
1796
|
});
|
|
1782
|
-
logger.info("Extracted citation sources from
|
|
1797
|
+
logger.info("Extracted citation sources from subagent (pre-compaction)", {
|
|
1783
1798
|
toolCallId: outputMsg.toolCallId,
|
|
1784
|
-
|
|
1785
|
-
sourcesCount: extractedSources.length,
|
|
1799
|
+
sourcesCount: subagentResult.sources.length,
|
|
1786
1800
|
});
|
|
1787
1801
|
}
|
|
1802
|
+
// Extract from regular tool outputs (WebSearch, WebFetch, MCP tools)
|
|
1803
|
+
const subagentHadSources = subagentResult && subagentResult.sources.length > 0;
|
|
1804
|
+
if (!subagentHadSources) {
|
|
1805
|
+
const extractedSources = this.extractSourcesFromToolOutput(toolCallBlock.title, rawOutput, outputMsg.toolCallId, session);
|
|
1806
|
+
if (extractedSources.length > 0) {
|
|
1807
|
+
session.sources.push(...extractedSources);
|
|
1808
|
+
this.connection.sessionUpdate({
|
|
1809
|
+
sessionId: params.sessionId,
|
|
1810
|
+
update: {
|
|
1811
|
+
sessionUpdate: "sources",
|
|
1812
|
+
sources: extractedSources,
|
|
1813
|
+
},
|
|
1814
|
+
});
|
|
1815
|
+
logger.info("Extracted citation sources from tool output (pre-compaction)", {
|
|
1816
|
+
toolCallId: outputMsg.toolCallId,
|
|
1817
|
+
toolName: toolCallBlock.title,
|
|
1818
|
+
sourcesCount: extractedSources.length,
|
|
1819
|
+
});
|
|
1820
|
+
}
|
|
1821
|
+
}
|
|
1788
1822
|
}
|
|
1789
1823
|
// Execute tool_response hooks if configured
|
|
1790
1824
|
const hooks = this.agent.definition.hooks ?? [];
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
import { existsSync, mkdirSync, readdirSync, readFileSync, unlinkSync, writeFileSync, } from "node:fs";
|
|
1
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, renameSync, unlinkSync, writeFileSync, } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { z } from "zod";
|
|
4
|
+
import { createLogger } from "../logger.js";
|
|
5
|
+
const logger = createLogger("session-storage");
|
|
4
6
|
/**
|
|
5
7
|
* Zod schema for validating session files
|
|
6
8
|
*/
|
|
@@ -273,12 +275,8 @@ export class SessionStorage {
|
|
|
273
275
|
try {
|
|
274
276
|
// Write to temp file
|
|
275
277
|
writeFileSync(tempPath, JSON.stringify(session, null, 2), "utf-8");
|
|
276
|
-
// Atomic rename
|
|
277
|
-
|
|
278
|
-
unlinkSync(sessionPath);
|
|
279
|
-
}
|
|
280
|
-
writeFileSync(sessionPath, readFileSync(tempPath, "utf-8"), "utf-8");
|
|
281
|
-
unlinkSync(tempPath);
|
|
278
|
+
// Atomic rename - renameSync is atomic on POSIX systems
|
|
279
|
+
renameSync(tempPath, sessionPath);
|
|
282
280
|
}
|
|
283
281
|
catch (error) {
|
|
284
282
|
// Clean up temp file on error
|
|
@@ -424,8 +422,12 @@ export class SessionStorage {
|
|
|
424
422
|
sessions.push(entry);
|
|
425
423
|
}
|
|
426
424
|
}
|
|
427
|
-
catch {
|
|
428
|
-
//
|
|
425
|
+
catch (error) {
|
|
426
|
+
// Log and skip invalid sessions so they don't break the session list
|
|
427
|
+
logger.warn("Failed to load session, skipping from list", {
|
|
428
|
+
sessionId,
|
|
429
|
+
error: error instanceof Error ? error.message : String(error),
|
|
430
|
+
});
|
|
429
431
|
}
|
|
430
432
|
}
|
|
431
433
|
// Sort by updatedAt, most recent first
|
|
@@ -506,11 +508,8 @@ export class SessionStorage {
|
|
|
506
508
|
const tempPath = `${sessionPath}.tmp`;
|
|
507
509
|
try {
|
|
508
510
|
writeFileSync(tempPath, JSON.stringify(updatedSession, null, 2), "utf-8");
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
}
|
|
512
|
-
writeFileSync(sessionPath, readFileSync(tempPath, "utf-8"), "utf-8");
|
|
513
|
-
unlinkSync(tempPath);
|
|
511
|
+
// Atomic rename - renameSync is atomic on POSIX systems
|
|
512
|
+
renameSync(tempPath, sessionPath);
|
|
514
513
|
}
|
|
515
514
|
catch (error) {
|
|
516
515
|
if (existsSync(tempPath)) {
|
|
@@ -39,6 +39,171 @@ getWeather.icon = "Cloud";
|
|
|
39
39
|
function isPlainRecord(v) {
|
|
40
40
|
return !!v && typeof v === "object" && !Array.isArray(v);
|
|
41
41
|
}
|
|
42
|
+
/**
|
|
43
|
+
* Extract source name from URL domain
|
|
44
|
+
*/
|
|
45
|
+
function getSourceNameFromUrl(url) {
|
|
46
|
+
try {
|
|
47
|
+
const domain = new URL(url).hostname.replace(/^www\./, "");
|
|
48
|
+
return domain;
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
return "Web";
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Extract favicon URL from domain
|
|
56
|
+
*/
|
|
57
|
+
function getFaviconFromUrl(url) {
|
|
58
|
+
try {
|
|
59
|
+
const domain = new URL(url).hostname;
|
|
60
|
+
return `https://www.google.com/s2/favicons?domain=${domain}&sz=32`;
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
return undefined;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Extract citation sources from raw tool output BEFORE compaction.
|
|
68
|
+
* This preserves URLs that may be removed during LLM compaction.
|
|
69
|
+
*/
|
|
70
|
+
function extractSourcesBeforeCompaction(toolName, rawOutput) {
|
|
71
|
+
const sources = [];
|
|
72
|
+
let sourceCounter = 0;
|
|
73
|
+
// Parse content if it's a JSON string (common for MCP tools)
|
|
74
|
+
let actualOutput = rawOutput;
|
|
75
|
+
if (typeof rawOutput.content === "string") {
|
|
76
|
+
try {
|
|
77
|
+
const parsed = JSON.parse(rawOutput.content);
|
|
78
|
+
if (typeof parsed === "object" && parsed !== null) {
|
|
79
|
+
actualOutput = parsed;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
// Not valid JSON, use rawOutput as-is
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
// Handle WebSearch (Exa) results
|
|
87
|
+
if (toolName === "WebSearch" ||
|
|
88
|
+
toolName === "web_search" ||
|
|
89
|
+
toolName === "town_web_search") {
|
|
90
|
+
const formattedResults = actualOutput.formattedForCitation;
|
|
91
|
+
if (Array.isArray(formattedResults)) {
|
|
92
|
+
for (const result of formattedResults) {
|
|
93
|
+
if (result &&
|
|
94
|
+
typeof result === "object" &&
|
|
95
|
+
"url" in result &&
|
|
96
|
+
typeof result.url === "string") {
|
|
97
|
+
sourceCounter++;
|
|
98
|
+
const url = result.url;
|
|
99
|
+
sources.push({
|
|
100
|
+
id: typeof result.citationId === "number"
|
|
101
|
+
? String(result.citationId)
|
|
102
|
+
: String(sourceCounter),
|
|
103
|
+
url,
|
|
104
|
+
title: typeof result.title === "string" ? result.title : "Untitled",
|
|
105
|
+
snippet: typeof result.text === "string"
|
|
106
|
+
? result.text.slice(0, 200)
|
|
107
|
+
: undefined,
|
|
108
|
+
favicon: getFaviconFromUrl(url),
|
|
109
|
+
sourceName: getSourceNameFromUrl(url),
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
else {
|
|
115
|
+
// Fallback to raw results
|
|
116
|
+
const results = actualOutput.results;
|
|
117
|
+
if (Array.isArray(results)) {
|
|
118
|
+
for (const result of results) {
|
|
119
|
+
if (result &&
|
|
120
|
+
typeof result === "object" &&
|
|
121
|
+
"url" in result &&
|
|
122
|
+
typeof result.url === "string") {
|
|
123
|
+
sourceCounter++;
|
|
124
|
+
const url = result.url;
|
|
125
|
+
sources.push({
|
|
126
|
+
id: String(sourceCounter),
|
|
127
|
+
url,
|
|
128
|
+
title: typeof result.title === "string" ? result.title : "Untitled",
|
|
129
|
+
snippet: typeof result.text === "string"
|
|
130
|
+
? result.text.slice(0, 200)
|
|
131
|
+
: undefined,
|
|
132
|
+
favicon: getFaviconFromUrl(url),
|
|
133
|
+
sourceName: getSourceNameFromUrl(url),
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
// Handle library/document retrieval tools
|
|
141
|
+
const isLibraryTool = toolName.startsWith("library__") ||
|
|
142
|
+
toolName.includes("get_document") ||
|
|
143
|
+
toolName.includes("retrieve_document") ||
|
|
144
|
+
toolName.includes("bibliotecha");
|
|
145
|
+
if (isLibraryTool) {
|
|
146
|
+
const extractDocSource = (doc) => {
|
|
147
|
+
const docUrl = typeof doc.document_url === "string"
|
|
148
|
+
? doc.document_url
|
|
149
|
+
: typeof doc.url === "string"
|
|
150
|
+
? doc.url
|
|
151
|
+
: typeof doc.source_url === "string"
|
|
152
|
+
? doc.source_url
|
|
153
|
+
: null;
|
|
154
|
+
const docTitle = typeof doc.title === "string" ? doc.title : null;
|
|
155
|
+
const docId = typeof doc.document_id === "number"
|
|
156
|
+
? String(doc.document_id)
|
|
157
|
+
: typeof doc.document_id === "string"
|
|
158
|
+
? doc.document_id
|
|
159
|
+
: null;
|
|
160
|
+
if (!docUrl && !docTitle)
|
|
161
|
+
return null;
|
|
162
|
+
sourceCounter++;
|
|
163
|
+
// Extract source name from source field or URL
|
|
164
|
+
let sourceName;
|
|
165
|
+
if (typeof doc.source === "string") {
|
|
166
|
+
sourceName = doc.source
|
|
167
|
+
.split("_")
|
|
168
|
+
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
|
|
169
|
+
.join(" ");
|
|
170
|
+
}
|
|
171
|
+
else if (docUrl) {
|
|
172
|
+
sourceName = getSourceNameFromUrl(docUrl);
|
|
173
|
+
}
|
|
174
|
+
return {
|
|
175
|
+
id: docId || String(sourceCounter),
|
|
176
|
+
url: docUrl || "",
|
|
177
|
+
title: docTitle || docUrl || "Document",
|
|
178
|
+
snippet: typeof doc.summary === "string"
|
|
179
|
+
? doc.summary.slice(0, 200)
|
|
180
|
+
: typeof doc.snippet === "string"
|
|
181
|
+
? doc.snippet.slice(0, 200)
|
|
182
|
+
: undefined,
|
|
183
|
+
favicon: docUrl ? getFaviconFromUrl(docUrl) : undefined,
|
|
184
|
+
sourceName,
|
|
185
|
+
};
|
|
186
|
+
};
|
|
187
|
+
// Check for results array (library__search_keyword, library__semantic_search)
|
|
188
|
+
const results = actualOutput.results ?? actualOutput.documents;
|
|
189
|
+
if (Array.isArray(results)) {
|
|
190
|
+
for (const result of results) {
|
|
191
|
+
if (result && typeof result === "object") {
|
|
192
|
+
const source = extractDocSource(result);
|
|
193
|
+
if (source)
|
|
194
|
+
sources.push(source);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
else {
|
|
199
|
+
// Handle single document
|
|
200
|
+
const source = extractDocSource(actualOutput);
|
|
201
|
+
if (source)
|
|
202
|
+
sources.push(source);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
return sources;
|
|
206
|
+
}
|
|
42
207
|
function stableStringify(value) {
|
|
43
208
|
const seen = new WeakSet();
|
|
44
209
|
const _stringify = (v) => {
|
|
@@ -589,6 +754,9 @@ export class LangchainAgent {
|
|
|
589
754
|
? { content: result }
|
|
590
755
|
: { content: JSON.stringify(result) };
|
|
591
756
|
const outputTokens = await countToolResultTokens(rawOutput);
|
|
757
|
+
// Extract citation sources BEFORE compaction to preserve URLs
|
|
758
|
+
// Compaction LLM may remove URLs as "unnecessary" during summarization
|
|
759
|
+
const preExtractedSources = extractSourcesBeforeCompaction(originalTool.name, rawOutput);
|
|
592
760
|
// Include current prompt as the last user message for better context.
|
|
593
761
|
const nowIso = new Date().toISOString();
|
|
594
762
|
const promptBlocksForHooks = req.prompt.map((block) => {
|
|
@@ -673,8 +841,20 @@ export class LangchainAgent {
|
|
|
673
841
|
if (typeof originalContentPath === "string") {
|
|
674
842
|
meta.originalContentPath = originalContentPath;
|
|
675
843
|
}
|
|
844
|
+
// Include pre-extracted sources (extracted before compaction)
|
|
845
|
+
if (preExtractedSources.length > 0) {
|
|
846
|
+
meta.preExtractedSources = preExtractedSources;
|
|
847
|
+
}
|
|
676
848
|
inflightCompactionMetaByToolCallId.set(toolCallId, meta);
|
|
677
849
|
}
|
|
850
|
+
else if (preExtractedSources.length > 0) {
|
|
851
|
+
// Even without compaction, pass pre-extracted sources to adapter
|
|
852
|
+
// This ensures sources are always available for citation extraction
|
|
853
|
+
inflightCompactionMetaByToolCallId.set(toolCallId, {
|
|
854
|
+
action: "compacted", // Minimal action to indicate metadata presence
|
|
855
|
+
preExtractedSources,
|
|
856
|
+
});
|
|
857
|
+
}
|
|
678
858
|
// Return compacted output to LangChain (model-visible), without metadata.
|
|
679
859
|
if (typeof result === "string") {
|
|
680
860
|
if (typeof finalOutput.content === "string") {
|
|
@@ -30,6 +30,11 @@ export async function createModelFromString(modelString) {
|
|
|
30
30
|
if (!shedAuth) {
|
|
31
31
|
throw new Error("Not logged in. Run 'town login' or set SHED_API_KEY.");
|
|
32
32
|
}
|
|
33
|
+
console.log("[model-factory] town- model, sending to shed:", {
|
|
34
|
+
shedUrl: shedAuth.shedUrl,
|
|
35
|
+
tokenPrefix: shedAuth.accessToken?.slice(0, 20),
|
|
36
|
+
tokenLength: shedAuth.accessToken?.length,
|
|
37
|
+
});
|
|
33
38
|
return new ChatAnthropic({
|
|
34
39
|
model: actualModel,
|
|
35
40
|
anthropicApiUrl: `${shedAuth.shedUrl}/api/anthropic`,
|