@townco/agent 0.1.88 → 0.1.101
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/acp-server/adapter.d.ts +49 -0
- package/dist/acp-server/adapter.js +693 -5
- package/dist/acp-server/http.d.ts +7 -0
- package/dist/acp-server/http.js +53 -6
- package/dist/definition/index.d.ts +29 -0
- package/dist/definition/index.js +24 -0
- package/dist/runner/agent-runner.d.ts +16 -1
- package/dist/runner/agent-runner.js +2 -1
- package/dist/runner/e2b-sandbox-manager.d.ts +18 -0
- package/dist/runner/e2b-sandbox-manager.js +99 -0
- package/dist/runner/hooks/executor.d.ts +3 -1
- package/dist/runner/hooks/executor.js +21 -1
- package/dist/runner/hooks/predefined/compaction-tool.js +67 -2
- package/dist/runner/hooks/types.d.ts +5 -0
- package/dist/runner/index.d.ts +11 -0
- package/dist/runner/langchain/index.d.ts +10 -0
- package/dist/runner/langchain/index.js +227 -7
- package/dist/runner/langchain/model-factory.js +28 -1
- package/dist/runner/langchain/tools/artifacts.js +6 -3
- package/dist/runner/langchain/tools/e2b.d.ts +54 -0
- package/dist/runner/langchain/tools/e2b.js +360 -0
- package/dist/runner/langchain/tools/filesystem.js +63 -0
- package/dist/runner/langchain/tools/subagent.d.ts +8 -0
- package/dist/runner/langchain/tools/subagent.js +76 -4
- package/dist/runner/langchain/tools/web_search.d.ts +36 -14
- package/dist/runner/langchain/tools/web_search.js +33 -2
- package/dist/runner/session-context.d.ts +20 -0
- package/dist/runner/session-context.js +54 -0
- package/dist/runner/tools.d.ts +2 -2
- package/dist/runner/tools.js +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +8 -7
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
import { Hono } from "hono";
|
|
1
2
|
import type { AgentDefinition } from "../definition";
|
|
2
3
|
import { type AgentRunner } from "../runner";
|
|
4
|
+
export declare function createAcpHttpApp(agent: AgentRunner | AgentDefinition, agentDir?: string, agentName?: string): {
|
|
5
|
+
/** The configured Hono app with all ACP routes */
|
|
6
|
+
app: Hono;
|
|
7
|
+
/** Cleanup function to stop the app and release resources */
|
|
8
|
+
stop: () => Promise<void>;
|
|
9
|
+
};
|
|
3
10
|
export declare function makeHttpTransport(agent: AgentRunner | AgentDefinition, agentDir?: string, agentName?: string): void;
|
package/dist/acp-server/http.js
CHANGED
|
@@ -43,17 +43,17 @@ function compressIfNeeded(rawMsg) {
|
|
|
43
43
|
compressedSize: envelope.length,
|
|
44
44
|
};
|
|
45
45
|
}
|
|
46
|
-
// Use PGlite in-memory database for LISTEN/NOTIFY
|
|
47
|
-
const pg = new PGlite();
|
|
48
|
-
// Store for oversized responses that can't go through PostgreSQL NOTIFY
|
|
49
|
-
// Key: request ID, Value: response object
|
|
50
|
-
const oversizedResponses = new Map();
|
|
51
46
|
// Helper to create safe channel names from untrusted IDs
|
|
52
47
|
function safeChannelName(prefix, id) {
|
|
53
48
|
const hash = createHash("sha256").update(id).digest("hex").slice(0, 16);
|
|
54
49
|
return `${prefix}_${hash}`;
|
|
55
50
|
}
|
|
56
|
-
export function
|
|
51
|
+
export function createAcpHttpApp(agent, agentDir, agentName) {
|
|
52
|
+
// Use PGlite in-memory database for LISTEN/NOTIFY
|
|
53
|
+
const pg = new PGlite();
|
|
54
|
+
// Store for oversized responses that can't go through PostgreSQL NOTIFY
|
|
55
|
+
// Key: request ID, Value: response object
|
|
56
|
+
const oversizedResponses = new Map();
|
|
57
57
|
// Configure logger to write to .logs/ directory
|
|
58
58
|
// Use TOWN_LOGS_DIR env var if set (for subagents), otherwise use agentDir
|
|
59
59
|
const logsDir = process.env.TOWN_LOGS_DIR ||
|
|
@@ -648,6 +648,40 @@ export function makeHttpTransport(agent, agentDir, agentName) {
|
|
|
648
648
|
await stream.sleep(1000 * 60 * 60 * 24);
|
|
649
649
|
});
|
|
650
650
|
});
|
|
651
|
+
// Edit and resend a message from a specific point
|
|
652
|
+
app.post("/sessions/:sessionId/edit-and-resend", async (c) => {
|
|
653
|
+
const sessionId = c.req.param("sessionId");
|
|
654
|
+
if (!sessionId) {
|
|
655
|
+
return c.json({ error: "Session ID required" }, 400);
|
|
656
|
+
}
|
|
657
|
+
const body = await c.req.json();
|
|
658
|
+
const { messageIndex, prompt } = body;
|
|
659
|
+
if (typeof messageIndex !== "number" || messageIndex < 0) {
|
|
660
|
+
return c.json({ error: "Valid messageIndex required" }, 400);
|
|
661
|
+
}
|
|
662
|
+
if (!Array.isArray(prompt) || prompt.length === 0) {
|
|
663
|
+
return c.json({ error: "prompt array required" }, 400);
|
|
664
|
+
}
|
|
665
|
+
logger.info("Edit and resend request", { sessionId, messageIndex });
|
|
666
|
+
// Get or create session ACP infrastructure
|
|
667
|
+
const sessionAcp = getOrCreateSessionAcp(sessionId);
|
|
668
|
+
try {
|
|
669
|
+
// Call editAndResend on the adapter directly
|
|
670
|
+
// Cast prompt to the ACP prompt type - it's validated by the adapter
|
|
671
|
+
const response = await sessionAcp.adapter.editAndResend(sessionId, messageIndex, prompt);
|
|
672
|
+
return c.json({ result: response });
|
|
673
|
+
}
|
|
674
|
+
catch (error) {
|
|
675
|
+
logger.error("Edit and resend failed", {
|
|
676
|
+
error: error instanceof Error ? error.message : String(error),
|
|
677
|
+
sessionId,
|
|
678
|
+
messageIndex,
|
|
679
|
+
});
|
|
680
|
+
return c.json({
|
|
681
|
+
error: error instanceof Error ? error.message : String(error),
|
|
682
|
+
}, 500);
|
|
683
|
+
}
|
|
684
|
+
});
|
|
651
685
|
app.post("/rpc", async (c) => {
|
|
652
686
|
// Get and validate the request body
|
|
653
687
|
const rawBody = await c.req.json();
|
|
@@ -686,6 +720,7 @@ export function makeHttpTransport(agent, agentDir, agentName) {
|
|
|
686
720
|
"session/prompt",
|
|
687
721
|
"session/load",
|
|
688
722
|
"session/stop",
|
|
723
|
+
"session/cancel",
|
|
689
724
|
];
|
|
690
725
|
const isSessionMethod = sessionMethods.includes(method);
|
|
691
726
|
// Extract sessionId from params for existing session operations
|
|
@@ -848,6 +883,18 @@ export function makeHttpTransport(agent, agentDir, agentName) {
|
|
|
848
883
|
});
|
|
849
884
|
}
|
|
850
885
|
});
|
|
886
|
+
// Cleanup function to stop the app and release resources
|
|
887
|
+
const stop = async () => {
|
|
888
|
+
await pg.close();
|
|
889
|
+
sessionAcpMap.clear();
|
|
890
|
+
sseStreams.clear();
|
|
891
|
+
initialMessageSentSessions.clear();
|
|
892
|
+
oversizedResponses.clear();
|
|
893
|
+
};
|
|
894
|
+
return { app, stop };
|
|
895
|
+
}
|
|
896
|
+
export function makeHttpTransport(agent, agentDir, agentName) {
|
|
897
|
+
const { app } = createAcpHttpApp(agent, agentDir, agentName);
|
|
851
898
|
const port = Number.parseInt(process.env.PORT || "3100", 10);
|
|
852
899
|
logger.info("Starting HTTP server", { port });
|
|
853
900
|
const hostname = Bun.env.BIND_HOST || "localhost";
|
|
@@ -34,6 +34,24 @@ export declare const InitialMessageSchema: z.ZodObject<{
|
|
|
34
34
|
export declare const UiConfigSchema: z.ZodObject<{
|
|
35
35
|
hideTopBar: z.ZodOptional<z.ZodBoolean>;
|
|
36
36
|
}, z.core.$strip>;
|
|
37
|
+
/** Schema for a single option within a prompt parameter. */
|
|
38
|
+
export declare const PromptParameterOptionSchema: z.ZodObject<{
|
|
39
|
+
id: z.ZodString;
|
|
40
|
+
label: z.ZodString;
|
|
41
|
+
systemPromptAddendum: z.ZodOptional<z.ZodString>;
|
|
42
|
+
}, z.core.$strip>;
|
|
43
|
+
/** Schema for a configurable prompt parameter that users can select per-message. */
|
|
44
|
+
export declare const PromptParameterSchema: z.ZodObject<{
|
|
45
|
+
id: z.ZodString;
|
|
46
|
+
label: z.ZodString;
|
|
47
|
+
description: z.ZodOptional<z.ZodString>;
|
|
48
|
+
options: z.ZodArray<z.ZodObject<{
|
|
49
|
+
id: z.ZodString;
|
|
50
|
+
label: z.ZodString;
|
|
51
|
+
systemPromptAddendum: z.ZodOptional<z.ZodString>;
|
|
52
|
+
}, z.core.$strip>>;
|
|
53
|
+
defaultOptionId: z.ZodOptional<z.ZodString>;
|
|
54
|
+
}, z.core.$strip>;
|
|
37
55
|
/** Agent definition schema. */
|
|
38
56
|
export declare const AgentDefinitionSchema: z.ZodObject<{
|
|
39
57
|
displayName: z.ZodOptional<z.ZodString>;
|
|
@@ -88,4 +106,15 @@ export declare const AgentDefinitionSchema: z.ZodObject<{
|
|
|
88
106
|
uiConfig: z.ZodOptional<z.ZodObject<{
|
|
89
107
|
hideTopBar: z.ZodOptional<z.ZodBoolean>;
|
|
90
108
|
}, z.core.$strip>>;
|
|
109
|
+
promptParameters: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
110
|
+
id: z.ZodString;
|
|
111
|
+
label: z.ZodString;
|
|
112
|
+
description: z.ZodOptional<z.ZodString>;
|
|
113
|
+
options: z.ZodArray<z.ZodObject<{
|
|
114
|
+
id: z.ZodString;
|
|
115
|
+
label: z.ZodString;
|
|
116
|
+
systemPromptAddendum: z.ZodOptional<z.ZodString>;
|
|
117
|
+
}, z.core.$strip>>;
|
|
118
|
+
defaultOptionId: z.ZodOptional<z.ZodString>;
|
|
119
|
+
}, z.core.$strip>>>;
|
|
91
120
|
}, z.core.$strip>;
|
package/dist/definition/index.js
CHANGED
|
@@ -82,6 +82,28 @@ export const UiConfigSchema = z.object({
|
|
|
82
82
|
/** Whether to hide the top bar (session switcher, debugger link, settings). Useful for embedded/deployed mode. */
|
|
83
83
|
hideTopBar: z.boolean().optional(),
|
|
84
84
|
});
|
|
85
|
+
/** Schema for a single option within a prompt parameter. */
|
|
86
|
+
export const PromptParameterOptionSchema = z.object({
|
|
87
|
+
/** Unique identifier for this option. */
|
|
88
|
+
id: z.string(),
|
|
89
|
+
/** Human-readable label shown in the UI. */
|
|
90
|
+
label: z.string(),
|
|
91
|
+
/** Instructions appended to the system prompt when this option is selected. */
|
|
92
|
+
systemPromptAddendum: z.string().optional(),
|
|
93
|
+
});
|
|
94
|
+
/** Schema for a configurable prompt parameter that users can select per-message. */
|
|
95
|
+
export const PromptParameterSchema = z.object({
|
|
96
|
+
/** Unique identifier for this parameter. */
|
|
97
|
+
id: z.string(),
|
|
98
|
+
/** Human-readable label shown in the UI dropdown. */
|
|
99
|
+
label: z.string(),
|
|
100
|
+
/** Optional description explaining what this parameter controls. */
|
|
101
|
+
description: z.string().optional(),
|
|
102
|
+
/** Available options for this parameter. */
|
|
103
|
+
options: z.array(PromptParameterOptionSchema),
|
|
104
|
+
/** The default option ID to use if none is selected. */
|
|
105
|
+
defaultOptionId: z.string().optional(),
|
|
106
|
+
});
|
|
85
107
|
/** Agent definition schema. */
|
|
86
108
|
export const AgentDefinitionSchema = z.object({
|
|
87
109
|
/** Human-readable display name for the agent (shown in UI). */
|
|
@@ -99,4 +121,6 @@ export const AgentDefinitionSchema = z.object({
|
|
|
99
121
|
initialMessage: InitialMessageSchema.optional(),
|
|
100
122
|
/** UI configuration for controlling the chat interface appearance. */
|
|
101
123
|
uiConfig: UiConfigSchema.optional(),
|
|
124
|
+
/** Configurable parameters that users can select per-message to influence agent behavior. */
|
|
125
|
+
promptParameters: z.array(PromptParameterSchema).optional(),
|
|
102
126
|
});
|
|
@@ -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<"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.ZodLiteral<"document_extract">]>, 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.ZodLiteral<"document_extract">, z.ZodLiteral<"town_e2b">]>, z.ZodObject<{
|
|
13
13
|
type: z.ZodLiteral<"custom">;
|
|
14
14
|
modulePath: z.ZodString;
|
|
15
15
|
}, z.core.$strip>, z.ZodObject<{
|
|
@@ -59,6 +59,17 @@ export declare const zAgentRunnerParams: z.ZodObject<{
|
|
|
59
59
|
uiConfig: z.ZodOptional<z.ZodObject<{
|
|
60
60
|
hideTopBar: z.ZodOptional<z.ZodBoolean>;
|
|
61
61
|
}, z.core.$strip>>;
|
|
62
|
+
promptParameters: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
63
|
+
id: z.ZodString;
|
|
64
|
+
label: z.ZodString;
|
|
65
|
+
description: z.ZodOptional<z.ZodString>;
|
|
66
|
+
options: z.ZodArray<z.ZodObject<{
|
|
67
|
+
id: z.ZodString;
|
|
68
|
+
label: z.ZodString;
|
|
69
|
+
systemPromptAddendum: z.ZodOptional<z.ZodString>;
|
|
70
|
+
}, z.core.$strip>>;
|
|
71
|
+
defaultOptionId: z.ZodOptional<z.ZodString>;
|
|
72
|
+
}, z.core.$strip>>>;
|
|
62
73
|
}, z.core.$strip>;
|
|
63
74
|
export type CreateAgentRunnerParams = z.infer<typeof zAgentRunnerParams>;
|
|
64
75
|
export interface SessionMessage {
|
|
@@ -78,6 +89,10 @@ export type InvokeRequest = Omit<PromptRequest, "_meta"> & {
|
|
|
78
89
|
sessionMeta?: Record<string, unknown>;
|
|
79
90
|
contextMessages?: SessionMessage[];
|
|
80
91
|
configOverrides?: ConfigOverrides;
|
|
92
|
+
/** Abort signal for cancellation - tools can listen for this to stop early */
|
|
93
|
+
abortSignal?: AbortSignal;
|
|
94
|
+
/** Selected prompt parameters for this message. Maps parameter ID to selected option ID. */
|
|
95
|
+
promptParameters?: Record<string, string>;
|
|
81
96
|
};
|
|
82
97
|
export interface TokenUsage {
|
|
83
98
|
inputTokens?: number;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import { HookConfigSchema, InitialMessageSchema, McpConfigSchema, UiConfigSchema, } from "../definition";
|
|
2
|
+
import { HookConfigSchema, InitialMessageSchema, McpConfigSchema, PromptParameterSchema, UiConfigSchema, } from "../definition";
|
|
3
3
|
import { zToolType } from "./tools";
|
|
4
4
|
export const zAgentRunnerParams = z.object({
|
|
5
5
|
displayName: z.string().optional(),
|
|
@@ -13,4 +13,5 @@ export const zAgentRunnerParams = z.object({
|
|
|
13
13
|
hooks: z.array(HookConfigSchema).optional(),
|
|
14
14
|
initialMessage: InitialMessageSchema.optional(),
|
|
15
15
|
uiConfig: UiConfigSchema.optional(),
|
|
16
|
+
promptParameters: z.array(PromptParameterSchema).optional(),
|
|
16
17
|
});
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { Sandbox } from "@e2b/code-interpreter";
|
|
2
|
+
/**
|
|
3
|
+
* Get or create an E2B sandbox for the current session.
|
|
4
|
+
* Sandboxes are session-scoped and reused across tool calls.
|
|
5
|
+
*/
|
|
6
|
+
export declare function getSessionSandbox(apiKey: string): Promise<Sandbox>;
|
|
7
|
+
/**
|
|
8
|
+
* Explicitly destroy a session's sandbox (called on session end).
|
|
9
|
+
*/
|
|
10
|
+
export declare function destroySessionSandbox(sessionId: string): Promise<void>;
|
|
11
|
+
/**
|
|
12
|
+
* Check if a session has an active sandbox.
|
|
13
|
+
*/
|
|
14
|
+
export declare function hasSessionSandbox(sessionId: string): boolean;
|
|
15
|
+
/**
|
|
16
|
+
* Get the number of active sandboxes (for debugging/monitoring).
|
|
17
|
+
*/
|
|
18
|
+
export declare function getActiveSandboxCount(): number;
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { createLogger } from "../logger.js";
|
|
2
|
+
import { getSessionContext, hasSessionContext } from "./session-context";
|
|
3
|
+
const logger = createLogger("e2b-sandbox-manager");
|
|
4
|
+
// Map sessionId -> Sandbox instance
|
|
5
|
+
const sessionSandboxes = new Map();
|
|
6
|
+
// Map sessionId -> last activity timestamp (for timeout-based cleanup)
|
|
7
|
+
const sandboxActivity = new Map();
|
|
8
|
+
// Map sessionId -> cleanup timeout handle
|
|
9
|
+
const cleanupTimeouts = new Map();
|
|
10
|
+
// Sandbox timeout in milliseconds (default: 15 minutes)
|
|
11
|
+
const SANDBOX_TIMEOUT_MS = 15 * 60 * 1000;
|
|
12
|
+
/**
|
|
13
|
+
* Get or create an E2B sandbox for the current session.
|
|
14
|
+
* Sandboxes are session-scoped and reused across tool calls.
|
|
15
|
+
*/
|
|
16
|
+
export async function getSessionSandbox(apiKey) {
|
|
17
|
+
if (!hasSessionContext()) {
|
|
18
|
+
throw new Error("E2B tools require session context");
|
|
19
|
+
}
|
|
20
|
+
const { sessionId } = getSessionContext();
|
|
21
|
+
// Check for existing sandbox
|
|
22
|
+
let sandbox = sessionSandboxes.get(sessionId);
|
|
23
|
+
if (sandbox) {
|
|
24
|
+
// Update activity timestamp and reschedule cleanup
|
|
25
|
+
sandboxActivity.set(sessionId, Date.now());
|
|
26
|
+
rescheduleCleanup(sessionId);
|
|
27
|
+
logger.debug("Reusing existing sandbox", { sessionId });
|
|
28
|
+
return sandbox;
|
|
29
|
+
}
|
|
30
|
+
// Create new sandbox - dynamic import to avoid loading if not used
|
|
31
|
+
logger.info("Creating new E2B sandbox", { sessionId });
|
|
32
|
+
const { Sandbox: SandboxClass } = await import("@e2b/code-interpreter");
|
|
33
|
+
sandbox = await SandboxClass.create({ apiKey });
|
|
34
|
+
sessionSandboxes.set(sessionId, sandbox);
|
|
35
|
+
sandboxActivity.set(sessionId, Date.now());
|
|
36
|
+
// Set up auto-cleanup timeout
|
|
37
|
+
scheduleCleanup(sessionId);
|
|
38
|
+
return sandbox;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Explicitly destroy a session's sandbox (called on session end).
|
|
42
|
+
*/
|
|
43
|
+
export async function destroySessionSandbox(sessionId) {
|
|
44
|
+
const sandbox = sessionSandboxes.get(sessionId);
|
|
45
|
+
if (sandbox) {
|
|
46
|
+
logger.info("Destroying sandbox", { sessionId });
|
|
47
|
+
try {
|
|
48
|
+
await sandbox.kill();
|
|
49
|
+
}
|
|
50
|
+
catch (error) {
|
|
51
|
+
logger.error("Error killing sandbox", { sessionId, error });
|
|
52
|
+
}
|
|
53
|
+
sessionSandboxes.delete(sessionId);
|
|
54
|
+
sandboxActivity.delete(sessionId);
|
|
55
|
+
// Clear any pending cleanup timeout
|
|
56
|
+
const timeout = cleanupTimeouts.get(sessionId);
|
|
57
|
+
if (timeout) {
|
|
58
|
+
clearTimeout(timeout);
|
|
59
|
+
cleanupTimeouts.delete(sessionId);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Schedule sandbox cleanup after inactivity timeout.
|
|
65
|
+
*/
|
|
66
|
+
function scheduleCleanup(sessionId) {
|
|
67
|
+
const timeout = setTimeout(async () => {
|
|
68
|
+
const lastActivity = sandboxActivity.get(sessionId);
|
|
69
|
+
if (lastActivity && Date.now() - lastActivity >= SANDBOX_TIMEOUT_MS) {
|
|
70
|
+
await destroySessionSandbox(sessionId);
|
|
71
|
+
}
|
|
72
|
+
cleanupTimeouts.delete(sessionId);
|
|
73
|
+
}, SANDBOX_TIMEOUT_MS);
|
|
74
|
+
cleanupTimeouts.set(sessionId, timeout);
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Reschedule cleanup when sandbox is accessed.
|
|
78
|
+
*/
|
|
79
|
+
function rescheduleCleanup(sessionId) {
|
|
80
|
+
// Clear existing timeout
|
|
81
|
+
const existingTimeout = cleanupTimeouts.get(sessionId);
|
|
82
|
+
if (existingTimeout) {
|
|
83
|
+
clearTimeout(existingTimeout);
|
|
84
|
+
}
|
|
85
|
+
// Schedule new timeout
|
|
86
|
+
scheduleCleanup(sessionId);
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Check if a session has an active sandbox.
|
|
90
|
+
*/
|
|
91
|
+
export function hasSessionSandbox(sessionId) {
|
|
92
|
+
return sessionSandboxes.has(sessionId);
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Get the number of active sandboxes (for debugging/monitoring).
|
|
96
|
+
*/
|
|
97
|
+
export function getActiveSandboxCount() {
|
|
98
|
+
return sessionSandboxes.size;
|
|
99
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ContextEntry } from "../../acp-server/session-storage";
|
|
2
|
+
import type { AgentDefinition } from "../../definition";
|
|
2
3
|
import type { HookCallback, HookConfig, HookNotification, HookStorageInterface, ReadonlySession } from "./types";
|
|
3
4
|
/**
|
|
4
5
|
* Callback for streaming hook notifications in real-time
|
|
@@ -12,9 +13,10 @@ export declare class HookExecutor {
|
|
|
12
13
|
private model;
|
|
13
14
|
private loadCallback;
|
|
14
15
|
private onNotification;
|
|
16
|
+
private agentDefinition;
|
|
15
17
|
private storage;
|
|
16
18
|
private sessionId;
|
|
17
|
-
constructor(hooks: HookConfig[], model: string, loadCallback: (callbackRef: string) => Promise<HookCallback>, onNotification?: OnHookNotification, storage?: HookStorageInterface, sessionId?: string);
|
|
19
|
+
constructor(hooks: HookConfig[], model: string, loadCallback: (callbackRef: string) => Promise<HookCallback>, onNotification?: OnHookNotification, agentDefinition?: Readonly<AgentDefinition>, storage?: HookStorageInterface, sessionId?: string);
|
|
18
20
|
/**
|
|
19
21
|
* Emit a notification - sends immediately if callback provided, otherwise collects for batch return
|
|
20
22
|
*/
|
|
@@ -9,13 +9,15 @@ export class HookExecutor {
|
|
|
9
9
|
model;
|
|
10
10
|
loadCallback;
|
|
11
11
|
onNotification;
|
|
12
|
+
agentDefinition;
|
|
12
13
|
storage;
|
|
13
14
|
sessionId;
|
|
14
|
-
constructor(hooks, model, loadCallback, onNotification, storage, sessionId) {
|
|
15
|
+
constructor(hooks, model, loadCallback, onNotification, agentDefinition, storage, sessionId) {
|
|
15
16
|
this.hooks = hooks;
|
|
16
17
|
this.model = model;
|
|
17
18
|
this.loadCallback = loadCallback;
|
|
18
19
|
this.onNotification = onNotification;
|
|
20
|
+
this.agentDefinition = agentDefinition ?? { model, systemPrompt: null };
|
|
19
21
|
this.storage = storage;
|
|
20
22
|
this.sessionId = sessionId;
|
|
21
23
|
}
|
|
@@ -81,17 +83,29 @@ export class HookExecutor {
|
|
|
81
83
|
}, notifications);
|
|
82
84
|
try {
|
|
83
85
|
// Load and execute callback
|
|
86
|
+
logger.info("Loading context_size hook callback", {
|
|
87
|
+
callback: hook.callback,
|
|
88
|
+
});
|
|
84
89
|
const callback = await this.loadCallback(hook.callback);
|
|
90
|
+
logger.info("Loaded context_size hook callback, executing...", {
|
|
91
|
+
callback: hook.callback,
|
|
92
|
+
});
|
|
85
93
|
const hookContext = {
|
|
86
94
|
session,
|
|
87
95
|
currentTokens: actualInputTokens,
|
|
88
96
|
maxTokens,
|
|
89
97
|
percentage,
|
|
90
98
|
model: this.model,
|
|
99
|
+
agent: this.agentDefinition,
|
|
91
100
|
sessionId: this.sessionId,
|
|
92
101
|
storage: this.storage,
|
|
93
102
|
};
|
|
94
103
|
const result = await callback(hookContext);
|
|
104
|
+
logger.info("Context_size hook callback completed", {
|
|
105
|
+
callback: hook.callback,
|
|
106
|
+
hasNewContextEntry: !!result.newContextEntry,
|
|
107
|
+
metadata: result.metadata,
|
|
108
|
+
});
|
|
95
109
|
// Notify completion
|
|
96
110
|
this.emitNotification({
|
|
97
111
|
type: "hook_completed",
|
|
@@ -106,6 +120,11 @@ export class HookExecutor {
|
|
|
106
120
|
};
|
|
107
121
|
}
|
|
108
122
|
catch (error) {
|
|
123
|
+
logger.error("Context_size hook callback failed", {
|
|
124
|
+
callback: hook.callback,
|
|
125
|
+
error: error instanceof Error ? error.message : String(error),
|
|
126
|
+
stack: error instanceof Error ? error.stack : undefined,
|
|
127
|
+
});
|
|
109
128
|
// Notify error
|
|
110
129
|
this.emitNotification({
|
|
111
130
|
type: "hook_error",
|
|
@@ -195,6 +214,7 @@ export class HookExecutor {
|
|
|
195
214
|
maxTokens,
|
|
196
215
|
percentage,
|
|
197
216
|
model: this.model,
|
|
217
|
+
agent: this.agentDefinition,
|
|
198
218
|
sessionId: this.sessionId,
|
|
199
219
|
storage: this.storage,
|
|
200
220
|
toolResponse,
|
|
@@ -16,6 +16,8 @@ export const compactionTool = async (ctx) => {
|
|
|
16
16
|
totalMessages: ctx.session.messages.length,
|
|
17
17
|
model: ctx.model,
|
|
18
18
|
});
|
|
19
|
+
// Check if "library" MCP is connected - only then do we extend prompt for knowledge graph analysis
|
|
20
|
+
const hasLibraryMcp = ctx.agent.mcps?.some((mcp) => typeof mcp === "string" ? mcp === "library" : mcp.name === "library");
|
|
19
21
|
try {
|
|
20
22
|
// Create the LLM client using the same model as the agent
|
|
21
23
|
const model = new ChatAnthropic({
|
|
@@ -59,7 +61,8 @@ export const compactionTool = async (ctx) => {
|
|
|
59
61
|
// Create system prompt for compaction
|
|
60
62
|
const systemPrompt = new SystemMessage("You are a helpful AI assistant tasked with summarizing conversations.");
|
|
61
63
|
// Create detailed compaction instructions with a generic, domain-agnostic approach
|
|
62
|
-
|
|
64
|
+
// Base prompt with sections 1-9
|
|
65
|
+
let userPrompt = `Your task is to create a detailed summary of the conversation so far, paying close attention to the user's explicit requests and your previous actions.
|
|
63
66
|
This summary should be thorough in capturing important details, decisions, and context that would be essential for continuing the conversation/task without losing important information.
|
|
64
67
|
|
|
65
68
|
Before providing your final summary, wrap your analysis in <analysis> tags to organize your thoughts and ensure you've covered all necessary points. In your analysis process:
|
|
@@ -84,7 +87,23 @@ Your summary should include the following sections:
|
|
|
84
87
|
7. Pending Tasks: Outline any pending tasks that you have explicitly been asked to work on
|
|
85
88
|
8. Current Work: Describe in detail precisely what was being worked on immediately before this summary, paying special attention to the most recent messages from both user and assistant
|
|
86
89
|
9. Optional Next Step: List the next step related to the most recent work. IMPORTANT: ensure this step is DIRECTLY in line with the user's most recent explicit requests and the task you were working on. If the last task was concluded, only list next steps if they are explicitly in line with the user's request. Do not start on tangential requests or old requests that were already completed without confirming with the user first.
|
|
87
|
-
If there is a next step, include direct quotes from the most recent conversation showing exactly what task you were working on and where you left off. This should be verbatim to ensure there's no drift in task interpretation
|
|
90
|
+
If there is a next step, include direct quotes from the most recent conversation showing exactly what task you were working on and where you left off. This should be verbatim to ensure there's no drift in task interpretation.`;
|
|
91
|
+
// Add section 10 only if "library" MCP is connected
|
|
92
|
+
if (hasLibraryMcp) {
|
|
93
|
+
logger.info("Adding knowledge graph analysis section to prompt (library MCP detected)");
|
|
94
|
+
userPrompt += `
|
|
95
|
+
10. Knowledge Graph Analysis Decision: Based on the conversation content, determine if this summary contains information valuable for knowledge graph updates. Output your decision as:
|
|
96
|
+
<knowledge_graph_analysis_needed>yes</knowledge_graph_analysis_needed>
|
|
97
|
+
or
|
|
98
|
+
<knowledge_graph_analysis_needed>no</knowledge_graph_analysis_needed>
|
|
99
|
+
|
|
100
|
+
Consider answering "yes" if:
|
|
101
|
+
- New entities, relationships, or facts were discussed
|
|
102
|
+
- User preferences or patterns were revealed
|
|
103
|
+
- Domain-specific knowledge was shared or created
|
|
104
|
+
- Important decisions or conclusions were reached`;
|
|
105
|
+
}
|
|
106
|
+
userPrompt += `
|
|
88
107
|
|
|
89
108
|
Here's the conversation to summarize:
|
|
90
109
|
|
|
@@ -131,6 +150,51 @@ Please provide your summary based on the conversation above, following this stru
|
|
|
131
150
|
toolResultsTokens: 0,
|
|
132
151
|
totalEstimated: summaryTokens,
|
|
133
152
|
});
|
|
153
|
+
// Parse knowledge graph decision and dispatch only if "library" MCP is connected
|
|
154
|
+
let needsKnowledgeGraphAnalysis = false;
|
|
155
|
+
if (hasLibraryMcp) {
|
|
156
|
+
const kgMatch = summaryText.match(/<knowledge_graph_analysis_needed>(yes|no)<\/knowledge_graph_analysis_needed>/i);
|
|
157
|
+
needsKnowledgeGraphAnalysis = kgMatch?.[1]?.toLowerCase() === "yes";
|
|
158
|
+
logger.info(`Knowledge graph analysis decision: needsAnalysis=${needsKnowledgeGraphAnalysis}, matchFound=${!!kgMatch}`);
|
|
159
|
+
if (needsKnowledgeGraphAnalysis) {
|
|
160
|
+
const apiUrl = process.env.LIBRARY_API_URL;
|
|
161
|
+
const apiKey = process.env.LIBRARY_ROOT_API_KEY;
|
|
162
|
+
logger.info("Attempting to dispatch to Library API", {
|
|
163
|
+
hasApiUrl: !!apiUrl,
|
|
164
|
+
hasApiKey: !!apiKey,
|
|
165
|
+
});
|
|
166
|
+
if (apiUrl && apiKey) {
|
|
167
|
+
const serviceUrl = `${apiUrl}/compaction_analysis`;
|
|
168
|
+
// Fire-and-forget - don't block compaction on external service
|
|
169
|
+
fetch(serviceUrl, {
|
|
170
|
+
method: "POST",
|
|
171
|
+
headers: {
|
|
172
|
+
"Content-Type": "application/json",
|
|
173
|
+
Authorization: `Bearer ${apiKey}`,
|
|
174
|
+
},
|
|
175
|
+
body: JSON.stringify({
|
|
176
|
+
conversation_text: conversationText,
|
|
177
|
+
timestamp: Date.now(),
|
|
178
|
+
}),
|
|
179
|
+
})
|
|
180
|
+
.then((response) => {
|
|
181
|
+
logger.info("Library API response received", {
|
|
182
|
+
status: response.status,
|
|
183
|
+
ok: response.ok,
|
|
184
|
+
});
|
|
185
|
+
})
|
|
186
|
+
.catch((error) => logger.error("Failed to send compaction for analysis", {
|
|
187
|
+
error: error instanceof Error ? error.message : String(error),
|
|
188
|
+
}));
|
|
189
|
+
}
|
|
190
|
+
else {
|
|
191
|
+
logger.warn("Skipping Library API dispatch - missing config", {
|
|
192
|
+
hasApiUrl: !!apiUrl,
|
|
193
|
+
hasApiKey: !!apiKey,
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
134
198
|
return {
|
|
135
199
|
newContextEntry,
|
|
136
200
|
metadata: {
|
|
@@ -140,6 +204,7 @@ Please provide your summary based on the conversation above, following this stru
|
|
|
140
204
|
tokensSaved: inputTokensUsed - summaryTokens,
|
|
141
205
|
summaryTokens, // Token count of the summary itself
|
|
142
206
|
summaryGenerated: true,
|
|
207
|
+
...(hasLibraryMcp && { needsKnowledgeGraphAnalysis }),
|
|
143
208
|
},
|
|
144
209
|
};
|
|
145
210
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ContextEntry } from "../../acp-server/session-storage";
|
|
2
|
+
import type { AgentDefinition } from "../../definition";
|
|
2
3
|
import type { SessionMessage } from "../agent-runner";
|
|
3
4
|
/**
|
|
4
5
|
* Storage interface for hooks that need to persist data
|
|
@@ -89,6 +90,10 @@ export interface HookContext {
|
|
|
89
90
|
* The model being used
|
|
90
91
|
*/
|
|
91
92
|
model: string;
|
|
93
|
+
/**
|
|
94
|
+
* Full agent definition
|
|
95
|
+
*/
|
|
96
|
+
agent: Readonly<AgentDefinition>;
|
|
92
97
|
/**
|
|
93
98
|
* Session ID for the current session
|
|
94
99
|
*/
|
package/dist/runner/index.d.ts
CHANGED
|
@@ -50,4 +50,15 @@ export declare const makeRunnerFromDefinition: (definition: {
|
|
|
50
50
|
uiConfig?: {
|
|
51
51
|
hideTopBar?: boolean | undefined;
|
|
52
52
|
} | undefined;
|
|
53
|
+
promptParameters?: {
|
|
54
|
+
id: string;
|
|
55
|
+
label: string;
|
|
56
|
+
description?: string | undefined;
|
|
57
|
+
options: {
|
|
58
|
+
id: string;
|
|
59
|
+
label: string;
|
|
60
|
+
systemPromptAddendum?: string | undefined;
|
|
61
|
+
}[];
|
|
62
|
+
defaultOptionId?: string | undefined;
|
|
63
|
+
}[] | undefined;
|
|
53
64
|
}) => AgentRunner;
|
|
@@ -16,3 +16,13 @@ export declare class LangchainAgent implements AgentRunner {
|
|
|
16
16
|
private invokeWithContext;
|
|
17
17
|
}
|
|
18
18
|
export { makeSubagentsTool } from "./tools/subagent.js";
|
|
19
|
+
/**
|
|
20
|
+
* Get metadata for children tools of a built-in tool group.
|
|
21
|
+
* This dynamically loads the tool factory and extracts metadata from each tool.
|
|
22
|
+
* Returns undefined if the tool is not a group (returns single tool or not found).
|
|
23
|
+
*/
|
|
24
|
+
export declare function getToolGroupChildren(toolName: string): Array<{
|
|
25
|
+
name: string;
|
|
26
|
+
prettyName: string;
|
|
27
|
+
icon?: string;
|
|
28
|
+
}> | undefined;
|