@townco/agent 0.1.116 → 0.1.118

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.
@@ -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
- await this.storage.saveSession(sessionId, session.messages, session.context);
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
- const session = this.sessions.get(params.sessionId);
923
- if (session) {
924
- const extractedSources = this.extractSourcesFromToolOutput(block.title, block.rawOutput, block.id, session);
925
- if (extractedSources.length > 0) {
926
- session.sources.push(...extractedSources);
927
- // Emit sources notification to client during replay
928
- this.connection.sessionUpdate({
929
- sessionId: params.sessionId,
930
- update: {
931
- sessionUpdate: "sources",
932
- sources: extractedSources,
933
- },
934
- });
935
- logger.info("Extracted sources during session replay", {
936
- toolCallId: block.id,
937
- toolName: block.title,
938
- sourcesCount: extractedSources.length,
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
- // Handle subagent tool outputs - extract citation sources
1754
- // Subagent tools return { text: string, sources: CitationSource[] }
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
@@ -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
- logger.trace("Sending SSE message", { sessionId, channel });
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
- export type AgentDefinition = z.infer<typeof AgentDefinitionSchema>;
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