@townco/agent 0.1.78 → 0.1.79

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.
@@ -65,16 +65,235 @@ export function makeHttpTransport(agent, agentDir, agentName) {
65
65
  isSubagent: isSubagent(),
66
66
  });
67
67
  }
68
- const inbound = new TransformStream();
69
- const outbound = new TransformStream();
70
- const bridge = acp.ndJsonStream(outbound.writable, inbound.readable);
71
68
  const agentRunner = "definition" in agent ? agent : makeRunnerFromDefinition(agent);
72
- // Store adapter reference so we can call methods on it (e.g., storeInitialMessage)
69
+ const sessionAcpMap = new Map();
70
+ // Create or get session-specific ACP infrastructure
71
+ function getOrCreateSessionAcp(sessionId) {
72
+ let sessionAcp = sessionAcpMap.get(sessionId);
73
+ if (!sessionAcp) {
74
+ const inbound = new TransformStream();
75
+ const outbound = new TransformStream();
76
+ const bridge = acp.ndJsonStream(outbound.writable, inbound.readable);
77
+ const encoder = new TextEncoder();
78
+ let adapter = null;
79
+ new acp.AgentSideConnection((conn) => {
80
+ adapter = new AgentAcpAdapter(agentRunner, conn, agentDir, agentName);
81
+ return adapter;
82
+ }, bridge);
83
+ // Start processing outbound messages for this session
84
+ const decoder = new TextDecoder();
85
+ (async () => {
86
+ const reader = outbound.readable.getReader();
87
+ let buf = "";
88
+ for (;;) {
89
+ const { value, done } = await reader.read();
90
+ if (done)
91
+ break;
92
+ buf += decoder.decode(value, { stream: true });
93
+ for (let nl = buf.indexOf("\n"); nl !== -1; nl = buf.indexOf("\n")) {
94
+ const line = buf.slice(0, nl).trim();
95
+ buf = buf.slice(nl + 1);
96
+ if (!line)
97
+ continue;
98
+ let rawMsg;
99
+ try {
100
+ rawMsg = JSON.parse(line);
101
+ }
102
+ catch {
103
+ continue;
104
+ }
105
+ const parseResult = acp.agentOutgoingMessageSchema.safeParse(rawMsg);
106
+ if (!parseResult.success) {
107
+ logger.error("Invalid agent message", {
108
+ issues: parseResult.error.issues,
109
+ });
110
+ continue;
111
+ }
112
+ if (rawMsg == null || typeof rawMsg !== "object") {
113
+ logger.warn("Malformed message, cannot route", {
114
+ message: rawMsg,
115
+ });
116
+ continue;
117
+ }
118
+ // Route response to correct channel
119
+ if ("id" in rawMsg &&
120
+ typeof rawMsg.id === "string" &&
121
+ rawMsg.id != null) {
122
+ const channel = safeChannelName("response", rawMsg.id);
123
+ const { payload, isCompressed, originalSize, compressedSize } = compressIfNeeded(rawMsg);
124
+ if (isCompressed) {
125
+ logger.info("Compressed response payload", {
126
+ requestId: rawMsg.id,
127
+ originalSize,
128
+ compressedSize,
129
+ compressionRatio: `${((1 - compressedSize / originalSize) * 100).toFixed(1)}%`,
130
+ });
131
+ }
132
+ const escapedPayload = payload.replace(/'/g, "''");
133
+ if (compressedSize > 7500) {
134
+ logger.error("Response payload too large even after compression", {
135
+ requestId: rawMsg.id,
136
+ originalSize,
137
+ compressedSize,
138
+ });
139
+ const errorResponse = {
140
+ jsonrpc: "2.0",
141
+ id: rawMsg.id,
142
+ error: {
143
+ code: -32603,
144
+ message: "Response payload too large even after compression",
145
+ data: { originalSize, compressedSize },
146
+ },
147
+ };
148
+ const errorPayload = JSON.stringify(errorResponse).replace(/'/g, "''");
149
+ await pg.query(`NOTIFY ${channel}, '${errorPayload}'`);
150
+ continue;
151
+ }
152
+ try {
153
+ await pg.query(`NOTIFY ${channel}, '${escapedPayload}'`);
154
+ }
155
+ catch (error) {
156
+ logger.error("Failed to send response", {
157
+ error,
158
+ requestId: rawMsg.id,
159
+ });
160
+ const errorResponse = {
161
+ jsonrpc: "2.0",
162
+ id: rawMsg.id,
163
+ error: {
164
+ code: -32603,
165
+ message: "Failed to send response",
166
+ data: {
167
+ error: error instanceof Error ? error.message : String(error),
168
+ },
169
+ },
170
+ };
171
+ const errorPayload = JSON.stringify(errorResponse).replace(/'/g, "''");
172
+ await pg.query(`NOTIFY ${channel}, '${errorPayload}'`);
173
+ }
174
+ }
175
+ else if ("params" in rawMsg &&
176
+ rawMsg.params != null &&
177
+ typeof rawMsg.params === "object" &&
178
+ "sessionId" in rawMsg.params &&
179
+ typeof rawMsg.params.sessionId === "string") {
180
+ const msgSessionId = rawMsg.params.sessionId;
181
+ const messageType = "method" in rawMsg && typeof rawMsg.method === "string"
182
+ ? rawMsg.method
183
+ : undefined;
184
+ // Handle tool_output - send directly via SSE
185
+ if (messageType === "session/update" &&
186
+ "params" in rawMsg &&
187
+ rawMsg.params != null &&
188
+ typeof rawMsg.params === "object" &&
189
+ "update" in rawMsg.params &&
190
+ rawMsg.params.update != null &&
191
+ typeof rawMsg.params.update === "object" &&
192
+ "sessionUpdate" in rawMsg.params.update &&
193
+ rawMsg.params.update.sessionUpdate === "tool_output") {
194
+ const stream = sseStreams.get(msgSessionId);
195
+ if (stream) {
196
+ try {
197
+ await stream.writeSSE({
198
+ event: "message",
199
+ data: JSON.stringify(rawMsg),
200
+ });
201
+ }
202
+ catch (error) {
203
+ logger.error("Failed to send tool output", {
204
+ error,
205
+ sessionId: msgSessionId,
206
+ });
207
+ }
208
+ }
209
+ continue;
210
+ }
211
+ // Handle subagent tool calls - send directly via SSE
212
+ if (messageType === "session/update" &&
213
+ "params" in rawMsg &&
214
+ rawMsg.params != null &&
215
+ typeof rawMsg.params === "object" &&
216
+ "update" in rawMsg.params &&
217
+ rawMsg.params.update != null &&
218
+ typeof rawMsg.params.update === "object" &&
219
+ "sessionUpdate" in rawMsg.params.update &&
220
+ rawMsg.params.update.sessionUpdate === "tool_call" &&
221
+ "_meta" in rawMsg.params.update &&
222
+ rawMsg.params.update._meta != null &&
223
+ typeof rawMsg.params.update._meta === "object" &&
224
+ "subagentMessages" in rawMsg.params.update._meta) {
225
+ const stream = sseStreams.get(msgSessionId);
226
+ if (stream) {
227
+ try {
228
+ await stream.writeSSE({
229
+ event: "message",
230
+ data: JSON.stringify(rawMsg),
231
+ });
232
+ }
233
+ catch (error) {
234
+ logger.error("Failed to send subagent tool call", {
235
+ error,
236
+ sessionId: msgSessionId,
237
+ });
238
+ }
239
+ }
240
+ continue;
241
+ }
242
+ // Regular session update - send via PubSub
243
+ const channel = safeChannelName("notifications", msgSessionId);
244
+ const { payload, isCompressed, originalSize, compressedSize } = compressIfNeeded(rawMsg);
245
+ if (compressedSize <= 7500) {
246
+ const escapedPayload = payload.replace(/'/g, "''");
247
+ try {
248
+ await pg.query(`NOTIFY ${channel}, '${escapedPayload}'`);
249
+ }
250
+ catch (error) {
251
+ logger.error("Failed to notify session channel", {
252
+ error,
253
+ sessionId: msgSessionId,
254
+ });
255
+ }
256
+ }
257
+ else {
258
+ const stream = sseStreams.get(msgSessionId);
259
+ if (stream) {
260
+ try {
261
+ await stream.writeSSE({
262
+ event: "message",
263
+ data: JSON.stringify(rawMsg),
264
+ });
265
+ }
266
+ catch (error) {
267
+ logger.error("Failed to send large message via SSE", {
268
+ error,
269
+ sessionId: msgSessionId,
270
+ });
271
+ }
272
+ }
273
+ }
274
+ }
275
+ }
276
+ }
277
+ })();
278
+ if (!adapter) {
279
+ throw new Error("Failed to create ACP adapter for session");
280
+ }
281
+ sessionAcp = { inbound, outbound, adapter, encoder };
282
+ sessionAcpMap.set(sessionId, sessionAcp);
283
+ logger.debug("Created new session ACP infrastructure", { sessionId });
284
+ }
285
+ return sessionAcp;
286
+ }
287
+ // Legacy global adapter for backwards compatibility with initialize/etc
288
+ // These don't need session isolation
289
+ const globalInbound = new TransformStream();
290
+ const globalOutbound = new TransformStream();
291
+ const globalBridge = acp.ndJsonStream(globalOutbound.writable, globalInbound.readable);
73
292
  let acpAdapter = null;
74
293
  new acp.AgentSideConnection((conn) => {
75
294
  acpAdapter = new AgentAcpAdapter(agentRunner, conn, agentDir, agentName);
76
295
  return acpAdapter;
77
- }, bridge);
296
+ }, globalBridge);
78
297
  const app = new Hono();
79
298
  // Track active SSE streams by sessionId for direct output delivery
80
299
  const sseStreams = new Map();
@@ -82,16 +301,19 @@ export function makeHttpTransport(agent, agentDir, agentName) {
82
301
  const initialMessageSentSessions = new Set();
83
302
  // Get initial message config from agent definition
84
303
  const initialMessageConfig = agentRunner.definition.initialMessage;
85
- const decoder = new TextDecoder();
304
+ // Global encoder for non-session-specific operations
86
305
  const encoder = new TextEncoder();
306
+ // Start processing outbound messages from the global adapter
307
+ // This handles non-session-specific operations like initialize
308
+ const globalDecoder = new TextDecoder();
87
309
  (async () => {
88
- const reader = outbound.readable.getReader();
310
+ const reader = globalOutbound.readable.getReader();
89
311
  let buf = "";
90
312
  for (;;) {
91
313
  const { value, done } = await reader.read();
92
314
  if (done)
93
315
  break;
94
- buf += decoder.decode(value, { stream: true });
316
+ buf += globalDecoder.decode(value, { stream: true });
95
317
  for (let nl = buf.indexOf("\n"); nl !== -1; nl = buf.indexOf("\n")) {
96
318
  const line = buf.slice(0, nl).trim();
97
319
  buf = buf.slice(nl + 1);
@@ -102,39 +324,36 @@ export function makeHttpTransport(agent, agentDir, agentName) {
102
324
  rawMsg = JSON.parse(line);
103
325
  }
104
326
  catch {
105
- // ignore malformed lines
106
327
  continue;
107
328
  }
108
- // Validate the agent message with Zod schema
109
329
  const parseResult = acp.agentOutgoingMessageSchema.safeParse(rawMsg);
110
330
  if (!parseResult.success) {
111
- logger.error("Invalid agent message", {
331
+ logger.error("Invalid agent message from global adapter", {
112
332
  issues: parseResult.error.issues,
113
333
  });
114
334
  continue;
115
335
  }
116
- //const msg = parseResult.data;
117
336
  if (rawMsg == null || typeof rawMsg !== "object") {
118
- logger.warn("Malformed message, cannot route", { message: rawMsg });
337
+ logger.warn("Malformed global message, cannot route", {
338
+ message: rawMsg,
339
+ });
119
340
  continue;
120
341
  }
342
+ // Route response to correct channel
121
343
  if ("id" in rawMsg &&
122
344
  typeof rawMsg.id === "string" &&
123
345
  rawMsg.id != null) {
124
- // This is a response to a request - send to response-specific channel
125
346
  const channel = safeChannelName("response", rawMsg.id);
126
347
  const { payload, isCompressed, originalSize, compressedSize } = compressIfNeeded(rawMsg);
127
348
  if (isCompressed) {
128
- logger.info("Compressed response payload", {
349
+ logger.info("Compressed global response payload", {
129
350
  requestId: rawMsg.id,
130
351
  originalSize,
131
352
  compressedSize,
132
353
  compressionRatio: `${((1 - compressedSize / originalSize) * 100).toFixed(1)}%`,
133
354
  });
134
355
  }
135
- // Escape single quotes for PostgreSQL
136
356
  const escapedPayload = payload.replace(/'/g, "''");
137
- // Check if even compressed payload is too large
138
357
  if (compressedSize > 7500) {
139
358
  logger.info("Response payload too large for NOTIFY, using direct storage", {
140
359
  requestId: rawMsg.id,
@@ -157,13 +376,10 @@ export function makeHttpTransport(agent, agentDir, agentName) {
157
376
  await pg.query(`NOTIFY ${channel}, '${escapedPayload}'`);
158
377
  }
159
378
  catch (error) {
160
- logger.error("Failed to send response", {
379
+ logger.error("Failed to send global response", {
161
380
  error,
162
381
  requestId: rawMsg.id,
163
- originalSize,
164
- compressedSize,
165
382
  });
166
- // For responses, we still need to send something to unblock the client
167
383
  const errorResponse = {
168
384
  jsonrpc: "2.0",
169
385
  id: rawMsg.id,
@@ -171,8 +387,6 @@ export function makeHttpTransport(agent, agentDir, agentName) {
171
387
  code: -32603,
172
388
  message: "Failed to send response",
173
389
  data: {
174
- originalSize,
175
- compressedSize,
176
390
  error: error instanceof Error ? error.message : String(error),
177
391
  },
178
392
  },
@@ -181,135 +395,6 @@ export function makeHttpTransport(agent, agentDir, agentName) {
181
395
  await pg.query(`NOTIFY ${channel}, '${errorPayload}'`);
182
396
  }
183
397
  }
184
- else if ("params" in rawMsg &&
185
- rawMsg.params != null &&
186
- typeof rawMsg.params === "object" &&
187
- "sessionId" in rawMsg.params &&
188
- typeof rawMsg.params.sessionId === "string") {
189
- const sessionId = rawMsg.params.sessionId;
190
- const messageType = "method" in rawMsg && typeof rawMsg.method === "string"
191
- ? rawMsg.method
192
- : undefined;
193
- // Check if this is a tool_output update - send directly via SSE
194
- if (messageType === "session/update" &&
195
- "params" in rawMsg &&
196
- rawMsg.params != null &&
197
- typeof rawMsg.params === "object" &&
198
- "update" in rawMsg.params &&
199
- rawMsg.params.update != null &&
200
- typeof rawMsg.params.update === "object" &&
201
- "sessionUpdate" in rawMsg.params.update &&
202
- rawMsg.params.update.sessionUpdate === "tool_output") {
203
- // Send tool output directly via SSE, bypassing PostgreSQL NOTIFY
204
- const stream = sseStreams.get(sessionId);
205
- if (stream) {
206
- try {
207
- await stream.writeSSE({
208
- event: "message",
209
- data: JSON.stringify(rawMsg),
210
- });
211
- logger.debug("Sent tool output", {
212
- sessionId,
213
- payloadSize: JSON.stringify(rawMsg).length,
214
- });
215
- }
216
- catch (error) {
217
- logger.error("Failed to send tool output", {
218
- error,
219
- sessionId,
220
- });
221
- }
222
- }
223
- else {
224
- logger.warn("No SSE stream found for tool output", { sessionId });
225
- }
226
- continue;
227
- }
228
- // Check if this is a tool_call with subagentMessages - send directly via SSE
229
- // to bypass PostgreSQL NOTIFY size limits (7500 bytes)
230
- if (messageType === "session/update" &&
231
- "params" in rawMsg &&
232
- rawMsg.params != null &&
233
- typeof rawMsg.params === "object" &&
234
- "update" in rawMsg.params &&
235
- rawMsg.params.update != null &&
236
- typeof rawMsg.params.update === "object" &&
237
- "sessionUpdate" in rawMsg.params.update &&
238
- rawMsg.params.update.sessionUpdate === "tool_call" &&
239
- "_meta" in rawMsg.params.update &&
240
- rawMsg.params.update._meta != null &&
241
- typeof rawMsg.params.update._meta === "object" &&
242
- "subagentMessages" in rawMsg.params.update._meta) {
243
- // Send subagent tool call directly via SSE, bypassing PostgreSQL NOTIFY
244
- const stream = sseStreams.get(sessionId);
245
- if (stream) {
246
- try {
247
- await stream.writeSSE({
248
- event: "message",
249
- data: JSON.stringify(rawMsg),
250
- });
251
- logger.debug("Sent subagent tool call directly via SSE", {
252
- sessionId,
253
- payloadSize: JSON.stringify(rawMsg).length,
254
- });
255
- }
256
- catch (error) {
257
- logger.error("Failed to send subagent tool call", {
258
- error,
259
- sessionId,
260
- });
261
- }
262
- }
263
- else {
264
- logger.warn("No SSE stream found for subagent tool call", {
265
- sessionId,
266
- });
267
- }
268
- continue;
269
- }
270
- // Other messages (notifications, requests from agent) go to
271
- // session-specific channel via PostgreSQL NOTIFY
272
- const channel = safeChannelName("notifications", sessionId);
273
- const { payload, isCompressed, originalSize, compressedSize } = compressIfNeeded(rawMsg);
274
- if (isCompressed) {
275
- logger.info("Compressed notification payload", {
276
- sessionId,
277
- messageType,
278
- originalSize,
279
- compressedSize,
280
- compressionRatio: `${((1 - compressedSize / originalSize) * 100).toFixed(1)}%`,
281
- });
282
- }
283
- // Escape single quotes for PostgreSQL
284
- const escapedPayload = payload.replace(/'/g, "''");
285
- // Check if even compressed payload is too large
286
- if (compressedSize > 7500) {
287
- logger.error("Notification payload too large even after compression, skipping", {
288
- sessionId,
289
- messageType,
290
- originalSize,
291
- compressedSize,
292
- });
293
- continue;
294
- }
295
- try {
296
- await pg.query(`NOTIFY ${channel}, '${escapedPayload}'`);
297
- }
298
- catch (error) {
299
- logger.error("Failed to send notification", {
300
- error,
301
- sessionId,
302
- messageType,
303
- originalSize,
304
- compressedSize,
305
- });
306
- }
307
- }
308
- else {
309
- logger.warn("Message without sessionId, cannot route", {
310
- message: rawMsg,
311
- });
312
- }
313
398
  }
314
399
  }
315
400
  })();
@@ -342,6 +427,34 @@ export function makeHttpTransport(agent, agentDir, agentName) {
342
427
  }, 500);
343
428
  }
344
429
  });
430
+ // Get full session data by ID
431
+ app.get("/sessions/:sessionId", async (c) => {
432
+ if (!agentDir || !agentName) {
433
+ return c.json({ error: "Session storage not configured" }, 500);
434
+ }
435
+ const noSession = process.env.TOWN_NO_SESSION === "true";
436
+ if (noSession) {
437
+ return c.json({ error: "Sessions disabled" }, 500);
438
+ }
439
+ const sessionId = c.req.param("sessionId");
440
+ if (!sessionId) {
441
+ return c.json({ error: "Session ID required" }, 400);
442
+ }
443
+ try {
444
+ const storage = new SessionStorage(agentDir, agentName);
445
+ const session = await storage.loadSession(sessionId);
446
+ if (!session) {
447
+ return c.json({ error: "Session not found" }, 404);
448
+ }
449
+ return c.json(session);
450
+ }
451
+ catch (error) {
452
+ logger.error("Failed to load session", { error, sessionId });
453
+ return c.json({
454
+ error: error instanceof Error ? error.message : String(error),
455
+ }, 500);
456
+ }
457
+ });
345
458
  // Serve static files from agent directory (for generated images, etc.)
346
459
  if (agentDir) {
347
460
  app.get("/static/*", async (c) => {
@@ -511,6 +624,56 @@ export function makeHttpTransport(agent, agentDir, agentName) {
511
624
  params: "params" in body ? body.params : "NO PARAMS",
512
625
  });
513
626
  }
627
+ // Determine which stream to use based on method
628
+ // Session-specific methods go to per-session streams to avoid interference
629
+ const sessionMethods = [
630
+ "session/new",
631
+ "session/prompt",
632
+ "session/load",
633
+ "session/stop",
634
+ ];
635
+ const isSessionMethod = sessionMethods.includes(method);
636
+ // Extract sessionId from params for existing session operations
637
+ let sessionId = null;
638
+ if (isSessionMethod &&
639
+ method !== "session/new" &&
640
+ "params" in body &&
641
+ body.params != null &&
642
+ typeof body.params === "object" &&
643
+ "sessionId" in body.params &&
644
+ typeof body.params.sessionId === "string") {
645
+ sessionId = body.params.sessionId;
646
+ }
647
+ // For session/new, generate sessionId upfront so we can create the session ACP
648
+ if (method === "session/new") {
649
+ sessionId = crypto.randomUUID();
650
+ // Inject the sessionId into the request if not already present
651
+ if ("params" in body &&
652
+ body.params != null &&
653
+ typeof body.params === "object") {
654
+ body.params.sessionId = sessionId;
655
+ }
656
+ else {
657
+ body.params = { sessionId };
658
+ }
659
+ logger.debug("POST /rpc - session/new with generated sessionId", {
660
+ sessionId,
661
+ });
662
+ }
663
+ // Choose the appropriate inbound stream
664
+ let inboundStream;
665
+ if (isSessionMethod && sessionId) {
666
+ const sessionAcp = getOrCreateSessionAcp(sessionId);
667
+ inboundStream = sessionAcp.inbound;
668
+ logger.debug("POST /rpc - Using session-specific stream", {
669
+ sessionId,
670
+ method,
671
+ });
672
+ }
673
+ else {
674
+ inboundStream = globalInbound;
675
+ logger.debug("POST /rpc - Using global stream", { method });
676
+ }
514
677
  if (isRequest) {
515
678
  // For requests, set up a listener before sending the request
516
679
  const responseChannel = safeChannelName("response", String(id));
@@ -573,8 +736,8 @@ export function makeHttpTransport(agent, agentDir, agentName) {
573
736
  }
574
737
  responseResolver(rawResponse);
575
738
  });
576
- // Write NDJSON line into the ACP inbound stream
577
- const writer = inbound.writable.getWriter();
739
+ // Write NDJSON line into the appropriate ACP inbound stream
740
+ const writer = inboundStream.writable.getWriter();
578
741
  await writer.write(encoder.encode(`${JSON.stringify(body)}\n`));
579
742
  writer.releaseLock();
580
743
  // Wait for response with ten minute timeout
@@ -582,6 +745,29 @@ export function makeHttpTransport(agent, agentDir, agentName) {
582
745
  try {
583
746
  const response = await Promise.race([responsePromise, timeoutPromise]);
584
747
  logger.debug("POST /rpc - Response received", { id });
748
+ // For session/new responses, map the returned sessionId to the same adapter
749
+ // The adapter may generate its own sessionId, so we need to ensure subsequent
750
+ // requests using that sessionId route to the correct adapter
751
+ if (method === "session/new" &&
752
+ sessionId &&
753
+ response &&
754
+ typeof response === "object" &&
755
+ "result" in response &&
756
+ response.result &&
757
+ typeof response.result === "object" &&
758
+ "sessionId" in response.result &&
759
+ typeof response.result.sessionId === "string" &&
760
+ response.result.sessionId !== sessionId) {
761
+ const returnedSessionId = response.result.sessionId;
762
+ const existingAcp = sessionAcpMap.get(sessionId);
763
+ if (existingAcp) {
764
+ sessionAcpMap.set(returnedSessionId, existingAcp);
765
+ logger.debug("POST /rpc - Added sessionId alias for session/new response", {
766
+ generatedSessionId: sessionId,
767
+ returnedSessionId,
768
+ });
769
+ }
770
+ }
585
771
  return c.json(response);
586
772
  }
587
773
  catch (error) {
@@ -598,7 +784,7 @@ export function makeHttpTransport(agent, agentDir, agentName) {
598
784
  }
599
785
  else {
600
786
  // For notifications, just send and return success
601
- const writer = inbound.writable.getWriter();
787
+ const writer = inboundStream.writable.getWriter();
602
788
  await writer.write(encoder.encode(`${JSON.stringify(body)}\n`));
603
789
  writer.releaseLock();
604
790
  return c.json({
@@ -125,6 +125,7 @@ export interface ContextEntry {
125
125
  toolResultsTokens: number;
126
126
  totalEstimated: number;
127
127
  llmReportedInputTokens?: number | undefined;
128
+ modelContextWindow?: number | undefined;
128
129
  };
129
130
  }
130
131
  /**
@@ -115,6 +115,7 @@ const contextEntrySchema = z.object({
115
115
  toolResultsTokens: z.number(),
116
116
  totalEstimated: z.number(),
117
117
  llmReportedInputTokens: z.number().optional(),
118
+ modelContextWindow: z.number().optional(),
118
119
  }),
119
120
  });
120
121
  const sessionMetadataSchema = z.object({
@@ -7,3 +7,7 @@ export declare const MODEL_CONTEXT_WINDOWS: Record<string, number>;
7
7
  * Default context size for unknown models
8
8
  */
9
9
  export declare const DEFAULT_CONTEXT_SIZE = 200000;
10
+ /**
11
+ * Get max context size for a model
12
+ */
13
+ export declare function getModelContextWindow(model: string): number;
@@ -17,3 +17,9 @@ export const MODEL_CONTEXT_WINDOWS = {
17
17
  * Default context size for unknown models
18
18
  */
19
19
  export const DEFAULT_CONTEXT_SIZE = 200000;
20
+ /**
21
+ * Get max context size for a model
22
+ */
23
+ export function getModelContextWindow(model) {
24
+ return MODEL_CONTEXT_WINDOWS[model] ?? DEFAULT_CONTEXT_SIZE;
25
+ }