@townco/agent 0.1.88 → 0.1.98

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.
Files changed (32) hide show
  1. package/dist/acp-server/adapter.d.ts +49 -0
  2. package/dist/acp-server/adapter.js +693 -5
  3. package/dist/acp-server/http.d.ts +7 -0
  4. package/dist/acp-server/http.js +53 -6
  5. package/dist/definition/index.d.ts +29 -0
  6. package/dist/definition/index.js +24 -0
  7. package/dist/runner/agent-runner.d.ts +16 -1
  8. package/dist/runner/agent-runner.js +2 -1
  9. package/dist/runner/e2b-sandbox-manager.d.ts +18 -0
  10. package/dist/runner/e2b-sandbox-manager.js +99 -0
  11. package/dist/runner/hooks/executor.d.ts +3 -1
  12. package/dist/runner/hooks/executor.js +21 -1
  13. package/dist/runner/hooks/predefined/compaction-tool.js +67 -2
  14. package/dist/runner/hooks/types.d.ts +5 -0
  15. package/dist/runner/index.d.ts +11 -0
  16. package/dist/runner/langchain/index.d.ts +10 -0
  17. package/dist/runner/langchain/index.js +227 -7
  18. package/dist/runner/langchain/model-factory.js +28 -1
  19. package/dist/runner/langchain/tools/artifacts.js +6 -3
  20. package/dist/runner/langchain/tools/e2b.d.ts +48 -0
  21. package/dist/runner/langchain/tools/e2b.js +305 -0
  22. package/dist/runner/langchain/tools/filesystem.js +63 -0
  23. package/dist/runner/langchain/tools/subagent.d.ts +8 -0
  24. package/dist/runner/langchain/tools/subagent.js +76 -4
  25. package/dist/runner/langchain/tools/web_search.d.ts +36 -14
  26. package/dist/runner/langchain/tools/web_search.js +33 -2
  27. package/dist/runner/session-context.d.ts +20 -0
  28. package/dist/runner/session-context.js +54 -0
  29. package/dist/runner/tools.d.ts +2 -2
  30. package/dist/runner/tools.js +1 -0
  31. package/dist/tsconfig.tsbuildinfo +1 -1
  32. 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;
@@ -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 makeHttpTransport(agent, agentDir, agentName) {
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>;
@@ -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
- const 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.
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
  */
@@ -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;