@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.
- package/dist/acp-server/adapter.js +104 -19
- package/dist/acp-server/http.js +341 -155
- package/dist/acp-server/session-storage.d.ts +1 -0
- package/dist/acp-server/session-storage.js +1 -0
- package/dist/runner/hooks/constants.d.ts +4 -0
- package/dist/runner/hooks/constants.js +6 -0
- package/dist/runner/hooks/executor.d.ts +11 -1
- package/dist/runner/hooks/executor.js +84 -27
- package/dist/runner/hooks/types.d.ts +3 -0
- package/dist/runner/langchain/index.d.ts +1 -0
- package/dist/runner/langchain/index.js +65 -34
- package/dist/runner/langchain/otel-callbacks.js +9 -1
- package/dist/runner/langchain/tools/todo.d.ts +23 -0
- package/dist/runner/langchain/tools/todo.js +25 -16
- package/dist/telemetry/index.d.ts +18 -0
- package/dist/telemetry/index.js +50 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/utils/context-size-calculator.d.ts +4 -1
- package/dist/utils/context-size-calculator.js +10 -2
- package/package.json +6 -6
package/dist/acp-server/http.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
},
|
|
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
|
-
|
|
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 =
|
|
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 +=
|
|
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", {
|
|
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 =
|
|
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 =
|
|
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({
|
|
@@ -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
|
+
}
|