@townco/agent 0.1.117 → 0.1.119
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 +3 -3
- package/dist/acp-server/adapter.js +101 -79
- package/dist/acp-server/http.js +28 -1
- package/dist/acp-server/session-storage.d.ts +27 -1
- package/dist/acp-server/session-storage.js +19 -1
- package/dist/definition/index.d.ts +4 -1
- package/dist/definition/models.d.ts +6 -0
- package/dist/definition/models.js +1 -0
- package/dist/mcp/index.d.ts +12 -0
- package/dist/mcp/index.js +83 -0
- package/dist/runner/hooks/predefined/compaction-tool.js +13 -1
- package/dist/runner/hooks/predefined/mid-turn-compaction.js +13 -1
- package/dist/runner/hooks/types.d.ts +6 -0
- package/dist/runner/index.d.ts +2 -66
- package/dist/runner/langchain/index.js +4 -4
- package/dist/runner/langchain/tools/artifacts.d.ts +68 -0
- package/dist/runner/langchain/tools/artifacts.js +474 -0
- package/dist/runner/langchain/tools/conversation_search.d.ts +22 -0
- package/dist/runner/langchain/tools/conversation_search.js +137 -0
- package/dist/runner/langchain/tools/generate_image.d.ts +47 -0
- package/dist/runner/langchain/tools/generate_image.js +175 -0
- package/dist/runner/langchain/tools/port-utils.d.ts +8 -0
- package/dist/runner/langchain/tools/port-utils.js +35 -0
- package/dist/runner/langchain/tools/subagent-connections.d.ts +2 -0
- package/dist/runner/langchain/tools/subagent.js +230 -3
- package/dist/templates/index.d.ts +5 -1
- package/dist/templates/index.js +22 -16
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +11 -7
- package/templates/index.ts +31 -20
|
@@ -19,10 +19,10 @@ export interface CitationSource {
|
|
|
19
19
|
id: string;
|
|
20
20
|
url: string;
|
|
21
21
|
title: string;
|
|
22
|
-
snippet?: string;
|
|
23
|
-
favicon?: string;
|
|
22
|
+
snippet?: string | undefined;
|
|
23
|
+
favicon?: string | undefined;
|
|
24
24
|
toolCallId: string;
|
|
25
|
-
sourceName?: string;
|
|
25
|
+
sourceName?: string | undefined;
|
|
26
26
|
}
|
|
27
27
|
/**
|
|
28
28
|
* Error thrown when mid-turn compaction requires the turn to be restarted
|
|
@@ -671,11 +671,17 @@ export class AgentAcpAdapter {
|
|
|
671
671
|
async saveSessionToDisk(sessionId, session) {
|
|
672
672
|
if (!this.noSession && this.storage) {
|
|
673
673
|
try {
|
|
674
|
-
|
|
674
|
+
// Build citation storage from current session state
|
|
675
|
+
const citations = {
|
|
676
|
+
sources: session.sources,
|
|
677
|
+
nextId: session.sourceCounter + 1,
|
|
678
|
+
};
|
|
679
|
+
await this.storage.saveSession(sessionId, session.messages, session.context, citations);
|
|
675
680
|
logger.debug("Saved session to disk", {
|
|
676
681
|
sessionId,
|
|
677
682
|
messageCount: session.messages.length,
|
|
678
683
|
contextCount: session.context.length,
|
|
684
|
+
citationsCount: session.sources.length,
|
|
679
685
|
});
|
|
680
686
|
}
|
|
681
687
|
catch (error) {
|
|
@@ -814,19 +820,25 @@ export class AgentAcpAdapter {
|
|
|
814
820
|
}
|
|
815
821
|
// Save repaired session if changes were made
|
|
816
822
|
if (sessionRepaired && this.storage) {
|
|
817
|
-
await this.storage.saveSession(params.sessionId, storedSession.messages, storedSession.context);
|
|
823
|
+
await this.storage.saveSession(params.sessionId, storedSession.messages, storedSession.context, storedSession.citations);
|
|
818
824
|
logger.info("Saved repaired session", { sessionId: params.sessionId });
|
|
819
825
|
}
|
|
820
826
|
// Restore session in active sessions map
|
|
827
|
+
// Restore citations from stored session to avoid ID conflicts
|
|
821
828
|
this.sessions.set(params.sessionId, {
|
|
822
829
|
pendingPrompt: null,
|
|
823
830
|
messages: storedSession.messages,
|
|
824
831
|
context: storedSession.context,
|
|
825
832
|
requestParams: { cwd: process.cwd(), mcpServers: [] },
|
|
826
833
|
isCancelled: false,
|
|
827
|
-
sourceCounter: 0,
|
|
828
|
-
sources: [],
|
|
834
|
+
sourceCounter: storedSession.citations?.nextId ?? 0,
|
|
835
|
+
sources: storedSession.citations?.sources ?? [],
|
|
836
|
+
...(storedSession.citations && {
|
|
837
|
+
persistedCitations: storedSession.citations,
|
|
838
|
+
}),
|
|
829
839
|
});
|
|
840
|
+
// Track whether we have persisted citations to skip re-extraction during replay
|
|
841
|
+
const hasPersistedCitations = (storedSession.citations?.sources.length ?? 0) > 0;
|
|
830
842
|
// Replay conversation history to client with ordered content blocks
|
|
831
843
|
logger.info(`Replaying ${storedSession.messages.length} messages for session ${params.sessionId}`);
|
|
832
844
|
for (const msg of storedSession.messages) {
|
|
@@ -918,25 +930,27 @@ export class AgentAcpAdapter {
|
|
|
918
930
|
sessionId: params.sessionId,
|
|
919
931
|
update: outputUpdate,
|
|
920
932
|
});
|
|
921
|
-
// Extract sources from replayed tool output
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
933
|
+
// Extract sources from replayed tool output (skip if we have persisted citations)
|
|
934
|
+
if (!hasPersistedCitations) {
|
|
935
|
+
const session = this.sessions.get(params.sessionId);
|
|
936
|
+
if (session) {
|
|
937
|
+
const extractedSources = this.extractSourcesFromToolOutput(block.title, block.rawOutput, block.id, session);
|
|
938
|
+
if (extractedSources.length > 0) {
|
|
939
|
+
session.sources.push(...extractedSources);
|
|
940
|
+
// Emit sources notification to client during replay
|
|
941
|
+
this.connection.sessionUpdate({
|
|
942
|
+
sessionId: params.sessionId,
|
|
943
|
+
update: {
|
|
944
|
+
sessionUpdate: "sources",
|
|
945
|
+
sources: extractedSources,
|
|
946
|
+
},
|
|
947
|
+
});
|
|
948
|
+
logger.info("Extracted sources during session replay", {
|
|
949
|
+
toolCallId: block.id,
|
|
950
|
+
toolName: block.title,
|
|
951
|
+
sourcesCount: extractedSources.length,
|
|
952
|
+
});
|
|
953
|
+
}
|
|
940
954
|
}
|
|
941
955
|
}
|
|
942
956
|
}
|
|
@@ -955,6 +969,20 @@ export class AgentAcpAdapter {
|
|
|
955
969
|
}
|
|
956
970
|
}
|
|
957
971
|
}
|
|
972
|
+
// Emit persisted citations AFTER message replay so UI can associate them with tool calls
|
|
973
|
+
if (hasPersistedCitations && storedSession.citations?.sources) {
|
|
974
|
+
this.connection.sessionUpdate({
|
|
975
|
+
sessionId: params.sessionId,
|
|
976
|
+
update: {
|
|
977
|
+
sessionUpdate: "sources",
|
|
978
|
+
sources: storedSession.citations.sources,
|
|
979
|
+
},
|
|
980
|
+
});
|
|
981
|
+
logger.info("Emitted persisted citations after replay", {
|
|
982
|
+
sessionId: params.sessionId,
|
|
983
|
+
citationsCount: storedSession.citations.sources.length,
|
|
984
|
+
});
|
|
985
|
+
}
|
|
958
986
|
// After replay completes, send the latest context size to the UI
|
|
959
987
|
const latestContext = storedSession.context.length > 0
|
|
960
988
|
? storedSession.context[storedSession.context.length - 1]
|
|
@@ -1500,8 +1528,7 @@ export class AgentAcpAdapter {
|
|
|
1500
1528
|
}
|
|
1501
1529
|
// Forward tool_call_update with _meta to the client (for subagent connection info, etc.)
|
|
1502
1530
|
if (updateMsg._meta) {
|
|
1503
|
-
const subagentCompletedValue = updateMsg._meta
|
|
1504
|
-
?.subagentCompleted;
|
|
1531
|
+
const subagentCompletedValue = updateMsg._meta?.subagentCompleted;
|
|
1505
1532
|
this.connection.sessionUpdate({
|
|
1506
1533
|
sessionId: params.sessionId,
|
|
1507
1534
|
update: {
|
|
@@ -1584,6 +1611,51 @@ export class AgentAcpAdapter {
|
|
|
1584
1611
|
rawOutput = cleanOutput;
|
|
1585
1612
|
}
|
|
1586
1613
|
if (rawOutput && !this.noSession) {
|
|
1614
|
+
// IMPORTANT: Extract citations BEFORE hook execution
|
|
1615
|
+
// Hooks may compact/modify rawOutput, losing citation data
|
|
1616
|
+
// We extract from the original output to preserve citation sources
|
|
1617
|
+
const rawOutputForCitations = rawOutput;
|
|
1618
|
+
logger.debug("Extracting citations BEFORE compaction hooks", {
|
|
1619
|
+
toolCallId: outputMsg.toolCallId,
|
|
1620
|
+
toolName: toolCallBlock.title,
|
|
1621
|
+
rawOutputType: typeof rawOutputForCitations,
|
|
1622
|
+
});
|
|
1623
|
+
// Handle subagent tool outputs first
|
|
1624
|
+
const subagentResult = this.extractSubagentSources(rawOutput, outputMsg.toolCallId);
|
|
1625
|
+
if (subagentResult && subagentResult.sources.length > 0) {
|
|
1626
|
+
session.sources.push(...subagentResult.sources);
|
|
1627
|
+
this.connection.sessionUpdate({
|
|
1628
|
+
sessionId: params.sessionId,
|
|
1629
|
+
update: {
|
|
1630
|
+
sessionUpdate: "sources",
|
|
1631
|
+
sources: subagentResult.sources,
|
|
1632
|
+
},
|
|
1633
|
+
});
|
|
1634
|
+
logger.info("Extracted citation sources from subagent (pre-compaction)", {
|
|
1635
|
+
toolCallId: outputMsg.toolCallId,
|
|
1636
|
+
sourcesCount: subagentResult.sources.length,
|
|
1637
|
+
});
|
|
1638
|
+
}
|
|
1639
|
+
// Extract from regular tool outputs (WebSearch, WebFetch, MCP tools)
|
|
1640
|
+
const subagentHadSources = subagentResult && subagentResult.sources.length > 0;
|
|
1641
|
+
if (!subagentHadSources) {
|
|
1642
|
+
const extractedSources = this.extractSourcesFromToolOutput(toolCallBlock.title, rawOutput, outputMsg.toolCallId, session);
|
|
1643
|
+
if (extractedSources.length > 0) {
|
|
1644
|
+
session.sources.push(...extractedSources);
|
|
1645
|
+
this.connection.sessionUpdate({
|
|
1646
|
+
sessionId: params.sessionId,
|
|
1647
|
+
update: {
|
|
1648
|
+
sessionUpdate: "sources",
|
|
1649
|
+
sources: extractedSources,
|
|
1650
|
+
},
|
|
1651
|
+
});
|
|
1652
|
+
logger.info("Extracted citation sources from tool output (pre-compaction)", {
|
|
1653
|
+
toolCallId: outputMsg.toolCallId,
|
|
1654
|
+
toolName: toolCallBlock.title,
|
|
1655
|
+
sourcesCount: extractedSources.length,
|
|
1656
|
+
});
|
|
1657
|
+
}
|
|
1658
|
+
}
|
|
1587
1659
|
// Execute tool_response hooks if configured
|
|
1588
1660
|
const hooks = this.agent.definition.hooks ?? [];
|
|
1589
1661
|
if (hooks.some((h) => h.type === "tool_response")) {
|
|
@@ -1750,60 +1822,8 @@ export class AgentAcpAdapter {
|
|
|
1750
1822
|
if (rawOutput) {
|
|
1751
1823
|
toolCallBlock.rawOutput = rawOutput;
|
|
1752
1824
|
}
|
|
1753
|
-
//
|
|
1754
|
-
//
|
|
1755
|
-
// Sources are already re-numbered by the runner with unique IDs (1001+, 2001+, etc.)
|
|
1756
|
-
const rawOutputForLog = rawOutput;
|
|
1757
|
-
logger.debug("Checking for subagent sources in tool output", {
|
|
1758
|
-
toolCallId: outputMsg.toolCallId,
|
|
1759
|
-
toolName: toolCallBlock.title,
|
|
1760
|
-
rawOutputType: typeof rawOutputForLog,
|
|
1761
|
-
rawOutputPreview: typeof rawOutputForLog === "string"
|
|
1762
|
-
? rawOutputForLog.slice(0, 200)
|
|
1763
|
-
: typeof rawOutputForLog === "object" && rawOutputForLog
|
|
1764
|
-
? JSON.stringify(rawOutputForLog).slice(0, 200)
|
|
1765
|
-
: String(rawOutputForLog),
|
|
1766
|
-
});
|
|
1767
|
-
const subagentResult = this.extractSubagentSources(rawOutput, outputMsg.toolCallId);
|
|
1768
|
-
if (subagentResult && subagentResult.sources.length > 0) {
|
|
1769
|
-
// Add sources to session (already uniquely numbered)
|
|
1770
|
-
session.sources.push(...subagentResult.sources);
|
|
1771
|
-
// Emit sources notification to client
|
|
1772
|
-
this.connection.sessionUpdate({
|
|
1773
|
-
sessionId: params.sessionId,
|
|
1774
|
-
update: {
|
|
1775
|
-
sessionUpdate: "sources",
|
|
1776
|
-
sources: subagentResult.sources,
|
|
1777
|
-
},
|
|
1778
|
-
});
|
|
1779
|
-
logger.info("Extracted citation sources from subagent", {
|
|
1780
|
-
toolCallId: outputMsg.toolCallId,
|
|
1781
|
-
sourcesCount: subagentResult.sources.length,
|
|
1782
|
-
});
|
|
1783
|
-
}
|
|
1784
|
-
// Extract citation sources from tool output (WebSearch, WebFetch, MCP tools)
|
|
1785
|
-
// Skip if we already extracted sources from a subagent result (with actual sources)
|
|
1786
|
-
const subagentHadSources = subagentResult && subagentResult.sources.length > 0;
|
|
1787
|
-
if (!subagentHadSources) {
|
|
1788
|
-
const extractedSources = this.extractSourcesFromToolOutput(toolCallBlock.title, rawOutput, outputMsg.toolCallId, session);
|
|
1789
|
-
if (extractedSources.length > 0) {
|
|
1790
|
-
// Add to session sources
|
|
1791
|
-
session.sources.push(...extractedSources);
|
|
1792
|
-
// Emit sources notification to client
|
|
1793
|
-
this.connection.sessionUpdate({
|
|
1794
|
-
sessionId: params.sessionId,
|
|
1795
|
-
update: {
|
|
1796
|
-
sessionUpdate: "sources",
|
|
1797
|
-
sources: extractedSources,
|
|
1798
|
-
},
|
|
1799
|
-
});
|
|
1800
|
-
logger.info("Extracted citation sources from tool output", {
|
|
1801
|
-
toolCallId: outputMsg.toolCallId,
|
|
1802
|
-
toolName: toolCallBlock.title,
|
|
1803
|
-
sourcesCount: extractedSources.length,
|
|
1804
|
-
});
|
|
1805
|
-
}
|
|
1806
|
-
}
|
|
1825
|
+
// NOTE: Citation extraction moved to BEFORE hook execution (around line 2138)
|
|
1826
|
+
// to ensure citations are captured before any compaction modifies rawOutput
|
|
1807
1827
|
// Store truncation warning if present (for UI display)
|
|
1808
1828
|
if (truncationWarning) {
|
|
1809
1829
|
if (!toolCallBlock._meta) {
|
|
@@ -2320,10 +2340,12 @@ export class AgentAcpAdapter {
|
|
|
2320
2340
|
};
|
|
2321
2341
|
const hookExecutor = new HookExecutor(hooks, this.agent.definition.model, (callbackRef) => loadHookCallback(callbackRef, this.agentDir), sendHookNotification, this.agent.definition);
|
|
2322
2342
|
// Create read-only session view for hooks
|
|
2343
|
+
// Include citations so compaction hooks can inject them into summaries
|
|
2323
2344
|
const readonlySession = {
|
|
2324
2345
|
messages: session.messages,
|
|
2325
2346
|
context: session.context,
|
|
2326
2347
|
requestParams: session.requestParams,
|
|
2348
|
+
citations: session.sources,
|
|
2327
2349
|
};
|
|
2328
2350
|
// Get input token count from latest context entry
|
|
2329
2351
|
const latestContext = session.context.length > 0
|
package/dist/acp-server/http.js
CHANGED
|
@@ -259,6 +259,19 @@ export function createAcpHttpApp(agent, agentDir, agentName) {
|
|
|
259
259
|
const stream = sseStreams.get(msgSessionId);
|
|
260
260
|
if (stream) {
|
|
261
261
|
try {
|
|
262
|
+
// Check if this is a sources message
|
|
263
|
+
const isSourcesMsg = rawMsg &&
|
|
264
|
+
typeof rawMsg === "object" &&
|
|
265
|
+
"params" in rawMsg &&
|
|
266
|
+
rawMsg.params?.update?.sessionUpdate === "sources";
|
|
267
|
+
if (isSourcesMsg) {
|
|
268
|
+
const sourcesCount = rawMsg.params?.update?.sources?.length;
|
|
269
|
+
logger.info("🔶 SENDING LARGE SOURCES VIA DIRECT SSE", {
|
|
270
|
+
sessionId: msgSessionId,
|
|
271
|
+
sourcesCount,
|
|
272
|
+
compressedSize,
|
|
273
|
+
});
|
|
274
|
+
}
|
|
262
275
|
await stream.writeSSE({
|
|
263
276
|
event: "message",
|
|
264
277
|
data: JSON.stringify(rawMsg),
|
|
@@ -798,7 +811,21 @@ export function createAcpHttpApp(agent, agentDir, agentName) {
|
|
|
798
811
|
return;
|
|
799
812
|
}
|
|
800
813
|
}
|
|
801
|
-
|
|
814
|
+
// Check if this is a sources message and log prominently
|
|
815
|
+
const isSourcesMsg = json &&
|
|
816
|
+
typeof json === "object" &&
|
|
817
|
+
"params" in json &&
|
|
818
|
+
json.params?.update?.sessionUpdate === "sources";
|
|
819
|
+
if (isSourcesMsg) {
|
|
820
|
+
logger.info("🔶 SENDING SOURCES VIA SSE", {
|
|
821
|
+
sessionId,
|
|
822
|
+
channel,
|
|
823
|
+
sourcesCount: json.params?.update?.sources?.length,
|
|
824
|
+
});
|
|
825
|
+
}
|
|
826
|
+
else {
|
|
827
|
+
logger.trace("Sending SSE message", { sessionId, channel });
|
|
828
|
+
}
|
|
802
829
|
await stream.writeSSE({
|
|
803
830
|
event: "message",
|
|
804
831
|
data: JSON.stringify(json),
|
|
@@ -25,6 +25,8 @@ export interface SubagentToolCallBlock {
|
|
|
25
25
|
prettyName?: string | undefined;
|
|
26
26
|
icon?: string | undefined;
|
|
27
27
|
status: "pending" | "in_progress" | "completed" | "failed";
|
|
28
|
+
rawInput?: Record<string, unknown> | undefined;
|
|
29
|
+
rawOutput?: Record<string, unknown> | undefined;
|
|
28
30
|
}
|
|
29
31
|
/**
|
|
30
32
|
* Content block for sub-agent messages - either text or a tool call
|
|
@@ -149,6 +151,30 @@ export interface StoredSession {
|
|
|
149
151
|
messages: SessionMessage[];
|
|
150
152
|
context: ContextEntry[];
|
|
151
153
|
metadata: SessionMetadata;
|
|
154
|
+
/** Persisted citation sources - survives compaction and session reload */
|
|
155
|
+
citations?: CitationStorage | undefined;
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Citation source persisted in session storage
|
|
159
|
+
* Matches CitationSource from adapter.ts and SourceSchema from UI
|
|
160
|
+
*/
|
|
161
|
+
export interface PersistedCitationSource {
|
|
162
|
+
id: string;
|
|
163
|
+
url: string;
|
|
164
|
+
title: string;
|
|
165
|
+
snippet?: string | undefined;
|
|
166
|
+
favicon?: string | undefined;
|
|
167
|
+
toolCallId: string;
|
|
168
|
+
sourceName?: string | undefined;
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Citation storage structure for session persistence
|
|
172
|
+
*/
|
|
173
|
+
export interface CitationStorage {
|
|
174
|
+
/** All citation sources accumulated in this session */
|
|
175
|
+
sources: PersistedCitationSource[];
|
|
176
|
+
/** Next citation ID to use (to avoid ID conflicts after reload) */
|
|
177
|
+
nextId: number;
|
|
152
178
|
}
|
|
153
179
|
/**
|
|
154
180
|
* File-based session storage
|
|
@@ -181,7 +207,7 @@ export declare class SessionStorage {
|
|
|
181
207
|
* Save a session to disk
|
|
182
208
|
* Uses atomic write (write to temp file, then rename)
|
|
183
209
|
*/
|
|
184
|
-
saveSession(sessionId: string, messages: SessionMessage[], context: ContextEntry[]): Promise<void>;
|
|
210
|
+
saveSession(sessionId: string, messages: SessionMessage[], context: ContextEntry[], citations?: CitationStorage): Promise<void>;
|
|
185
211
|
/**
|
|
186
212
|
* Load a session from disk
|
|
187
213
|
*/
|
|
@@ -32,6 +32,8 @@ const subagentToolCallBlockSchema = z.object({
|
|
|
32
32
|
prettyName: z.string().optional(),
|
|
33
33
|
icon: z.string().optional(),
|
|
34
34
|
status: z.enum(["pending", "in_progress", "completed", "failed"]),
|
|
35
|
+
rawInput: z.record(z.string(), z.unknown()).optional(),
|
|
36
|
+
rawOutput: z.record(z.string(), z.unknown()).optional(),
|
|
35
37
|
});
|
|
36
38
|
const subagentContentBlockSchema = z.discriminatedUnion("type", [
|
|
37
39
|
z.object({
|
|
@@ -134,11 +136,26 @@ const sessionMetadataSchema = z.object({
|
|
|
134
136
|
agentName: z.string(),
|
|
135
137
|
sandboxId: z.string().optional(),
|
|
136
138
|
});
|
|
139
|
+
// Citation schemas - matches SourceSchema from packages/ui/src/core/schemas/source.ts
|
|
140
|
+
const persistedCitationSourceSchema = z.object({
|
|
141
|
+
id: z.string(),
|
|
142
|
+
url: z.string(),
|
|
143
|
+
title: z.string(),
|
|
144
|
+
snippet: z.string().optional(),
|
|
145
|
+
favicon: z.string().optional(),
|
|
146
|
+
toolCallId: z.string(),
|
|
147
|
+
sourceName: z.string().optional(),
|
|
148
|
+
});
|
|
149
|
+
const citationStorageSchema = z.object({
|
|
150
|
+
sources: z.array(persistedCitationSourceSchema),
|
|
151
|
+
nextId: z.number(),
|
|
152
|
+
});
|
|
137
153
|
const storedSessionSchema = z.object({
|
|
138
154
|
sessionId: z.string(),
|
|
139
155
|
messages: z.array(sessionMessageSchema),
|
|
140
156
|
context: z.array(contextEntrySchema),
|
|
141
157
|
metadata: sessionMetadataSchema,
|
|
158
|
+
citations: citationStorageSchema.optional(),
|
|
142
159
|
});
|
|
143
160
|
/**
|
|
144
161
|
* File-based session storage
|
|
@@ -183,7 +200,7 @@ export class SessionStorage {
|
|
|
183
200
|
* Save a session to disk
|
|
184
201
|
* Uses atomic write (write to temp file, then rename)
|
|
185
202
|
*/
|
|
186
|
-
async saveSession(sessionId, messages, context) {
|
|
203
|
+
async saveSession(sessionId, messages, context, citations) {
|
|
187
204
|
// Debug: log subagent data being saved
|
|
188
205
|
const messagesWithSubagents = messages.filter((msg) => msg.content.some((block) => block.type === "tool_call" &&
|
|
189
206
|
"subagentMessages" in block &&
|
|
@@ -223,6 +240,7 @@ export class SessionStorage {
|
|
|
223
240
|
updatedAt: now,
|
|
224
241
|
agentName: this.agentName,
|
|
225
242
|
},
|
|
243
|
+
...(citations && { citations }),
|
|
226
244
|
};
|
|
227
245
|
try {
|
|
228
246
|
// Write to temp file
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
|
|
2
|
+
import type { Model } from "./models";
|
|
3
|
+
export type AgentDefinition = Omit<z.infer<typeof AgentDefinitionSchema>, "model"> & {
|
|
4
|
+
model: Model | (string & {});
|
|
5
|
+
};
|
|
3
6
|
/** MCP configuration types. */
|
|
4
7
|
export declare const McpConfigSchema: z.ZodUnion<readonly [z.ZodObject<{
|
|
5
8
|
name: z.ZodString;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
type AnthropicModel = "claude-opus-4-5-20251101" | "claude-opus-4-5" | "claude-3-7-sonnet-latest" | "claude-3-7-sonnet-20250219" | "claude-3-5-haiku-latest" | "claude-3-5-haiku-20241022" | "claude-haiku-4-5" | "claude-haiku-4-5-20251001" | "claude-sonnet-4-20250514" | "claude-sonnet-4-0" | "claude-4-sonnet-20250514" | "claude-sonnet-4-5" | "claude-sonnet-4-5-20250929" | "claude-opus-4-0" | "claude-opus-4-20250514" | "claude-4-opus-20250514" | "claude-opus-4-1-20250805" | "claude-3-opus-latest" | "claude-3-opus-20240229" | "claude-3-haiku-20240307";
|
|
2
|
+
type TownAnthropicModel = `town-${AnthropicModel}`;
|
|
3
|
+
type GeminiModel = "gemini-3-pro-preview" | "gemini-2.5-flash" | "gemini-2.5-flash-lite" | "gemini-2.5-flash-lite" | "gemini-2.5-pro" | "gemini-2.0-flash" | "gemini-2.0-flash-lite";
|
|
4
|
+
type VertexModel = `vertex-${GeminiModel}`;
|
|
5
|
+
export type Model = AnthropicModel | TownAnthropicModel | GeminiModel | VertexModel;
|
|
6
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { z } from "zod";
|
|
2
|
+
import type { McpConfigSchema } from "../definition";
|
|
3
|
+
type McpConfig = z.infer<typeof McpConfigSchema>;
|
|
4
|
+
interface McpHealthResult {
|
|
5
|
+
name: string;
|
|
6
|
+
status: "pass" | "fail";
|
|
7
|
+
message: string;
|
|
8
|
+
toolCount?: number;
|
|
9
|
+
details?: string;
|
|
10
|
+
}
|
|
11
|
+
export declare function checkAllMcpHealth(configs: McpConfig[], timeoutMs?: number): Promise<McpHealthResult[]>;
|
|
12
|
+
export {};
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { MultiServerMCPClient } from "@langchain/mcp-adapters";
|
|
2
|
+
import { getShedAuth } from "@townco/core/auth";
|
|
3
|
+
const DEFAULT_TIMEOUT_MS = 10000;
|
|
4
|
+
async function resolveServerConfig(config) {
|
|
5
|
+
if (typeof config === "string") {
|
|
6
|
+
const shedAuth = await getShedAuth();
|
|
7
|
+
if (!shedAuth) {
|
|
8
|
+
throw new Error("Not logged in. Run 'town login' or set SHED_API_KEY to use cloud MCP servers.");
|
|
9
|
+
}
|
|
10
|
+
const proxyUrl = process.env.MCP_PROXY_URL ?? `${shedAuth.shedUrl}/mcp_proxy`;
|
|
11
|
+
return {
|
|
12
|
+
url: `${proxyUrl}?server=${config}`,
|
|
13
|
+
headers: {
|
|
14
|
+
Authorization: `Bearer ${shedAuth.accessToken}`,
|
|
15
|
+
},
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
if (config.transport === "http") {
|
|
19
|
+
return config.headers
|
|
20
|
+
? { url: config.url, headers: config.headers }
|
|
21
|
+
: { url: config.url };
|
|
22
|
+
}
|
|
23
|
+
return {
|
|
24
|
+
transport: "stdio",
|
|
25
|
+
command: config.command,
|
|
26
|
+
args: config.args ?? [],
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
function getMcpName(config) {
|
|
30
|
+
return typeof config === "string" ? config : config.name;
|
|
31
|
+
}
|
|
32
|
+
async function checkMcpHealth(config, timeoutMs = DEFAULT_TIMEOUT_MS) {
|
|
33
|
+
const name = getMcpName(config);
|
|
34
|
+
let serverConfig;
|
|
35
|
+
try {
|
|
36
|
+
serverConfig = await resolveServerConfig(config);
|
|
37
|
+
}
|
|
38
|
+
catch (error) {
|
|
39
|
+
return {
|
|
40
|
+
name,
|
|
41
|
+
status: "fail",
|
|
42
|
+
message: "Configuration error",
|
|
43
|
+
details: error instanceof Error ? error.message : String(error),
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
const client = new MultiServerMCPClient({
|
|
47
|
+
throwOnLoadError: true,
|
|
48
|
+
mcpServers: { [name]: serverConfig },
|
|
49
|
+
});
|
|
50
|
+
let timeoutId;
|
|
51
|
+
try {
|
|
52
|
+
const tools = await Promise.race([
|
|
53
|
+
client.getTools(),
|
|
54
|
+
new Promise((_, reject) => {
|
|
55
|
+
timeoutId = setTimeout(() => reject(new Error("Connection timeout")), timeoutMs);
|
|
56
|
+
}),
|
|
57
|
+
]);
|
|
58
|
+
return {
|
|
59
|
+
name,
|
|
60
|
+
status: "pass",
|
|
61
|
+
message: "Connected successfully",
|
|
62
|
+
toolCount: tools.length,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
catch (error) {
|
|
66
|
+
return {
|
|
67
|
+
name,
|
|
68
|
+
status: "fail",
|
|
69
|
+
message: error instanceof Error ? error.message : "Connection failed",
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
finally {
|
|
73
|
+
if (timeoutId)
|
|
74
|
+
clearTimeout(timeoutId);
|
|
75
|
+
await client.close();
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
export async function checkAllMcpHealth(configs, timeoutMs = DEFAULT_TIMEOUT_MS) {
|
|
79
|
+
if (configs.length === 0) {
|
|
80
|
+
return [];
|
|
81
|
+
}
|
|
82
|
+
return Promise.all(configs.map((c) => checkMcpHealth(c, timeoutMs)));
|
|
83
|
+
}
|
|
@@ -184,8 +184,20 @@ Please provide your summary based on the conversation above, following this stru
|
|
|
184
184
|
summaryTokens,
|
|
185
185
|
tokensSaved: inputTokensUsed - summaryTokens,
|
|
186
186
|
});
|
|
187
|
+
// Build citation reference section if citations exist
|
|
188
|
+
// This ensures the agent retains access to citation sources after compaction
|
|
189
|
+
let citationSection = "";
|
|
190
|
+
if (ctx.session.citations && ctx.session.citations.length > 0) {
|
|
191
|
+
const citationLines = ctx.session.citations
|
|
192
|
+
.map((s) => `[[${s.id}]] "${s.title}" - ${s.url}`)
|
|
193
|
+
.join("\n");
|
|
194
|
+
citationSection = `\n\n--- CITATION SOURCES ---\nThe following sources were referenced in the conversation:\n${citationLines}\n--- END CITATIONS ---`;
|
|
195
|
+
logger.info("Injecting citation sources into compaction summary", {
|
|
196
|
+
citationsCount: ctx.session.citations.length,
|
|
197
|
+
});
|
|
198
|
+
}
|
|
187
199
|
// Create a new context entry with the summary
|
|
188
|
-
const summaryEntry = createFullMessageEntry("user", `This session is being continued from a previous conversation that ran out of context. The conversation is summarized below:\n${summaryText}`);
|
|
200
|
+
const summaryEntry = createFullMessageEntry("user", `This session is being continued from a previous conversation that ran out of context. The conversation is summarized below:\n${summaryText}${citationSection}`);
|
|
189
201
|
// Set compactedUpTo to indicate all messages have been compacted into the summary
|
|
190
202
|
const lastMessageIndex = messagesToCompact.length - 1;
|
|
191
203
|
const newContextEntry = createContextEntry([summaryEntry], undefined, lastMessageIndex, {
|
|
@@ -180,9 +180,21 @@ Please provide your summary based on the conversation above, following this stru
|
|
|
180
180
|
summaryTokens,
|
|
181
181
|
tokensSaved: inputTokensUsed - summaryTokens,
|
|
182
182
|
});
|
|
183
|
+
// Build citation reference section if citations exist
|
|
184
|
+
// This ensures the agent retains access to citation sources after compaction
|
|
185
|
+
let citationSection = "";
|
|
186
|
+
if (ctx.session.citations && ctx.session.citations.length > 0) {
|
|
187
|
+
const citationLines = ctx.session.citations
|
|
188
|
+
.map((s) => `[[${s.id}]] "${s.title}" - ${s.url}`)
|
|
189
|
+
.join("\n");
|
|
190
|
+
citationSection = `\n\n--- CITATION SOURCES ---\nThe following sources were referenced in the conversation:\n${citationLines}\n--- END CITATIONS ---`;
|
|
191
|
+
logger.info("Injecting citation sources into compaction summary", {
|
|
192
|
+
citationsCount: ctx.session.citations.length,
|
|
193
|
+
});
|
|
194
|
+
}
|
|
183
195
|
// Create a new context entry with the summary
|
|
184
196
|
// Mark it as a mid-turn compaction so the agent knows to continue
|
|
185
|
-
const summaryEntry = createFullMessageEntry("user", `This session is being continued from a previous conversation that ran out of context during a tool call. The conversation AND the pending tool response are summarized below. IMPORTANT: Continue from where you left off.\n\n${summaryText}`);
|
|
197
|
+
const summaryEntry = createFullMessageEntry("user", `This session is being continued from a previous conversation that ran out of context during a tool call. The conversation AND the pending tool response are summarized below. IMPORTANT: Continue from where you left off.\n\n${summaryText}${citationSection}`);
|
|
186
198
|
// Set compactedUpTo to indicate all messages have been compacted into the summary
|
|
187
199
|
const lastMessageIndex = messagesToCompact.length - 1;
|
|
188
200
|
const newContextEntry = createContextEntry([summaryEntry], undefined, lastMessageIndex, {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { CitationSource } from "../../acp-server/adapter";
|
|
1
2
|
import type { ContextEntry } from "../../acp-server/session-storage";
|
|
2
3
|
import type { AgentDefinition } from "../../definition";
|
|
3
4
|
import type { SessionMessage } from "../agent-runner";
|
|
@@ -88,6 +89,11 @@ export interface ReadonlySession {
|
|
|
88
89
|
* Original request parameters
|
|
89
90
|
*/
|
|
90
91
|
readonly requestParams: Readonly<Record<string, unknown>>;
|
|
92
|
+
/**
|
|
93
|
+
* Citation sources accumulated in this session
|
|
94
|
+
* Used for injecting into compaction summaries so agent retains citation references
|
|
95
|
+
*/
|
|
96
|
+
readonly citations?: ReadonlyArray<Readonly<CitationSource>>;
|
|
91
97
|
}
|
|
92
98
|
/**
|
|
93
99
|
* Context passed to hook callbacks
|