@tyvm/knowhow 0.0.138 → 0.0.141

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 (173) hide show
  1. package/autodoc/chat-guide.md +1 -0
  2. package/package.json +5 -2
  3. package/scripts/build-for-node.sh +4 -4
  4. package/src/agents/base/base.ts +254 -29
  5. package/src/agents/patcher/patcher.ts +1 -1
  6. package/src/agents/researcher/researcher.ts +2 -2
  7. package/src/agents/tools/agentCall.ts +6 -0
  8. package/src/agents/tools/connectAgent.ts +353 -0
  9. package/src/agents/tools/index.ts +6 -0
  10. package/src/agents/tools/list.ts +48 -0
  11. package/src/agents/tools/observe.ts +245 -0
  12. package/src/agents/tools/replyToParent.ts +122 -0
  13. package/src/agents/tools/runGenerate.ts +209 -0
  14. package/src/agents/tools/sendAgentMessage.ts +138 -0
  15. package/src/agents/tools/startAgentTask.ts +92 -5
  16. package/src/agents/tools/waitForAgentCompleted.ts +269 -0
  17. package/src/ai.ts +1 -0
  18. package/src/chat/modules/AgentModule.ts +89 -27
  19. package/src/chat/modules/SessionsModule.ts +6 -10
  20. package/src/clients/contextLimits.ts +26 -0
  21. package/src/clients/pricing/anthropic.ts +12 -0
  22. package/src/commands/agent.ts +22 -0
  23. package/src/commands/agents.ts +83 -0
  24. package/src/commands/misc.ts +6 -0
  25. package/src/commands/modules.ts +159 -7
  26. package/src/commands/services.ts +14 -0
  27. package/src/generate.ts +25 -3
  28. package/src/services/AgentService.ts +21 -3
  29. package/src/services/AgentSyncFs.ts +56 -0
  30. package/src/services/AgentSyncKnowhowWeb.ts +1 -1
  31. package/src/services/EventService.ts +4 -1
  32. package/src/services/KnowhowClient.ts +7 -0
  33. package/src/services/LazyToolsService.ts +7 -3
  34. package/src/services/MinimalToolsService.ts +15 -7
  35. package/src/services/SyncedAgentWatcher.ts +2 -0
  36. package/src/services/SyncerService.ts +35 -6
  37. package/src/services/Tools.ts +43 -3
  38. package/src/services/modules/index.ts +127 -46
  39. package/src/types.ts +5 -0
  40. package/tests/agents/interrupt.test.ts +11 -18
  41. package/tests/agents/observe-base.test.ts +183 -0
  42. package/tests/agents/tools/connectAgent.test.ts +138 -0
  43. package/tests/agents/tools/observe.test.ts +179 -0
  44. package/tests/agents/tools/replyToParent.test.ts +125 -0
  45. package/tests/agents/tools/sendAgentMessage.test.ts +99 -0
  46. package/tests/agents/tools/waitForAgentCompleted.test.ts +147 -0
  47. package/tests/chat/detach-attach-rendering.test.ts +315 -0
  48. package/tests/clients/contextLimits.test.ts +23 -1
  49. package/tests/clients/pricing.test.ts +20 -1
  50. package/tests/services/Tools.test.ts +172 -9
  51. package/tests/unit/modules/moduleLoading.test.ts +69 -0
  52. package/ts_build/package.json +5 -2
  53. package/ts_build/src/agents/base/base.d.ts +24 -1
  54. package/ts_build/src/agents/base/base.js +111 -16
  55. package/ts_build/src/agents/base/base.js.map +1 -1
  56. package/ts_build/src/agents/patcher/patcher.js +1 -1
  57. package/ts_build/src/agents/patcher/patcher.js.map +1 -1
  58. package/ts_build/src/agents/researcher/researcher.js +2 -2
  59. package/ts_build/src/agents/researcher/researcher.js.map +1 -1
  60. package/ts_build/src/agents/tools/agentCall.js.map +1 -1
  61. package/ts_build/src/agents/tools/connectAgent.d.ts +21 -0
  62. package/ts_build/src/agents/tools/connectAgent.js +284 -0
  63. package/ts_build/src/agents/tools/connectAgent.js.map +1 -0
  64. package/ts_build/src/agents/tools/index.d.ts +6 -0
  65. package/ts_build/src/agents/tools/index.js +6 -0
  66. package/ts_build/src/agents/tools/index.js.map +1 -1
  67. package/ts_build/src/agents/tools/list.js +40 -0
  68. package/ts_build/src/agents/tools/list.js.map +1 -1
  69. package/ts_build/src/agents/tools/observe.d.ts +20 -0
  70. package/ts_build/src/agents/tools/observe.js +144 -0
  71. package/ts_build/src/agents/tools/observe.js.map +1 -0
  72. package/ts_build/src/agents/tools/replyToParent.d.ts +4 -0
  73. package/ts_build/src/agents/tools/replyToParent.js +118 -0
  74. package/ts_build/src/agents/tools/replyToParent.js.map +1 -0
  75. package/ts_build/src/agents/tools/runGenerate.d.ts +19 -0
  76. package/ts_build/src/agents/tools/runGenerate.js +138 -0
  77. package/ts_build/src/agents/tools/runGenerate.js.map +1 -0
  78. package/ts_build/src/agents/tools/sendAgentMessage.d.ts +11 -0
  79. package/ts_build/src/agents/tools/sendAgentMessage.js +118 -0
  80. package/ts_build/src/agents/tools/sendAgentMessage.js.map +1 -0
  81. package/ts_build/src/agents/tools/startAgentTask.d.ts +7 -0
  82. package/ts_build/src/agents/tools/startAgentTask.js +53 -6
  83. package/ts_build/src/agents/tools/startAgentTask.js.map +1 -1
  84. package/ts_build/src/agents/tools/waitForAgentCompleted.d.ts +10 -0
  85. package/ts_build/src/agents/tools/waitForAgentCompleted.js +229 -0
  86. package/ts_build/src/agents/tools/waitForAgentCompleted.js.map +1 -0
  87. package/ts_build/src/ai.d.ts +1 -0
  88. package/ts_build/src/ai.js.map +1 -1
  89. package/ts_build/src/chat/modules/AgentModule.d.ts +3 -1
  90. package/ts_build/src/chat/modules/AgentModule.js +46 -21
  91. package/ts_build/src/chat/modules/AgentModule.js.map +1 -1
  92. package/ts_build/src/chat/modules/SessionsModule.js +1 -6
  93. package/ts_build/src/chat/modules/SessionsModule.js.map +1 -1
  94. package/ts_build/src/clients/contextLimits.js +21 -0
  95. package/ts_build/src/clients/contextLimits.js.map +1 -1
  96. package/ts_build/src/clients/pricing/anthropic.d.ts +2 -0
  97. package/ts_build/src/clients/pricing/anthropic.js +9 -0
  98. package/ts_build/src/clients/pricing/anthropic.js.map +1 -1
  99. package/ts_build/src/commands/agent.js +11 -0
  100. package/ts_build/src/commands/agent.js.map +1 -1
  101. package/ts_build/src/commands/agents.js +61 -0
  102. package/ts_build/src/commands/agents.js.map +1 -1
  103. package/ts_build/src/commands/misc.js +2 -0
  104. package/ts_build/src/commands/misc.js.map +1 -1
  105. package/ts_build/src/commands/modules.js +86 -7
  106. package/ts_build/src/commands/modules.js.map +1 -1
  107. package/ts_build/src/commands/services.js +10 -0
  108. package/ts_build/src/commands/services.js.map +1 -1
  109. package/ts_build/src/generate.d.ts +2 -0
  110. package/ts_build/src/generate.js +11 -3
  111. package/ts_build/src/generate.js.map +1 -1
  112. package/ts_build/src/services/AgentService.d.ts +4 -1
  113. package/ts_build/src/services/AgentService.js +9 -2
  114. package/ts_build/src/services/AgentService.js.map +1 -1
  115. package/ts_build/src/services/AgentSyncFs.d.ts +2 -0
  116. package/ts_build/src/services/AgentSyncFs.js +35 -0
  117. package/ts_build/src/services/AgentSyncFs.js.map +1 -1
  118. package/ts_build/src/services/AgentSyncKnowhowWeb.d.ts +1 -1
  119. package/ts_build/src/services/EventService.d.ts +4 -1
  120. package/ts_build/src/services/EventService.js.map +1 -1
  121. package/ts_build/src/services/KnowhowClient.js +3 -0
  122. package/ts_build/src/services/KnowhowClient.js.map +1 -1
  123. package/ts_build/src/services/LazyToolsService.d.ts +2 -2
  124. package/ts_build/src/services/LazyToolsService.js +2 -2
  125. package/ts_build/src/services/LazyToolsService.js.map +1 -1
  126. package/ts_build/src/services/MinimalToolsService.d.ts +2 -2
  127. package/ts_build/src/services/MinimalToolsService.js +6 -6
  128. package/ts_build/src/services/MinimalToolsService.js.map +1 -1
  129. package/ts_build/src/services/SyncedAgentWatcher.d.ts +1 -0
  130. package/ts_build/src/services/SyncedAgentWatcher.js.map +1 -1
  131. package/ts_build/src/services/SyncerService.d.ts +2 -1
  132. package/ts_build/src/services/SyncerService.js +8 -2
  133. package/ts_build/src/services/SyncerService.js.map +1 -1
  134. package/ts_build/src/services/Tools.d.ts +6 -1
  135. package/ts_build/src/services/Tools.js +9 -3
  136. package/ts_build/src/services/Tools.js.map +1 -1
  137. package/ts_build/src/services/modules/index.d.ts +1 -1
  138. package/ts_build/src/services/modules/index.js +67 -27
  139. package/ts_build/src/services/modules/index.js.map +1 -1
  140. package/ts_build/src/types.d.ts +3 -0
  141. package/ts_build/src/types.js.map +1 -1
  142. package/ts_build/tests/agents/interrupt.test.d.ts +0 -1
  143. package/ts_build/tests/agents/interrupt.test.js +3 -13
  144. package/ts_build/tests/agents/interrupt.test.js.map +1 -1
  145. package/ts_build/tests/agents/observe-base.test.d.ts +1 -0
  146. package/ts_build/tests/agents/observe-base.test.js +149 -0
  147. package/ts_build/tests/agents/observe-base.test.js.map +1 -0
  148. package/ts_build/tests/agents/tools/connectAgent.test.d.ts +1 -0
  149. package/ts_build/tests/agents/tools/connectAgent.test.js +142 -0
  150. package/ts_build/tests/agents/tools/connectAgent.test.js.map +1 -0
  151. package/ts_build/tests/agents/tools/observe.test.d.ts +1 -0
  152. package/ts_build/tests/agents/tools/observe.test.js +148 -0
  153. package/ts_build/tests/agents/tools/observe.test.js.map +1 -0
  154. package/ts_build/tests/agents/tools/replyToParent.test.d.ts +1 -0
  155. package/ts_build/tests/agents/tools/replyToParent.test.js +116 -0
  156. package/ts_build/tests/agents/tools/replyToParent.test.js.map +1 -0
  157. package/ts_build/tests/agents/tools/sendAgentMessage.test.d.ts +1 -0
  158. package/ts_build/tests/agents/tools/sendAgentMessage.test.js +112 -0
  159. package/ts_build/tests/agents/tools/sendAgentMessage.test.js.map +1 -0
  160. package/ts_build/tests/agents/tools/waitForAgentCompleted.test.d.ts +1 -0
  161. package/ts_build/tests/agents/tools/waitForAgentCompleted.test.js +147 -0
  162. package/ts_build/tests/agents/tools/waitForAgentCompleted.test.js.map +1 -0
  163. package/ts_build/tests/chat/detach-attach-rendering.test.d.ts +1 -0
  164. package/ts_build/tests/chat/detach-attach-rendering.test.js +168 -0
  165. package/ts_build/tests/chat/detach-attach-rendering.test.js.map +1 -0
  166. package/ts_build/tests/clients/contextLimits.test.js +11 -1
  167. package/ts_build/tests/clients/contextLimits.test.js.map +1 -1
  168. package/ts_build/tests/clients/pricing.test.js +10 -1
  169. package/ts_build/tests/clients/pricing.test.js.map +1 -1
  170. package/ts_build/tests/services/Tools.test.js +126 -8
  171. package/ts_build/tests/services/Tools.test.js.map +1 -1
  172. package/ts_build/tests/unit/modules/moduleLoading.test.js +84 -0
  173. package/ts_build/tests/unit/modules/moduleLoading.test.js.map +1 -1
@@ -1,4 +1,4 @@
1
- import { ToolsService, ToolContext } from "../../src/services/Tools";
1
+ import { ToolsService, ToolContext, ToolCallContext } from "../../src/services/Tools";
2
2
  import { Tool, ToolCall } from "../../src/clients/types";
3
3
  import { createPatternMatcher } from "../../src/services/types";
4
4
  import { AgentService } from "../../src/services/AgentService";
@@ -268,7 +268,11 @@ describe("ToolsService", () => {
268
268
 
269
269
  const result = await toolsService.callTool(mockToolCall);
270
270
 
271
- expect(mockFunction).toHaveBeenCalledWith({ input: "test value" });
271
+ // Non-positional tools receive `_ctx` merged onto the args object.
272
+ expect(mockFunction).toHaveBeenCalledWith(
273
+ expect.objectContaining({ input: "test value" })
274
+ );
275
+ expect(mockFunction.mock.calls[0][0]._ctx).toBeDefined();
272
276
  expect(result.toolMessages).toHaveLength(1);
273
277
  expect(result.toolMessages[0]).toEqual({
274
278
  tool_call_id: "call_123",
@@ -312,7 +316,12 @@ describe("ToolsService", () => {
312
316
 
313
317
  await toolsService.callTool(positionalCall);
314
318
 
315
- expect(mockFunction).toHaveBeenCalledWith("hello", 42);
319
+ // Positional tools receive `_ctx` as the LAST positional argument.
320
+ expect(mockFunction).toHaveBeenCalledWith(
321
+ "hello",
322
+ 42,
323
+ expect.objectContaining({ Tools: expect.anything() })
324
+ );
316
325
  });
317
326
 
318
327
  it("should handle tool not enabled error", async () => {
@@ -433,7 +442,10 @@ describe("ToolsService", () => {
433
442
 
434
443
  await toolsService.callTool(toolCallWithStringArgs);
435
444
 
436
- expect(mockFunction).toHaveBeenCalledWith({ input: "escaped\nstring" });
445
+ expect(mockFunction).toHaveBeenCalledWith(
446
+ expect.objectContaining({ input: "escaped\nstring" })
447
+ );
448
+ expect(mockFunction.mock.calls[0][0]._ctx).toBeDefined();
437
449
  });
438
450
  });
439
451
  describe("Tool Override System", () => {
@@ -492,8 +504,9 @@ describe("ToolsService", () => {
492
504
 
493
505
  const result = await toolsService.callTool(toolCall);
494
506
 
507
+ // Non-positional args now include `_ctx`; the override receives them in an array.
495
508
  expect(overrideFunction).toHaveBeenCalledWith(
496
- [{ input: "test" }],
509
+ [expect.objectContaining({ input: "test" })],
497
510
  expect.any(Object)
498
511
  );
499
512
  expect(originalFunction).not.toHaveBeenCalled();
@@ -627,7 +640,9 @@ describe("ToolsService", () => {
627
640
  const result = await toolsService.callTool(toolCall);
628
641
 
629
642
  expect(wrapperFunction).toHaveBeenCalled();
630
- expect(originalFunction).toHaveBeenCalledWith({ input: "test" });
643
+ expect(originalFunction).toHaveBeenCalledWith(
644
+ expect.objectContaining({ input: "test" })
645
+ );
631
646
  expect(result.functionResp).toBe("wrapped: original result");
632
647
  });
633
648
 
@@ -841,7 +856,8 @@ describe("ToolsService", () => {
841
856
 
842
857
  const result = await toolsService.callTool(noParamCall);
843
858
 
844
- expect(mockFunction).toHaveBeenCalledWith({});
859
+ expect(mockFunction).toHaveBeenCalledWith(expect.any(Object));
860
+ expect(mockFunction.mock.calls[0][0]._ctx).toBeDefined();
845
861
  expect(result.functionResp).toBe("no param result");
846
862
  });
847
863
 
@@ -897,14 +913,16 @@ describe("ToolsService", () => {
897
913
 
898
914
  const result = await toolsService.callTool(complexCall);
899
915
 
900
- expect(mockFunction).toHaveBeenCalledWith({
916
+ expect(mockFunction).toHaveBeenCalledWith(
917
+ expect.objectContaining({
901
918
  config: {
902
919
  settings: [
903
920
  { key: "timeout", value: 5000 },
904
921
  { key: "retries", value: 3 },
905
922
  ],
906
923
  },
907
- });
924
+ })
925
+ );
908
926
  expect(result.functionResp).toBe("complex result");
909
927
  });
910
928
 
@@ -1570,4 +1588,149 @@ describe("ToolsService", () => {
1570
1588
  expect(foundTools.map(t => t.function.name)).toContain("tool2");
1571
1589
  });
1572
1590
  });
1591
+
1592
+ describe("_ctx per-call context threading", () => {
1593
+ const positionalTool: Tool = {
1594
+ type: "function",
1595
+ function: {
1596
+ name: "posCtxTool",
1597
+ parameters: {
1598
+ type: "object",
1599
+ positional: true,
1600
+ properties: {
1601
+ arg1: { type: "string" },
1602
+ },
1603
+ required: ["arg1"],
1604
+ },
1605
+ },
1606
+ };
1607
+
1608
+ const nonPositionalTool: Tool = {
1609
+ type: "function",
1610
+ function: {
1611
+ name: "nonPosCtxTool",
1612
+ parameters: {
1613
+ type: "object",
1614
+ properties: {
1615
+ input: { type: "string" },
1616
+ },
1617
+ required: ["input"],
1618
+ },
1619
+ },
1620
+ };
1621
+
1622
+ it("passes _ctx as the last positional argument for positional tools", async () => {
1623
+ const fn = jest.fn().mockResolvedValue("ok");
1624
+ toolsService.addTool(positionalTool);
1625
+ toolsService.setFunction("posCtxTool", fn);
1626
+
1627
+ const caller = { name: "TestAgent" };
1628
+ await toolsService.callTool(
1629
+ {
1630
+ id: "c1",
1631
+ type: "function",
1632
+ function: {
1633
+ name: "posCtxTool",
1634
+ arguments: JSON.stringify({ arg1: "hello" }),
1635
+ },
1636
+ },
1637
+ undefined,
1638
+ { caller, taskId: "task-123" }
1639
+ );
1640
+
1641
+ const callArgs = fn.mock.calls[0];
1642
+ expect(callArgs[0]).toBe("hello");
1643
+ const ctx = callArgs[1] as ToolCallContext;
1644
+ expect(ctx).toBeDefined();
1645
+ expect(ctx.caller).toBe(caller);
1646
+ expect(ctx.taskId).toBe("task-123");
1647
+ expect(ctx.Tools).toBe(toolsService);
1648
+ });
1649
+
1650
+ it("merges _ctx onto the args object for non-positional tools", async () => {
1651
+ const fn = jest.fn().mockResolvedValue("ok");
1652
+ toolsService.addTool(nonPositionalTool);
1653
+ toolsService.setFunction("nonPosCtxTool", fn);
1654
+
1655
+ const caller = { name: "TestAgent" };
1656
+ await toolsService.callTool(
1657
+ {
1658
+ id: "c2",
1659
+ type: "function",
1660
+ function: {
1661
+ name: "nonPosCtxTool",
1662
+ arguments: JSON.stringify({ input: "value" }),
1663
+ },
1664
+ },
1665
+ undefined,
1666
+ { caller, taskId: "task-456" }
1667
+ );
1668
+
1669
+ const argObj = fn.mock.calls[0][0];
1670
+ expect(argObj.input).toBe("value");
1671
+ expect(argObj._ctx).toBeDefined();
1672
+ expect(argObj._ctx.caller).toBe(caller);
1673
+ expect(argObj._ctx.taskId).toBe("task-456");
1674
+ expect(argObj._ctx.Tools).toBe(toolsService);
1675
+ });
1676
+
1677
+ it("still provides a _ctx (with Tools) when no callContext is passed", async () => {
1678
+ const fn = jest.fn().mockResolvedValue("ok");
1679
+ toolsService.addTool(nonPositionalTool);
1680
+ toolsService.setFunction("nonPosCtxTool", fn);
1681
+
1682
+ await toolsService.callTool({
1683
+ id: "c3",
1684
+ type: "function",
1685
+ function: {
1686
+ name: "nonPosCtxTool",
1687
+ arguments: JSON.stringify({ input: "value" }),
1688
+ },
1689
+ });
1690
+
1691
+ const argObj = fn.mock.calls[0][0];
1692
+ expect(argObj._ctx).toBeDefined();
1693
+ expect(argObj._ctx.Tools).toBe(toolsService);
1694
+ expect(argObj._ctx.caller).toBeUndefined();
1695
+ });
1696
+
1697
+ it("does not leak _ctx into the multi_tool_use.parallel extraction", async () => {
1698
+ // The parallel path should extract calls from the ORIGINAL args, not the
1699
+ // _ctx-augmented ones.
1700
+ const parallelTool: Tool = {
1701
+ type: "function",
1702
+ function: {
1703
+ name: "multi_tool_use.parallel",
1704
+ parameters: { type: "object", properties: {} },
1705
+ },
1706
+ };
1707
+ const inner = jest
1708
+ .fn()
1709
+ .mockResolvedValue(["r1", "r2"]);
1710
+ toolsService.addTool(parallelTool);
1711
+ toolsService.setFunction("multi_tool_use.parallel", inner);
1712
+
1713
+ const result = await toolsService.callTool(
1714
+ {
1715
+ id: "c4",
1716
+ type: "function",
1717
+ function: {
1718
+ name: "multi_tool_use.parallel",
1719
+ arguments: JSON.stringify([
1720
+ { recipient_name: "functions.a", parameters: {} },
1721
+ { recipient_name: "functions.b", parameters: {} },
1722
+ ]),
1723
+ },
1724
+ },
1725
+ undefined,
1726
+ { caller: {}, taskId: "t" }
1727
+ );
1728
+
1729
+ // Two tool messages, one per parallel call — proves extraction used the
1730
+ // original array shape rather than an _ctx-wrapped object.
1731
+ expect(result.toolMessages).toHaveLength(2);
1732
+ expect(result.toolMessages[0].name).toBe("a");
1733
+ expect(result.toolMessages[1].name).toBe("b");
1734
+ });
1735
+ });
1573
1736
  });
@@ -1,3 +1,5 @@
1
+ import * as os from "os";
2
+ import * as path from "path";
1
3
  import { Config } from "../../../src/types";
2
4
 
3
5
  // Mock config before any other imports that depend on it
@@ -233,4 +235,71 @@ describe("ModulesService.loadModulesFromConfig", () => {
233
235
  expect(context.Agents.registerAgent).toHaveBeenCalledTimes(2);
234
236
  spy.mockRestore();
235
237
  });
238
+
239
+ it("resolves global-config modules against ~/.knowhow/node_modules first, local against cwd/.knowhow/node_modules first", async () => {
240
+ mockGetGlobalConfig.mockResolvedValue({
241
+ modules: ["global-pkg"],
242
+ } as unknown as Config);
243
+ mockGetConfig.mockResolvedValue({
244
+ modules: ["local-pkg"],
245
+ } as unknown as Config);
246
+
247
+ const service = new ModulesService();
248
+ const context = makeContext();
249
+
250
+ const calls: { modules: string[]; resolvePaths: string[] }[] = [];
251
+ const spy = jest
252
+ .spyOn(service as any, "loadModulesFrom")
253
+ .mockImplementation(async (...args: any[]) => {
254
+ const [cfg, , resolvePaths] = args;
255
+ calls.push({ modules: cfg.modules, resolvePaths });
256
+ });
257
+
258
+ await service.loadModulesFromConfig(context);
259
+
260
+ const globalKnowhow = path.join(os.homedir(), ".knowhow", "node_modules");
261
+ const localKnowhow = path.join(process.cwd(), ".knowhow", "node_modules");
262
+
263
+ const globalCall = calls.find((c) => c.modules.includes("global-pkg"));
264
+ const localCall = calls.find((c) => c.modules.includes("local-pkg"));
265
+
266
+ expect(globalCall).toBeDefined();
267
+ expect(localCall).toBeDefined();
268
+ // Global-config module: global node_modules is first in the search order
269
+ expect(globalCall!.resolvePaths[0]).toBe(globalKnowhow);
270
+ // Local-config module: local project node_modules is first in the search order
271
+ expect(localCall!.resolvePaths[0]).toBe(localKnowhow);
272
+
273
+ spy.mockRestore();
274
+ });
275
+
276
+ it("does not load a local module again if it is already declared globally (avoids duplicate command registration)", async () => {
277
+ mockGetGlobalConfig.mockResolvedValue({
278
+ modules: ["shared-pkg"],
279
+ } as unknown as Config);
280
+ mockGetConfig.mockResolvedValue({
281
+ modules: ["shared-pkg", "local-only-pkg"],
282
+ } as unknown as Config);
283
+
284
+ const service = new ModulesService();
285
+ const context = makeContext();
286
+
287
+ const loadedModuleLists: string[][] = [];
288
+ const spy = jest
289
+ .spyOn(service as any, "loadModulesFrom")
290
+ .mockImplementation(async (...args: any[]) => {
291
+ const [cfg] = args;
292
+ loadedModuleLists.push(cfg.modules);
293
+ });
294
+
295
+ await service.loadModulesFromConfig(context);
296
+
297
+ const allLoaded = loadedModuleLists.flat();
298
+ // shared-pkg loaded exactly once (via the global pass)
299
+ expect(allLoaded.filter((m) => m === "shared-pkg")).toHaveLength(1);
300
+ // local-only-pkg still loaded
301
+ expect(allLoaded).toContain("local-only-pkg");
302
+
303
+ spy.mockRestore();
304
+ });
236
305
  });
@@ -1,11 +1,14 @@
1
1
  {
2
2
  "name": "@tyvm/knowhow",
3
- "version": "0.0.138",
3
+ "version": "0.0.141",
4
4
  "description": "ai cli with plugins and agents",
5
5
  "main": "ts_build/src/index.js",
6
6
  "bin": {
7
7
  "knowhow": "bin/knowhow.js"
8
8
  },
9
+ "engines": {
10
+ "node": ">=20"
11
+ },
9
12
  "scripts": {
10
13
  "test": "jest --testTimeout 300000",
11
14
  "test:debug": "node --inspect-brk ../../node_modules/jest/bin/jest.js --detectOpenHandles --forceExit --testTimeout 300000",
@@ -39,7 +42,7 @@
39
42
  "@types/express": "^4.17.13",
40
43
  "@types/jest": "^29.5.13",
41
44
  "@types/mocha": "^10.0.8",
42
- "@types/node": "^20.6.3",
45
+ "@types/node": "^24.0.0",
43
46
  "@types/ws": "^8.18.1",
44
47
  "jest": "^29.1.1",
45
48
  "prettier": "2.6.2",
@@ -21,6 +21,22 @@ export interface ToolCallEvent {
21
21
  toolCall: ToolCall;
22
22
  functionResp: any;
23
23
  }
24
+ export interface ObservationSource {
25
+ label: string;
26
+ start(emit: (datum: unknown) => void): void | (() => void) | Promise<void | (() => void)>;
27
+ }
28
+ export interface ObserveOpts {
29
+ onlyOnChange?: boolean;
30
+ maxUpdates?: number;
31
+ maxDurationMs?: number;
32
+ }
33
+ interface ActiveObservation {
34
+ id: string;
35
+ label: string;
36
+ updates: number;
37
+ teardownPromise: Promise<void | (() => void)>;
38
+ expiry?: NodeJS.Timeout;
39
+ }
24
40
  export declare abstract class BaseAgent implements IAgent {
25
41
  abstract name: string;
26
42
  abstract description: string;
@@ -46,14 +62,17 @@ export declare abstract class BaseAgent implements IAgent {
46
62
  protected totalOutputTokens: number;
47
63
  protected totalCacheReadTokens: number;
48
64
  protected totalCacheWriteTokens: number;
65
+ protected lastPromptTokens: number;
49
66
  protected compressThreshold: number;
50
67
  protected compressMinMessages: number;
68
+ protected _forceCompact: boolean;
51
69
  private _interruptResolve;
52
70
  private _interruptToken;
53
- private _pendingInterrupt;
54
71
  protected threads: Message[][];
55
72
  protected pendingUserMessages: Message[];
56
73
  protected pendingMessages: Message[];
74
+ protected _observations: Map<string, ActiveObservation>;
75
+ private _observationSeq;
57
76
  protected taskBreakdown: string;
58
77
  protected summaries: string[];
59
78
  protected currentTaskId: string | null;
@@ -87,6 +106,7 @@ export declare abstract class BaseAgent implements IAgent {
87
106
  constructor(context: AgentContext);
88
107
  protected log(message: string, level?: "info" | "warn" | "error"): void;
89
108
  setCompressThreshold(threshold: number): void;
109
+ requestCompact(): void;
90
110
  getCompressThreshold(): number;
91
111
  setMaxTurns(maxTurns: number | null): void;
92
112
  setMaxSpend(maxSpend: number | null): void;
@@ -155,7 +175,10 @@ export declare abstract class BaseAgent implements IAgent {
155
175
  logStatus(): void;
156
176
  addPendingMessage(message: Message): void;
157
177
  addPendingUserMessage(message: Message): void;
178
+ observe(source: ObservationSource, opts?: ObserveOpts): string;
179
+ stopObserving(id?: string, reason?: string): string;
158
180
  getMessagesLength(messages: Message[]): number;
181
+ getContextTokenCount(messages: Message[]): number;
159
182
  detectTruncatedToolCalls(toolCalls: ToolCall[], response: CompletionResponse): {
160
183
  role: string;
161
184
  content: string;
@@ -30,14 +30,17 @@ class BaseAgent {
30
30
  totalOutputTokens = 0;
31
31
  totalCacheReadTokens = 0;
32
32
  totalCacheWriteTokens = 0;
33
+ lastPromptTokens = 0;
33
34
  compressThreshold = 30000;
34
35
  compressMinMessages = 30;
36
+ _forceCompact = false;
35
37
  _interruptResolve = null;
36
38
  _interruptToken = 0;
37
- _pendingInterrupt = false;
38
39
  threads = [];
39
40
  pendingUserMessages = [];
40
41
  pendingMessages = [];
42
+ _observations = new Map();
43
+ _observationSeq = 0;
41
44
  taskBreakdown = "";
42
45
  summaries = [];
43
46
  currentTaskId = null;
@@ -105,6 +108,9 @@ class BaseAgent {
105
108
  setCompressThreshold(threshold) {
106
109
  this.compressThreshold = threshold;
107
110
  }
111
+ requestCompact() {
112
+ this._forceCompact = true;
113
+ }
108
114
  getCompressThreshold() {
109
115
  if (this.compressThreshold !== contextLimits_1.DEFAULT_CONTEXT_LIMIT) {
110
116
  return this.compressThreshold;
@@ -112,7 +118,7 @@ class BaseAgent {
112
118
  const result = this.clientService.getContextLimit(this.getProvider(), this.getModel());
113
119
  const contextLimit = result?.contextLimit ?? contextLimits_1.DEFAULT_CONTEXT_LIMIT;
114
120
  const threshold = result?.threshold ?? contextLimit;
115
- return Math.floor(threshold * 0.85);
121
+ return Math.floor(threshold * 0.7);
116
122
  }
117
123
  setMaxTurns(maxTurns) {
118
124
  this.maxTurns = maxTurns;
@@ -133,12 +139,14 @@ class BaseAgent {
133
139
  this.totalOutputTokens = 0;
134
140
  this.totalCacheReadTokens = 0;
135
141
  this.totalCacheWriteTokens = 0;
142
+ this.lastPromptTokens = 0;
136
143
  this.status = this.eventTypes.inProgress;
137
144
  this.turnCount = 0;
138
145
  this.startTimeMs = Date.now();
139
146
  this.currentTaskId = taskId || this.startTimeMs.toString();
140
147
  this._interruptResolve = null;
141
- this._pendingInterrupt = false;
148
+ this._forceCompact = false;
149
+ this.stopObserving(undefined, "new task started");
142
150
  const id = taskId || this.startTimeMs.toString();
143
151
  this.events.emit(this.eventTypes.agentNewTask, {
144
152
  taskId: id,
@@ -318,6 +326,10 @@ class BaseAgent {
318
326
  usage.prompt_tokens_details?.cached_tokens ??
319
327
  0;
320
328
  const cacheWriteTokens = usage.cache_creation_input_tokens ?? usage.cache_write_tokens ?? 0;
329
+ const promptTokens = inputTokens + cacheReadTokens + cacheWriteTokens;
330
+ if (promptTokens > 0) {
331
+ this.lastPromptTokens = promptTokens;
332
+ }
321
333
  this.totalInputTokens += inputTokens;
322
334
  this.totalOutputTokens += outputTokens;
323
335
  this.totalCacheReadTokens += cacheReadTokens;
@@ -385,7 +397,10 @@ class BaseAgent {
385
397
  },
386
398
  ],
387
399
  };
388
- const { functionResp, toolMessages } = await this.makeInterruptible(this.tools.callTool(toolCall, this.getEnabledToolNames()), interruptResult);
400
+ const { functionResp, toolMessages } = await this.makeInterruptible(this.tools.callTool(toolCall, this.getEnabledToolNames(), {
401
+ caller: this,
402
+ taskId: this.currentTaskId,
403
+ }), interruptResult);
389
404
  this.agentEvents.emit(this.eventTypes.toolUsed, {
390
405
  toolCall,
391
406
  functionResp,
@@ -521,11 +536,6 @@ class BaseAgent {
521
536
  fn();
522
537
  };
523
538
  this._interruptResolve = () => finish(() => resolve(interruptValue));
524
- if (this._pendingInterrupt) {
525
- this._pendingInterrupt = false;
526
- finish(() => resolve(interruptValue));
527
- return;
528
- }
529
539
  promise
530
540
  .then((result) => {
531
541
  finish(() => resolve(result));
@@ -547,8 +557,7 @@ class BaseAgent {
547
557
  this._interruptResolve();
548
558
  }
549
559
  else {
550
- this._pendingInterrupt = true;
551
- this.log("No active interruptible operation — queued interrupt for next step", "warn");
560
+ this.log("No active interruptible operation to interrupt", "warn");
552
561
  }
553
562
  }
554
563
  async call(userInput, _messages, retryCount = 0) {
@@ -608,8 +617,12 @@ class BaseAgent {
608
617
  tools: this.getEnabledTools(),
609
618
  tool_choice: "auto",
610
619
  long_ttl_cache: this.runTime() > 300_000,
611
- ...(this.reasoningEffort !== undefined && { reasoning_effort: this.reasoningEffort }),
612
- ...(this.summarizeReasoning !== undefined && { reasoning_summary: this.summarizeReasoning }),
620
+ ...(this.reasoningEffort !== undefined && {
621
+ reasoning_effort: this.reasoningEffort,
622
+ }),
623
+ ...(this.summarizeReasoning !== undefined && {
624
+ reasoning_summary: this.summarizeReasoning,
625
+ }),
613
626
  }, { interruptValue: interruptResponse });
614
627
  if (this.status === this.eventTypes.pause) {
615
628
  this.log("Agent was paused after completion, waiting before processing tool calls");
@@ -710,12 +723,18 @@ class BaseAgent {
710
723
  this.agentEvents.emit(this.eventTypes.done, firstMessage.content);
711
724
  return firstMessage.content;
712
725
  }
713
- if (this.getMessagesLength(messages) > this.getCompressThreshold() &&
714
- messages.length > this.compressMinMessages) {
726
+ const contextTokens = this.getContextTokenCount(messages);
727
+ const overThreshold = contextTokens > this.getCompressThreshold() &&
728
+ messages.length > this.compressMinMessages;
729
+ if (overThreshold || this._forceCompact) {
715
730
  const taskBreakdown = await this.getTaskBreakdown(messages);
716
- this.log(`Compressing messages: ${this.getMessagesLength(messages)} exceeds ${this.getCompressThreshold()}`);
731
+ this.log(this._forceCompact
732
+ ? `Compacting messages (manual /compact request): ${contextTokens} tokens`
733
+ : `Compressing messages: ${contextTokens} tokens exceeds ${this.getCompressThreshold()}`);
734
+ this._forceCompact = false;
717
735
  messages = await this.compressMessages(messages, startIndex, endIndex);
718
736
  this.startNewThread(messages);
737
+ this.lastPromptTokens = 0;
719
738
  }
720
739
  if (["assistant", "tool"].includes(messages[messages.length - 1].role)) {
721
740
  const statusMessage = this.getStatusMessage();
@@ -836,9 +855,85 @@ class BaseAgent {
836
855
  }
837
856
  this.events.emit(this.eventTypes.userSay, message.content);
838
857
  }
858
+ observe(source, opts = {}) {
859
+ const { onlyOnChange = true, maxUpdates = 20, maxDurationMs = 10 * 60 * 1000, } = opts;
860
+ this._observationSeq += 1;
861
+ const safeLabel = source.label.replace(/[^A-Za-z0-9]+/g, "_").slice(0, 24);
862
+ const id = `obs_${safeLabel}_${Date.now()}_${this._observationSeq}`;
863
+ let last;
864
+ const emit = (datum) => {
865
+ if (this.status === this.eventTypes.done) {
866
+ this.stopObserving(id);
867
+ return;
868
+ }
869
+ const str = typeof datum === "string" ? datum : JSON.stringify(datum);
870
+ if (onlyOnChange && str === last)
871
+ return;
872
+ last = str;
873
+ const obs = this._observations.get(id);
874
+ if (!obs)
875
+ return;
876
+ obs.updates += 1;
877
+ this.addPendingMessage({
878
+ role: "user",
879
+ content: `[observe:${id}] ${source.label} (update ${obs.updates}/${maxUpdates}):\n${str}`,
880
+ });
881
+ if (obs.updates >= maxUpdates) {
882
+ this.stopObserving(id, "max updates reached");
883
+ }
884
+ };
885
+ const teardownPromise = Promise.resolve(source.start(emit)).catch((e) => {
886
+ this.log(`observe(${source.label}) start failed: ${e?.message ?? e}`, "warn");
887
+ return undefined;
888
+ });
889
+ const expiry = maxDurationMs
890
+ ? setTimeout(() => this.stopObserving(id, "max duration reached"), maxDurationMs)
891
+ : undefined;
892
+ if (expiry && typeof expiry.unref === "function") {
893
+ expiry.unref();
894
+ }
895
+ this._observations.set(id, {
896
+ id,
897
+ label: source.label,
898
+ updates: 0,
899
+ teardownPromise,
900
+ expiry,
901
+ });
902
+ return id;
903
+ }
904
+ stopObserving(id, reason = "stopped") {
905
+ const ids = id ? [id] : [...this._observations.keys()];
906
+ let stopped = 0;
907
+ for (const oid of ids) {
908
+ const obs = this._observations.get(oid);
909
+ if (!obs)
910
+ continue;
911
+ if (obs.expiry)
912
+ clearTimeout(obs.expiry);
913
+ Promise.resolve(obs.teardownPromise)
914
+ .then((fn) => {
915
+ if (typeof fn === "function")
916
+ fn();
917
+ })
918
+ .catch(() => { });
919
+ this._observations.delete(oid);
920
+ stopped += 1;
921
+ }
922
+ return id
923
+ ? stopped
924
+ ? `Stopped observer ${id} (${reason}).`
925
+ : `No active observer found with id: ${id}.`
926
+ : `Stopped ${stopped} active observer(s) (${reason}).`;
927
+ }
839
928
  getMessagesLength(messages) {
840
929
  return JSON.stringify(messages).split(" ").length;
841
930
  }
931
+ getContextTokenCount(messages) {
932
+ if (this.lastPromptTokens > 0) {
933
+ return this.lastPromptTokens;
934
+ }
935
+ return this.getMessagesLength(messages);
936
+ }
842
937
  detectTruncatedToolCalls(toolCalls, response) {
843
938
  const rawOutputTokens = response?.usage?.completion_tokens || 0;
844
939
  const thinkingTokens = (response?.usage?.output_tokens_details?.thinking_tokens ?? 0) +