@townco/agent 0.1.102 → 0.1.105

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 (27) hide show
  1. package/dist/acp-server/adapter.d.ts +10 -0
  2. package/dist/acp-server/adapter.js +101 -31
  3. package/dist/definition/index.d.ts +17 -4
  4. package/dist/definition/index.js +19 -2
  5. package/dist/runner/agent-runner.d.ts +6 -2
  6. package/dist/runner/hooks/executor.d.ts +5 -3
  7. package/dist/runner/hooks/executor.js +190 -150
  8. package/dist/runner/hooks/loader.d.ts +13 -1
  9. package/dist/runner/hooks/loader.js +27 -0
  10. package/dist/runner/hooks/predefined/compaction-tool.d.ts +3 -1
  11. package/dist/runner/hooks/predefined/compaction-tool.js +38 -2
  12. package/dist/runner/hooks/predefined/context-validator.d.ts +57 -0
  13. package/dist/runner/hooks/predefined/context-validator.js +92 -0
  14. package/dist/runner/hooks/predefined/document-context-extractor/chunk-manager.js +2 -2
  15. package/dist/runner/hooks/predefined/document-context-extractor/content-extractor.js +29 -0
  16. package/dist/runner/hooks/predefined/document-context-extractor/relevance-scorer.js +29 -0
  17. package/dist/runner/hooks/predefined/mid-turn-compaction.d.ts +17 -0
  18. package/dist/runner/hooks/predefined/mid-turn-compaction.js +224 -0
  19. package/dist/runner/hooks/predefined/token-utils.d.ts +11 -0
  20. package/dist/runner/hooks/predefined/token-utils.js +13 -0
  21. package/dist/runner/hooks/predefined/tool-response-compactor.js +155 -25
  22. package/dist/runner/hooks/registry.js +2 -0
  23. package/dist/runner/hooks/types.d.ts +37 -4
  24. package/dist/runner/index.d.ts +6 -2
  25. package/dist/runner/langchain/index.js +60 -8
  26. package/dist/tsconfig.tsbuildinfo +1 -1
  27. package/package.json +1 -1
@@ -1,5 +1,6 @@
1
1
  import * as acp from "@agentclientprotocol/sdk";
2
2
  import type { AgentRunner } from "../runner";
3
+ import { type ContextEntry } from "./session-storage.js";
3
4
  /**
4
5
  * ACP extension key for subagent mode indicator
5
6
  * Following ACP extensibility pattern with namespaced key
@@ -23,6 +24,15 @@ export interface CitationSource {
23
24
  toolCallId: string;
24
25
  sourceName?: string;
25
26
  }
27
+ /**
28
+ * Error thrown when mid-turn compaction requires the turn to be restarted
29
+ * with compacted context. The adapter catches this error and restarts
30
+ * the invocation with the updated context.
31
+ */
32
+ export declare class MidTurnRestartError extends Error {
33
+ newContextEntry: ContextEntry;
34
+ constructor(message: string, newContextEntry: ContextEntry);
35
+ }
26
36
  /** Adapts an Agent to speak the ACP protocol */
27
37
  export declare class AgentAcpAdapter implements acp.Agent {
28
38
  private connection;
@@ -13,6 +13,19 @@ const logger = createLogger("adapter");
13
13
  * Following ACP extensibility pattern with namespaced key
14
14
  */
15
15
  export const SUBAGENT_MODE_KEY = "town.com/isSubagent";
16
+ /**
17
+ * Error thrown when mid-turn compaction requires the turn to be restarted
18
+ * with compacted context. The adapter catches this error and restarts
19
+ * the invocation with the updated context.
20
+ */
21
+ export class MidTurnRestartError extends Error {
22
+ newContextEntry;
23
+ constructor(message, newContextEntry) {
24
+ super(message);
25
+ this.name = "MidTurnRestartError";
26
+ this.newContextEntry = newContextEntry;
27
+ }
28
+ }
16
29
  /**
17
30
  * Create a context snapshot based on the previous context
18
31
  * Preserves full messages from previous context and adds new pointers
@@ -1456,31 +1469,35 @@ export class AgentAcpAdapter {
1456
1469
  typeof rawOutput === "object" &&
1457
1470
  "_compactionMeta" in rawOutput) {
1458
1471
  const compactionMeta = rawOutput._compactionMeta;
1459
- // Store in _meta for UI persistence
1460
- if (!toolCallBlock._meta) {
1461
- toolCallBlock._meta = {};
1462
- }
1463
- if (compactionMeta.action) {
1472
+ // Determine if actual compaction occurred:
1473
+ // 1. Action is a compaction action (not "none" or "no_action_needed")
1474
+ // 2. Tokens were actually saved (finalTokens < originalTokens)
1475
+ const isCompactionAction = compactionMeta.action === "compacted" ||
1476
+ compactionMeta.action === "truncated" ||
1477
+ compactionMeta.action === "compacted_then_truncated";
1478
+ const actuallyCompacted = isCompactionAction &&
1479
+ compactionMeta.originalTokens !== undefined &&
1480
+ compactionMeta.finalTokens !== undefined &&
1481
+ compactionMeta.finalTokens < compactionMeta.originalTokens;
1482
+ // Store in _meta for UI persistence only if actual compaction occurred
1483
+ if (actuallyCompacted) {
1484
+ if (!toolCallBlock._meta) {
1485
+ toolCallBlock._meta = {};
1486
+ }
1464
1487
  toolCallBlock._meta.compactionAction = compactionMeta.action;
1465
- }
1466
- if (compactionMeta.originalTokens !== undefined) {
1467
1488
  toolCallBlock._meta.originalTokens =
1468
1489
  compactionMeta.originalTokens;
1469
- }
1470
- if (compactionMeta.finalTokens !== undefined) {
1471
1490
  toolCallBlock._meta.finalTokens = compactionMeta.finalTokens;
1472
1491
  }
1473
- // Store original content only if compaction or truncation actually occurred
1474
- const wasCompacted = compactionMeta.action === "compacted" ||
1475
- compactionMeta.action === "truncated" ||
1476
- compactionMeta.action === "compacted_then_truncated";
1477
1492
  if (compactionMeta.originalContent &&
1478
- wasCompacted &&
1493
+ actuallyCompacted &&
1479
1494
  this.storage) {
1480
1495
  try {
1481
1496
  const toolName = toolCallBlock.title || "unknown";
1482
1497
  const originalContentPath = this.storage.saveToolOriginal(params.sessionId, toolName, outputMsg.toolCallId, compactionMeta.originalContent);
1483
- toolCallBlock._meta.originalContentPath = originalContentPath;
1498
+ // _meta is guaranteed to be initialized since actuallyCompacted is true
1499
+ toolCallBlock._meta.originalContentPath =
1500
+ originalContentPath;
1484
1501
  logger.info("Saved original content to artifacts", {
1485
1502
  toolCallId: outputMsg.toolCallId,
1486
1503
  toolName,
@@ -1577,23 +1594,25 @@ export class AgentAcpAdapter {
1577
1594
  if (!toolCallBlock._meta) {
1578
1595
  toolCallBlock._meta = {};
1579
1596
  }
1580
- // Store compaction action and stats
1581
- if (hookResult.metadata.action) {
1582
- toolCallBlock._meta.compactionAction = hookResult.metadata
1583
- .action;
1597
+ // Store compaction action and stats only when actual compaction occurred
1598
+ // Skip "none" and "no_action_needed" which mean no compaction was performed
1599
+ // Also verify that tokens were actually saved (finalTokens < originalTokens)
1600
+ const action = hookResult.metadata.action;
1601
+ const originalTokens = hookResult.metadata.originalTokens;
1602
+ const finalTokens = hookResult.metadata.finalTokens;
1603
+ const actuallyCompacted = action &&
1604
+ action !== "none" &&
1605
+ action !== "no_action_needed" &&
1606
+ originalTokens !== undefined &&
1607
+ finalTokens !== undefined &&
1608
+ finalTokens < originalTokens;
1609
+ if (actuallyCompacted) {
1610
+ toolCallBlock._meta.compactionAction = action;
1611
+ toolCallBlock._meta.originalTokens = originalTokens;
1612
+ toolCallBlock._meta.finalTokens = finalTokens;
1584
1613
  }
1585
- if (hookResult.metadata.originalTokens !== undefined) {
1586
- toolCallBlock._meta.originalTokens = hookResult.metadata
1587
- .originalTokens;
1588
- }
1589
- if (hookResult.metadata.finalTokens !== undefined) {
1590
- toolCallBlock._meta.finalTokens = hookResult.metadata
1591
- .finalTokens;
1592
- }
1593
- // Store original content if compaction occurred (action !== "none")
1594
- if (hookResult.metadata.action &&
1595
- hookResult.metadata.action !== "none" &&
1596
- this.storage) {
1614
+ // Store original content only if actual compaction occurred
1615
+ if (actuallyCompacted && this.storage) {
1597
1616
  try {
1598
1617
  const toolName = toolCallBlock.title || "unknown";
1599
1618
  const originalContentPath = this.storage.saveToolOriginal(params.sessionId, toolName, outputMsg.toolCallId, originalContentStr);
@@ -1615,6 +1634,34 @@ export class AgentAcpAdapter {
1615
1634
  }
1616
1635
  }
1617
1636
  }
1637
+ // Apply context compaction if hook returned a new context entry
1638
+ // This happens when compaction_tool runs in the tool_response chain
1639
+ if (hookResult.newContextEntry) {
1640
+ session.context.push(hookResult.newContextEntry);
1641
+ logger.info("Context compacted by tool_response hook, new context entry added", {
1642
+ toolCallId: outputMsg.toolCallId,
1643
+ contextSize: hookResult.newContextEntry.context_size?.totalEstimated,
1644
+ compactedUpTo: hookResult.newContextEntry.compactedUpTo,
1645
+ });
1646
+ // Check if mid-turn compaction requires a restart
1647
+ if (hookResult.metadata?.requiresRestart) {
1648
+ logger.warn("Mid-turn compaction requires restart - aborting current turn", {
1649
+ toolCallId: outputMsg.toolCallId,
1650
+ tokensSaved: hookResult.metadata.tokensSaved,
1651
+ summaryTokens: hookResult.metadata.summaryTokens,
1652
+ });
1653
+ // Store the pending tool output in session for replay after restart
1654
+ // We'll include this as part of the compacted context
1655
+ session.pendingToolOutput = {
1656
+ toolCallId: outputMsg.toolCallId,
1657
+ toolName: toolCallBlock.title || "unknown",
1658
+ rawOutput,
1659
+ };
1660
+ // Throw an error to abort the current turn
1661
+ // The handlePrompt method will catch this and restart
1662
+ throw new MidTurnRestartError("Context compacted mid-turn, restart required", hookResult.newContextEntry);
1663
+ }
1664
+ }
1618
1665
  }
1619
1666
  }
1620
1667
  // Store the (potentially modified) output
@@ -1877,6 +1924,29 @@ export class AgentAcpAdapter {
1877
1924
  await saveCancelledMessage();
1878
1925
  return { stopReason: "cancelled" };
1879
1926
  }
1927
+ // Handle mid-turn compaction restart
1928
+ if (err instanceof MidTurnRestartError) {
1929
+ logger.warn("Mid-turn compaction restart triggered - rebuilding context and restarting invocation", {
1930
+ sessionId: params.sessionId,
1931
+ newContextSize: err.newContextEntry.context_size?.totalEstimated,
1932
+ });
1933
+ // Clear the pending tool output since it's now part of the context
1934
+ delete session.pendingToolOutput;
1935
+ // Notify UI that a restart is happening
1936
+ this.connection.sessionUpdate({
1937
+ sessionId: params.sessionId,
1938
+ update: {
1939
+ sessionUpdate: "agent_message_chunk",
1940
+ content: {
1941
+ type: "text",
1942
+ text: "\n\n---\n*Context compacted mid-turn. Continuing from summary...*\n\n",
1943
+ },
1944
+ },
1945
+ });
1946
+ // Recursive call with the updated session (already modified in place)
1947
+ // _promptImpl will resolve the updated context from session.context
1948
+ return this._promptImpl(params);
1949
+ }
1880
1950
  throw err;
1881
1951
  }
1882
1952
  // Store the complete assistant response in session messages
@@ -12,6 +12,11 @@ export declare const McpConfigSchema: z.ZodUnion<readonly [z.ZodObject<{
12
12
  url: z.ZodString;
13
13
  headers: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
14
14
  }, z.core.$strip>, z.ZodString]>;
15
+ /** Individual callback configuration schema. */
16
+ export declare const CallbackConfigSchema: z.ZodObject<{
17
+ name: z.ZodString;
18
+ setting: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
19
+ }, z.core.$strip>;
15
20
  /** Hook configuration schema. */
16
21
  export declare const HookConfigSchema: z.ZodObject<{
17
22
  type: z.ZodEnum<{
@@ -22,8 +27,12 @@ export declare const HookConfigSchema: z.ZodObject<{
22
27
  threshold: z.ZodNumber;
23
28
  }, z.core.$strip>, z.ZodObject<{
24
29
  maxTokensSize: z.ZodOptional<z.ZodNumber>;
25
- }, z.core.$strip>]>>;
26
- callback: z.ZodString;
30
+ }, z.core.$strip>, z.ZodRecord<z.ZodString, z.ZodUnknown>]>>;
31
+ callback: z.ZodOptional<z.ZodString>;
32
+ callbacks: z.ZodOptional<z.ZodArray<z.ZodObject<{
33
+ name: z.ZodString;
34
+ setting: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
35
+ }, z.core.$strip>>>;
27
36
  }, z.core.$strip>;
28
37
  /** Initial message configuration schema. */
29
38
  export declare const InitialMessageSchema: z.ZodObject<{
@@ -96,8 +105,12 @@ export declare const AgentDefinitionSchema: z.ZodObject<{
96
105
  threshold: z.ZodNumber;
97
106
  }, z.core.$strip>, z.ZodObject<{
98
107
  maxTokensSize: z.ZodOptional<z.ZodNumber>;
99
- }, z.core.$strip>]>>;
100
- callback: z.ZodString;
108
+ }, z.core.$strip>, z.ZodRecord<z.ZodString, z.ZodUnknown>]>>;
109
+ callback: z.ZodOptional<z.ZodString>;
110
+ callbacks: z.ZodOptional<z.ZodArray<z.ZodObject<{
111
+ name: z.ZodString;
112
+ setting: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
113
+ }, z.core.$strip>>>;
101
114
  }, z.core.$strip>>>;
102
115
  initialMessage: z.ZodOptional<z.ZodObject<{
103
116
  enabled: z.ZodBoolean;
@@ -53,9 +53,18 @@ const ToolSchema = z.union([
53
53
  FilesystemToolSchema,
54
54
  DirectToolSchema,
55
55
  ]);
56
+ /** Individual callback configuration schema. */
57
+ export const CallbackConfigSchema = z.object({
58
+ /** Callback reference - predefined hook name or file path */
59
+ name: z.string(),
60
+ /** Callback-specific settings */
61
+ setting: z.record(z.string(), z.unknown()).optional(),
62
+ });
56
63
  /** Hook configuration schema. */
57
- export const HookConfigSchema = z.object({
64
+ export const HookConfigSchema = z
65
+ .object({
58
66
  type: z.enum(["context_size", "tool_response"]),
67
+ /** @deprecated Use callbacks array instead */
59
68
  setting: z
60
69
  .union([
61
70
  // For context_size hooks
@@ -66,9 +75,17 @@ export const HookConfigSchema = z.object({
66
75
  z.object({
67
76
  maxTokensSize: z.number().min(0).optional(),
68
77
  }),
78
+ // Generic settings for callbacks
79
+ z.record(z.string(), z.unknown()),
69
80
  ])
70
81
  .optional(),
71
- callback: z.string(),
82
+ /** @deprecated Use callbacks array instead */
83
+ callback: z.string().optional(),
84
+ /** Array of callback configurations to execute in order */
85
+ callbacks: z.array(CallbackConfigSchema).optional(),
86
+ })
87
+ .refine((data) => data.callback || data.callbacks, {
88
+ message: "Either 'callback' or 'callbacks' must be provided",
72
89
  });
73
90
  /** Initial message configuration schema. */
74
91
  export const InitialMessageSchema = z.object({
@@ -49,8 +49,12 @@ export declare const zAgentRunnerParams: z.ZodObject<{
49
49
  threshold: z.ZodNumber;
50
50
  }, z.core.$strip>, z.ZodObject<{
51
51
  maxTokensSize: z.ZodOptional<z.ZodNumber>;
52
- }, z.core.$strip>]>>;
53
- callback: z.ZodString;
52
+ }, z.core.$strip>, z.ZodRecord<z.ZodString, z.ZodUnknown>]>>;
53
+ callback: z.ZodOptional<z.ZodString>;
54
+ callbacks: z.ZodOptional<z.ZodArray<z.ZodObject<{
55
+ name: z.ZodString;
56
+ setting: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
57
+ }, z.core.$strip>>>;
54
58
  }, z.core.$strip>>>;
55
59
  initialMessage: z.ZodOptional<z.ZodObject<{
56
60
  enabled: z.ZodBoolean;
@@ -16,7 +16,8 @@ export declare class HookExecutor {
16
16
  private agentDefinition;
17
17
  private storage;
18
18
  private sessionId;
19
- constructor(hooks: HookConfig[], model: string, loadCallback: (callbackRef: string) => Promise<HookCallback>, onNotification?: OnHookNotification, agentDefinition?: Readonly<AgentDefinition>, storage?: HookStorageInterface, sessionId?: string);
19
+ private agentDir;
20
+ constructor(hooks: HookConfig[], model: string, loadCallback: (callbackRef: string) => Promise<HookCallback>, onNotification?: OnHookNotification, agentDefinition?: Readonly<AgentDefinition>, storage?: HookStorageInterface, sessionId?: string, agentDir?: string);
20
21
  /**
21
22
  * Emit a notification - sends immediately if callback provided, otherwise collects for batch return
22
23
  */
@@ -31,7 +32,8 @@ export declare class HookExecutor {
31
32
  }>;
32
33
  private executeContextSizeHook;
33
34
  /**
34
- * Execute tool_response hooks when a tool returns output
35
+ * Execute tool_response hooks when a tool returns output.
36
+ * Chains callbacks in order, passing modified output between them.
35
37
  */
36
38
  executeToolResponseHooks(session: ReadonlySession, currentContextTokens: number, toolResponse: {
37
39
  toolCallId: string;
@@ -43,7 +45,7 @@ export declare class HookExecutor {
43
45
  modifiedOutput?: Record<string, unknown>;
44
46
  truncationWarning?: string;
45
47
  metadata?: Record<string, unknown>;
48
+ newContextEntry?: ContextEntry;
46
49
  notifications: HookNotification[];
47
50
  }>;
48
- private executeToolResponseHook;
49
51
  }