@townco/agent 0.1.81 → 0.1.83
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 +12 -12
- package/dist/acp-server/http.js +1 -1
- package/dist/acp-server/session-storage.d.ts +13 -6
- package/dist/acp-server/session-storage.js +94 -59
- package/dist/runner/agent-runner.d.ts +3 -1
- package/dist/runner/hooks/executor.js +1 -1
- package/dist/runner/hooks/predefined/compaction-tool.js +31 -8
- package/dist/runner/hooks/predefined/tool-response-compactor.js +2 -2
- package/dist/runner/langchain/index.d.ts +1 -0
- package/dist/runner/langchain/index.js +151 -27
- package/dist/runner/langchain/tools/artifacts.d.ts +68 -0
- package/dist/runner/langchain/tools/artifacts.js +469 -0
- package/dist/runner/langchain/tools/browser.js +15 -3
- package/dist/runner/langchain/tools/filesystem.d.ts +8 -4
- package/dist/runner/langchain/tools/filesystem.js +118 -82
- package/dist/runner/langchain/tools/generate_image.d.ts +19 -0
- package/dist/runner/langchain/tools/generate_image.js +54 -14
- package/dist/runner/langchain/tools/subagent.js +2 -2
- package/dist/runner/langchain/tools/todo.js +3 -0
- package/dist/runner/langchain/tools/web_search.js +6 -0
- package/dist/runner/session-context.d.ts +40 -0
- package/dist/runner/session-context.js +69 -0
- package/dist/runner/tools.d.ts +2 -2
- package/dist/runner/tools.js +2 -0
- package/dist/scaffold/project-scaffold.js +7 -3
- package/dist/telemetry/setup.js +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/utils/context-size-calculator.d.ts +1 -2
- package/dist/utils/context-size-calculator.js +2 -6
- package/dist/utils/token-counter.js +2 -2
- package/package.json +10 -10
|
@@ -278,8 +278,10 @@ export class AgentAcpAdapter {
|
|
|
278
278
|
return response;
|
|
279
279
|
}
|
|
280
280
|
async newSession(params) {
|
|
281
|
-
//
|
|
282
|
-
|
|
281
|
+
// Use sessionId from params if provided (HTTP transport injects it),
|
|
282
|
+
// otherwise generate a unique session ID for this session
|
|
283
|
+
const sessionId = params.sessionId ??
|
|
284
|
+
Math.random().toString(36).substring(2);
|
|
283
285
|
// Extract configOverrides from _meta if provided (Town Hall comparison feature)
|
|
284
286
|
const configOverrides = params._meta?.configOverrides;
|
|
285
287
|
const sessionData = {
|
|
@@ -602,8 +604,7 @@ export class AgentAcpAdapter {
|
|
|
602
604
|
const context_size = calculateContextSize(contextMessages, this.agent.definition.systemPrompt ?? undefined, undefined, // No LLM-reported tokens yet
|
|
603
605
|
this.currentToolOverheadTokens, // Include tool overhead
|
|
604
606
|
this.currentMcpOverheadTokens, // Include MCP overhead
|
|
605
|
-
getModelContextWindow(this.agent.definition.model)
|
|
606
|
-
true);
|
|
607
|
+
getModelContextWindow(this.agent.definition.model));
|
|
607
608
|
const contextSnapshot = createContextSnapshot(session.messages.length - 1, // Exclude the newly added user message (it will be passed separately via prompt)
|
|
608
609
|
new Date().toISOString(), previousContext, context_size);
|
|
609
610
|
session.context.push(contextSnapshot);
|
|
@@ -620,7 +621,7 @@ export class AgentAcpAdapter {
|
|
|
620
621
|
}
|
|
621
622
|
};
|
|
622
623
|
// Declare agentResponse and turnTokenUsage outside try block so they're accessible after catch
|
|
623
|
-
let
|
|
624
|
+
let _agentResponse;
|
|
624
625
|
// Track accumulated token usage during the turn
|
|
625
626
|
const turnTokenUsage = {
|
|
626
627
|
inputTokens: 0,
|
|
@@ -654,8 +655,8 @@ export class AgentAcpAdapter {
|
|
|
654
655
|
latestContextEntry: session.context.length > 0 &&
|
|
655
656
|
session.context[session.context.length - 1]
|
|
656
657
|
? {
|
|
657
|
-
messageCount: session.context[session.context.length - 1]
|
|
658
|
-
contextSize: session.context[session.context.length - 1]
|
|
658
|
+
messageCount: session.context[session.context.length - 1]?.messages.length,
|
|
659
|
+
contextSize: session.context[session.context.length - 1]?.context_size,
|
|
659
660
|
}
|
|
660
661
|
: null,
|
|
661
662
|
});
|
|
@@ -663,6 +664,8 @@ export class AgentAcpAdapter {
|
|
|
663
664
|
prompt: params.prompt,
|
|
664
665
|
sessionId: params.sessionId,
|
|
665
666
|
messageId,
|
|
667
|
+
// Pass agent directory for session-scoped file storage (only if defined)
|
|
668
|
+
...(this.agentDir ? { agentDir: this.agentDir } : {}),
|
|
666
669
|
// Pass resolved context messages to agent
|
|
667
670
|
contextMessages,
|
|
668
671
|
};
|
|
@@ -1155,7 +1158,7 @@ export class AgentAcpAdapter {
|
|
|
1155
1158
|
iterResult = await generator.next();
|
|
1156
1159
|
}
|
|
1157
1160
|
// Capture the return value (PromptResponse with tokenUsage)
|
|
1158
|
-
|
|
1161
|
+
_agentResponse = iterResult.value;
|
|
1159
1162
|
// Flush any remaining pending text
|
|
1160
1163
|
flushPendingText();
|
|
1161
1164
|
}
|
|
@@ -1215,13 +1218,10 @@ export class AgentAcpAdapter {
|
|
|
1215
1218
|
}
|
|
1216
1219
|
}
|
|
1217
1220
|
// Calculate context size with LLM-reported tokens from this turn
|
|
1218
|
-
// Exclude tool results - they're only sent during the turn they were received,
|
|
1219
|
-
// not in subsequent turns (only messages are sent)
|
|
1220
1221
|
const context_size = calculateContextSize(contextMessages, this.agent.definition.systemPrompt ?? undefined, turnTokenUsage.inputTokens, // Final LLM-reported tokens from this turn
|
|
1221
1222
|
this.currentToolOverheadTokens, // Include tool overhead
|
|
1222
1223
|
this.currentMcpOverheadTokens, // Include MCP overhead
|
|
1223
|
-
getModelContextWindow(this.agent.definition.model)
|
|
1224
|
-
true);
|
|
1224
|
+
getModelContextWindow(this.agent.definition.model));
|
|
1225
1225
|
const contextSnapshot = createContextSnapshot(session.messages.length, new Date().toISOString(), previousContext, context_size);
|
|
1226
1226
|
session.context.push(contextSnapshot);
|
|
1227
1227
|
await this.saveSessionToDisk(params.sessionId, session);
|
package/dist/acp-server/http.js
CHANGED
|
@@ -241,7 +241,7 @@ export function makeHttpTransport(agent, agentDir, agentName) {
|
|
|
241
241
|
}
|
|
242
242
|
// Regular session update - send via PubSub
|
|
243
243
|
const channel = safeChannelName("notifications", msgSessionId);
|
|
244
|
-
const { payload,
|
|
244
|
+
const { payload, compressedSize } = compressIfNeeded(rawMsg);
|
|
245
245
|
if (compressedSize <= 7500) {
|
|
246
246
|
const escapedPayload = payload.replace(/'/g, "''");
|
|
247
247
|
try {
|
|
@@ -147,7 +147,8 @@ export interface StoredSession {
|
|
|
147
147
|
}
|
|
148
148
|
/**
|
|
149
149
|
* File-based session storage
|
|
150
|
-
* Stores sessions in agents/<agent-name>/.sessions/<session_id
|
|
150
|
+
* Stores sessions in agents/<agent-name>/.sessions/<session_id>/session.json
|
|
151
|
+
* (Legacy: agents/<agent-name>/.sessions/<session_id>.json)
|
|
151
152
|
*/
|
|
152
153
|
export declare class SessionStorage {
|
|
153
154
|
private sessionsDir;
|
|
@@ -159,13 +160,18 @@ export declare class SessionStorage {
|
|
|
159
160
|
*/
|
|
160
161
|
constructor(agentDir: string, agentName: string);
|
|
161
162
|
/**
|
|
162
|
-
* Ensure the
|
|
163
|
+
* Ensure the session directory exists
|
|
163
164
|
*/
|
|
164
|
-
private
|
|
165
|
+
private ensureSessionDir;
|
|
165
166
|
/**
|
|
166
167
|
* Get the file path for a session
|
|
167
168
|
*/
|
|
168
169
|
private getSessionPath;
|
|
170
|
+
/**
|
|
171
|
+
* Get the legacy file path for a session (for backwards compatibility)
|
|
172
|
+
* Legacy format: .sessions/<session_id>.json (flat file, not directory)
|
|
173
|
+
*/
|
|
174
|
+
private getLegacySessionPath;
|
|
169
175
|
/**
|
|
170
176
|
* Save a session to disk
|
|
171
177
|
* Uses atomic write (write to temp file, then rename)
|
|
@@ -177,18 +183,19 @@ export declare class SessionStorage {
|
|
|
177
183
|
loadSession(sessionId: string): Promise<StoredSession | null>;
|
|
178
184
|
/**
|
|
179
185
|
* Synchronous session loading (for internal use)
|
|
186
|
+
* Checks new location first, falls back to legacy location
|
|
180
187
|
*/
|
|
181
188
|
private loadSessionSync;
|
|
182
189
|
/**
|
|
183
|
-
* Check if a session exists
|
|
190
|
+
* Check if a session exists (checks both new and legacy locations)
|
|
184
191
|
*/
|
|
185
192
|
sessionExists(sessionId: string): boolean;
|
|
186
193
|
/**
|
|
187
|
-
* Delete a session
|
|
194
|
+
* Delete a session (deletes entire session directory or legacy file)
|
|
188
195
|
*/
|
|
189
196
|
deleteSession(sessionId: string): Promise<boolean>;
|
|
190
197
|
/**
|
|
191
|
-
* List all session IDs
|
|
198
|
+
* List all session IDs (checks both new and legacy locations)
|
|
192
199
|
*/
|
|
193
200
|
listSessions(): Promise<string[]>;
|
|
194
201
|
/**
|
|
@@ -131,7 +131,8 @@ const storedSessionSchema = z.object({
|
|
|
131
131
|
});
|
|
132
132
|
/**
|
|
133
133
|
* File-based session storage
|
|
134
|
-
* Stores sessions in agents/<agent-name>/.sessions/<session_id
|
|
134
|
+
* Stores sessions in agents/<agent-name>/.sessions/<session_id>/session.json
|
|
135
|
+
* (Legacy: agents/<agent-name>/.sessions/<session_id>.json)
|
|
135
136
|
*/
|
|
136
137
|
export class SessionStorage {
|
|
137
138
|
sessionsDir;
|
|
@@ -146,17 +147,25 @@ export class SessionStorage {
|
|
|
146
147
|
this.agentName = agentName;
|
|
147
148
|
}
|
|
148
149
|
/**
|
|
149
|
-
* Ensure the
|
|
150
|
+
* Ensure the session directory exists
|
|
150
151
|
*/
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
152
|
+
ensureSessionDir(sessionId) {
|
|
153
|
+
const sessionDir = join(this.sessionsDir, sessionId);
|
|
154
|
+
if (!existsSync(sessionDir)) {
|
|
155
|
+
mkdirSync(sessionDir, { recursive: true });
|
|
154
156
|
}
|
|
155
157
|
}
|
|
156
158
|
/**
|
|
157
159
|
* Get the file path for a session
|
|
158
160
|
*/
|
|
159
161
|
getSessionPath(sessionId) {
|
|
162
|
+
return join(this.sessionsDir, sessionId, "session.json");
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Get the legacy file path for a session (for backwards compatibility)
|
|
166
|
+
* Legacy format: .sessions/<session_id>.json (flat file, not directory)
|
|
167
|
+
*/
|
|
168
|
+
getLegacySessionPath(sessionId) {
|
|
160
169
|
return join(this.sessionsDir, `${sessionId}.json`);
|
|
161
170
|
}
|
|
162
171
|
/**
|
|
@@ -164,7 +173,7 @@ export class SessionStorage {
|
|
|
164
173
|
* Uses atomic write (write to temp file, then rename)
|
|
165
174
|
*/
|
|
166
175
|
async saveSession(sessionId, messages, context) {
|
|
167
|
-
this.
|
|
176
|
+
this.ensureSessionDir(sessionId);
|
|
168
177
|
const sessionPath = this.getSessionPath(sessionId);
|
|
169
178
|
const tempPath = `${sessionPath}.tmp`;
|
|
170
179
|
const now = new Date().toISOString();
|
|
@@ -207,14 +216,24 @@ export class SessionStorage {
|
|
|
207
216
|
}
|
|
208
217
|
/**
|
|
209
218
|
* Synchronous session loading (for internal use)
|
|
219
|
+
* Checks new location first, falls back to legacy location
|
|
210
220
|
*/
|
|
211
221
|
loadSessionSync(sessionId) {
|
|
212
222
|
const sessionPath = this.getSessionPath(sessionId);
|
|
223
|
+
const legacyPath = this.getLegacySessionPath(sessionId);
|
|
224
|
+
// Check new location first
|
|
225
|
+
let actualPath = sessionPath;
|
|
213
226
|
if (!existsSync(sessionPath)) {
|
|
214
|
-
|
|
227
|
+
// Fall back to legacy location
|
|
228
|
+
if (existsSync(legacyPath)) {
|
|
229
|
+
actualPath = legacyPath;
|
|
230
|
+
}
|
|
231
|
+
else {
|
|
232
|
+
return null;
|
|
233
|
+
}
|
|
215
234
|
}
|
|
216
235
|
try {
|
|
217
|
-
const content = readFileSync(
|
|
236
|
+
const content = readFileSync(actualPath, "utf-8");
|
|
218
237
|
const parsed = JSON.parse(content);
|
|
219
238
|
// Validate with zod
|
|
220
239
|
const validated = storedSessionSchema.parse(parsed);
|
|
@@ -225,39 +244,64 @@ export class SessionStorage {
|
|
|
225
244
|
}
|
|
226
245
|
}
|
|
227
246
|
/**
|
|
228
|
-
* Check if a session exists
|
|
247
|
+
* Check if a session exists (checks both new and legacy locations)
|
|
229
248
|
*/
|
|
230
249
|
sessionExists(sessionId) {
|
|
231
|
-
return existsSync(this.getSessionPath(sessionId))
|
|
250
|
+
return (existsSync(this.getSessionPath(sessionId)) ||
|
|
251
|
+
existsSync(this.getLegacySessionPath(sessionId)));
|
|
232
252
|
}
|
|
233
253
|
/**
|
|
234
|
-
* Delete a session
|
|
254
|
+
* Delete a session (deletes entire session directory or legacy file)
|
|
235
255
|
*/
|
|
236
256
|
async deleteSession(sessionId) {
|
|
237
|
-
const
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
}
|
|
257
|
+
const sessionDir = join(this.sessionsDir, sessionId);
|
|
258
|
+
const legacyPath = this.getLegacySessionPath(sessionId);
|
|
259
|
+
let deleted = false;
|
|
241
260
|
try {
|
|
242
|
-
|
|
243
|
-
|
|
261
|
+
// Delete session directory (.sessions/<sessionId>/)
|
|
262
|
+
if (existsSync(sessionDir)) {
|
|
263
|
+
// Recursively delete the session directory
|
|
264
|
+
const { rmSync } = await import("node:fs");
|
|
265
|
+
rmSync(sessionDir, { recursive: true, force: true });
|
|
266
|
+
deleted = true;
|
|
267
|
+
}
|
|
268
|
+
// Also delete legacy location if it exists (.sessions/<sessionId>.json)
|
|
269
|
+
if (existsSync(legacyPath)) {
|
|
270
|
+
unlinkSync(legacyPath);
|
|
271
|
+
deleted = true;
|
|
272
|
+
}
|
|
273
|
+
return deleted;
|
|
244
274
|
}
|
|
245
275
|
catch (error) {
|
|
246
276
|
throw new Error(`Failed to delete session ${sessionId}: ${error instanceof Error ? error.message : String(error)}`);
|
|
247
277
|
}
|
|
248
278
|
}
|
|
249
279
|
/**
|
|
250
|
-
* List all session IDs
|
|
280
|
+
* List all session IDs (checks both new and legacy locations)
|
|
251
281
|
*/
|
|
252
282
|
async listSessions() {
|
|
253
|
-
|
|
254
|
-
return [];
|
|
255
|
-
}
|
|
283
|
+
const sessionIds = new Set();
|
|
256
284
|
try {
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
285
|
+
// Check .sessions/ directory
|
|
286
|
+
if (existsSync(this.sessionsDir)) {
|
|
287
|
+
const entries = readdirSync(this.sessionsDir, { withFileTypes: true });
|
|
288
|
+
for (const entry of entries) {
|
|
289
|
+
if (entry.isDirectory()) {
|
|
290
|
+
// New format: .sessions/<sessionId>/session.json
|
|
291
|
+
const sessionJsonPath = join(this.sessionsDir, entry.name, "session.json");
|
|
292
|
+
if (existsSync(sessionJsonPath)) {
|
|
293
|
+
sessionIds.add(entry.name);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
else if (entry.isFile() &&
|
|
297
|
+
entry.name.endsWith(".json") &&
|
|
298
|
+
!entry.name.endsWith(".tmp")) {
|
|
299
|
+
// Legacy format: .sessions/<sessionId>.json
|
|
300
|
+
sessionIds.add(entry.name.replace(".json", ""));
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
return Array.from(sessionIds);
|
|
261
305
|
}
|
|
262
306
|
catch (error) {
|
|
263
307
|
throw new Error(`Failed to list sessions: ${error instanceof Error ? error.message : String(error)}`);
|
|
@@ -271,43 +315,34 @@ export class SessionStorage {
|
|
|
271
315
|
* Returns sessions sorted by updatedAt (most recent first)
|
|
272
316
|
*/
|
|
273
317
|
async listSessionsWithMetadata() {
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
const
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
updatedAt: session.metadata.updatedAt,
|
|
293
|
-
messageCount: session.messages.length,
|
|
294
|
-
};
|
|
295
|
-
if (firstUserText && "text" in firstUserText) {
|
|
296
|
-
entry.firstUserMessage = firstUserText.text.slice(0, 100);
|
|
297
|
-
}
|
|
298
|
-
sessions.push(entry);
|
|
318
|
+
// Get all session IDs from both locations
|
|
319
|
+
const sessionIds = await this.listSessions();
|
|
320
|
+
const sessions = [];
|
|
321
|
+
for (const sessionId of sessionIds) {
|
|
322
|
+
try {
|
|
323
|
+
const session = this.loadSessionSync(sessionId);
|
|
324
|
+
if (session) {
|
|
325
|
+
// Find the first user message for preview
|
|
326
|
+
const firstUserMsg = session.messages.find((m) => m.role === "user");
|
|
327
|
+
const firstUserText = firstUserMsg?.content.find((c) => c.type === "text");
|
|
328
|
+
const entry = {
|
|
329
|
+
sessionId: session.sessionId,
|
|
330
|
+
createdAt: session.metadata.createdAt,
|
|
331
|
+
updatedAt: session.metadata.updatedAt,
|
|
332
|
+
messageCount: session.messages.length,
|
|
333
|
+
};
|
|
334
|
+
if (firstUserText && "text" in firstUserText) {
|
|
335
|
+
entry.firstUserMessage = firstUserText.text.slice(0, 100);
|
|
299
336
|
}
|
|
300
|
-
|
|
301
|
-
catch {
|
|
302
|
-
// Skip invalid sessions
|
|
337
|
+
sessions.push(entry);
|
|
303
338
|
}
|
|
304
339
|
}
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
}
|
|
309
|
-
catch (error) {
|
|
310
|
-
throw new Error(`Failed to list sessions: ${error instanceof Error ? error.message : String(error)}`);
|
|
340
|
+
catch {
|
|
341
|
+
// Skip invalid sessions
|
|
342
|
+
}
|
|
311
343
|
}
|
|
344
|
+
// Sort by updatedAt, most recent first
|
|
345
|
+
sessions.sort((a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime());
|
|
346
|
+
return sessions;
|
|
312
347
|
}
|
|
313
348
|
}
|
|
@@ -9,7 +9,7 @@ export declare const zAgentRunnerParams: z.ZodObject<{
|
|
|
9
9
|
suggestedPrompts: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
10
10
|
systemPrompt: z.ZodNullable<z.ZodString>;
|
|
11
11
|
model: z.ZodString;
|
|
12
|
-
tools: z.ZodOptional<z.ZodArray<z.ZodUnion<readonly [z.ZodUnion<readonly [z.ZodLiteral<"todo_write">, z.ZodLiteral<"get_weather">, z.ZodLiteral<"web_search">, z.ZodLiteral<"town_web_search">, z.ZodLiteral<"filesystem">, z.ZodLiteral<"generate_image">, z.ZodLiteral<"browser">]>, z.ZodObject<{
|
|
12
|
+
tools: z.ZodOptional<z.ZodArray<z.ZodUnion<readonly [z.ZodUnion<readonly [z.ZodLiteral<"artifacts">, z.ZodLiteral<"todo_write">, z.ZodLiteral<"get_weather">, z.ZodLiteral<"web_search">, z.ZodLiteral<"town_web_search">, z.ZodLiteral<"filesystem">, z.ZodLiteral<"generate_image">, z.ZodLiteral<"town_generate_image">, z.ZodLiteral<"browser">]>, z.ZodObject<{
|
|
13
13
|
type: z.ZodLiteral<"custom">;
|
|
14
14
|
modulePath: z.ZodString;
|
|
15
15
|
}, z.core.$strip>, z.ZodObject<{
|
|
@@ -74,6 +74,8 @@ export interface ConfigOverrides {
|
|
|
74
74
|
}
|
|
75
75
|
export type InvokeRequest = Omit<PromptRequest, "_meta"> & {
|
|
76
76
|
messageId: string;
|
|
77
|
+
/** Agent directory path for session-scoped file storage */
|
|
78
|
+
agentDir?: string;
|
|
77
79
|
sessionMeta?: Record<string, unknown>;
|
|
78
80
|
contextMessages?: SessionMessage[];
|
|
79
81
|
configOverrides?: ConfigOverrides;
|
|
@@ -186,7 +186,7 @@ export class HookExecutor {
|
|
|
186
186
|
};
|
|
187
187
|
const result = await callback(hookContext);
|
|
188
188
|
// Extract modified output and warnings from metadata
|
|
189
|
-
if (result.metadata
|
|
189
|
+
if (result.metadata?.modifiedOutput) {
|
|
190
190
|
// Hook took action - emit completed notification
|
|
191
191
|
const response = { notifications };
|
|
192
192
|
response.modifiedOutput = result.metadata.modifiedOutput;
|
|
@@ -11,7 +11,7 @@ export const compactionTool = async (ctx) => {
|
|
|
11
11
|
logger.info("Compaction tool triggered", {
|
|
12
12
|
currentTokens: ctx.currentTokens,
|
|
13
13
|
maxTokens: ctx.maxTokens,
|
|
14
|
-
percentage: ctx.percentage.toFixed(2)
|
|
14
|
+
percentage: `${ctx.percentage.toFixed(2)}%`,
|
|
15
15
|
contextEntries: ctx.session.context.length,
|
|
16
16
|
totalMessages: ctx.session.messages.length,
|
|
17
17
|
model: ctx.model,
|
|
@@ -24,14 +24,36 @@ export const compactionTool = async (ctx) => {
|
|
|
24
24
|
});
|
|
25
25
|
// Build the conversation history to compact
|
|
26
26
|
const messagesToCompact = ctx.session.messages;
|
|
27
|
-
// Convert session messages to text for context
|
|
27
|
+
// Convert session messages to text for context, including tool calls and results
|
|
28
28
|
const conversationText = messagesToCompact
|
|
29
29
|
.map((msg) => {
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
30
|
+
const parts = [];
|
|
31
|
+
for (const block of msg.content) {
|
|
32
|
+
if (block.type === "text") {
|
|
33
|
+
parts.push(block.text);
|
|
34
|
+
}
|
|
35
|
+
else if (block.type === "tool_call") {
|
|
36
|
+
// Include tool call info
|
|
37
|
+
parts.push(`[Tool: ${block.title}]`);
|
|
38
|
+
if (block.rawInput) {
|
|
39
|
+
parts.push(`Input: ${JSON.stringify(block.rawInput, null, 2)}`);
|
|
40
|
+
}
|
|
41
|
+
if (block.rawOutput) {
|
|
42
|
+
// Summarize large outputs to avoid overwhelming the compaction LLM
|
|
43
|
+
const outputStr = JSON.stringify(block.rawOutput);
|
|
44
|
+
if (outputStr.length > 2000) {
|
|
45
|
+
parts.push(`Output: [Large output - ${outputStr.length} chars]`);
|
|
46
|
+
}
|
|
47
|
+
else {
|
|
48
|
+
parts.push(`Output: ${outputStr}`);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
if (block.error) {
|
|
52
|
+
parts.push(`Error: ${block.error}`);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return `${msg.role.toUpperCase()}:\n${parts.join("\n")}`;
|
|
35
57
|
})
|
|
36
58
|
.join("\n\n---\n\n");
|
|
37
59
|
// Create system prompt for compaction
|
|
@@ -85,7 +107,8 @@ Please provide your summary based on the conversation above, following this stru
|
|
|
85
107
|
.join("\n")
|
|
86
108
|
: "Failed to extract summary";
|
|
87
109
|
// Extract token usage from LLM response
|
|
88
|
-
const responseUsage = response
|
|
110
|
+
const responseUsage = response
|
|
111
|
+
.usage_metadata;
|
|
89
112
|
const summaryTokens = responseUsage?.output_tokens ?? 0;
|
|
90
113
|
const inputTokensUsed = responseUsage?.input_tokens ?? ctx.currentTokens;
|
|
91
114
|
logger.info("Generated compaction summary", {
|
|
@@ -89,7 +89,7 @@ export const toolResponseCompactor = async (ctx) => {
|
|
|
89
89
|
// Try more aggressive truncation (70% of target as emergency measure)
|
|
90
90
|
const emergencySize = Math.floor(targetSize * 0.7);
|
|
91
91
|
const emergencyTruncated = truncateToolResponse(rawOutput, emergencySize);
|
|
92
|
-
|
|
92
|
+
const emergencyTokens = countToolResultTokens(emergencyTruncated);
|
|
93
93
|
// Final safety check - if emergency truncation STILL exceeded target, use ultra-conservative fallback
|
|
94
94
|
if (emergencyTokens > targetSize) {
|
|
95
95
|
logger.error("Emergency truncation STILL exceeded target - using ultra-conservative fallback", {
|
|
@@ -151,7 +151,7 @@ export const toolResponseCompactor = async (ctx) => {
|
|
|
151
151
|
.map((msg) => {
|
|
152
152
|
const text = msg.content
|
|
153
153
|
.filter((b) => b.type === "text")
|
|
154
|
-
.map((b) => b.text)
|
|
154
|
+
.map((b) => (b.type === "text" ? b.text : ""))
|
|
155
155
|
.join("\n");
|
|
156
156
|
return `${msg.role}: ${text}`;
|
|
157
157
|
})
|
|
@@ -13,5 +13,6 @@ export declare class LangchainAgent implements AgentRunner {
|
|
|
13
13
|
constructor(params: CreateAgentRunnerParams);
|
|
14
14
|
invoke(req: InvokeRequest): AsyncGenerator<ExtendedSessionUpdate, PromptResponse, undefined>;
|
|
15
15
|
private invokeInternal;
|
|
16
|
+
private invokeWithContext;
|
|
16
17
|
}
|
|
17
18
|
export { makeSubagentsTool } from "./tools/subagent.js";
|