@tyvm/knowhow 0.0.153 → 0.0.155

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 (85) hide show
  1. package/package.json +1 -1
  2. package/src/agents/base/base.ts +72 -28
  3. package/src/agents/base/prompt.ts +3 -2
  4. package/src/agents/tools/list.ts +31 -28
  5. package/src/auth/keyAuth.ts +35 -3
  6. package/src/chat/modules/AgentModule.ts +98 -42
  7. package/src/clients/anthropic.ts +3 -1
  8. package/src/clients/contextLimits.ts +3 -1
  9. package/src/clients/gemini.ts +16 -4
  10. package/src/clients/http.ts +8 -5
  11. package/src/clients/index.ts +7 -4
  12. package/src/clients/knowhowMcp.ts +2 -2
  13. package/src/clients/openai.ts +44 -8
  14. package/src/clients/pricing/google.ts +11 -2
  15. package/src/clients/pricing/xai.ts +5 -1
  16. package/src/clients/xai.ts +6 -3
  17. package/src/services/KnowhowClient.ts +6 -5
  18. package/src/services/watchers/FsSyncer.ts +40 -2
  19. package/src/services/watchers/RemoteSyncer.ts +40 -2
  20. package/src/tunnel.ts +18 -12
  21. package/src/worker.ts +62 -12
  22. package/tests/clients/reasoningEffort.test.ts +62 -1
  23. package/tests/unit/auth/keyAuth.test.ts +31 -2
  24. package/tests/unit/services/KnowhowClient.test.ts +84 -0
  25. package/tests/unit/services/SyncedAgentWatcher.test.ts +89 -0
  26. package/ts_build/package.json +1 -1
  27. package/ts_build/src/agents/base/base.d.ts +2 -1
  28. package/ts_build/src/agents/base/base.js +39 -26
  29. package/ts_build/src/agents/base/base.js.map +1 -1
  30. package/ts_build/src/agents/base/prompt.d.ts +1 -1
  31. package/ts_build/src/agents/base/prompt.js +3 -2
  32. package/ts_build/src/agents/base/prompt.js.map +1 -1
  33. package/ts_build/src/agents/tools/list.js +0 -26
  34. package/ts_build/src/agents/tools/list.js.map +1 -1
  35. package/ts_build/src/auth/keyAuth.d.ts +1 -0
  36. package/ts_build/src/auth/keyAuth.js +19 -2
  37. package/ts_build/src/auth/keyAuth.js.map +1 -1
  38. package/ts_build/src/chat/modules/AgentModule.d.ts +3 -0
  39. package/ts_build/src/chat/modules/AgentModule.js +74 -34
  40. package/ts_build/src/chat/modules/AgentModule.js.map +1 -1
  41. package/ts_build/src/clients/anthropic.js +3 -1
  42. package/ts_build/src/clients/anthropic.js.map +1 -1
  43. package/ts_build/src/clients/contextLimits.js +2 -0
  44. package/ts_build/src/clients/contextLimits.js.map +1 -1
  45. package/ts_build/src/clients/gemini.js +14 -3
  46. package/ts_build/src/clients/gemini.js.map +1 -1
  47. package/ts_build/src/clients/http.js +8 -5
  48. package/ts_build/src/clients/http.js.map +1 -1
  49. package/ts_build/src/clients/index.js +7 -4
  50. package/ts_build/src/clients/index.js.map +1 -1
  51. package/ts_build/src/clients/knowhowMcp.js +2 -2
  52. package/ts_build/src/clients/knowhowMcp.js.map +1 -1
  53. package/ts_build/src/clients/openai.js +36 -8
  54. package/ts_build/src/clients/openai.js.map +1 -1
  55. package/ts_build/src/clients/pricing/google.d.ts +1 -0
  56. package/ts_build/src/clients/pricing/google.js +7 -0
  57. package/ts_build/src/clients/pricing/google.js.map +1 -1
  58. package/ts_build/src/clients/pricing/xai.d.ts +1 -0
  59. package/ts_build/src/clients/pricing/xai.js +4 -0
  60. package/ts_build/src/clients/pricing/xai.js.map +1 -1
  61. package/ts_build/src/clients/xai.js +6 -3
  62. package/ts_build/src/clients/xai.js.map +1 -1
  63. package/ts_build/src/services/KnowhowClient.js +5 -3
  64. package/ts_build/src/services/KnowhowClient.js.map +1 -1
  65. package/ts_build/src/services/watchers/FsSyncer.d.ts +4 -0
  66. package/ts_build/src/services/watchers/FsSyncer.js +36 -2
  67. package/ts_build/src/services/watchers/FsSyncer.js.map +1 -1
  68. package/ts_build/src/services/watchers/RemoteSyncer.d.ts +4 -0
  69. package/ts_build/src/services/watchers/RemoteSyncer.js +36 -2
  70. package/ts_build/src/services/watchers/RemoteSyncer.js.map +1 -1
  71. package/ts_build/src/tunnel.js +11 -8
  72. package/ts_build/src/tunnel.js.map +1 -1
  73. package/ts_build/src/types.d.ts +2 -0
  74. package/ts_build/src/worker.js +47 -10
  75. package/ts_build/src/worker.js.map +1 -1
  76. package/ts_build/tests/clients/reasoningEffort.test.js +49 -1
  77. package/ts_build/tests/clients/reasoningEffort.test.js.map +1 -1
  78. package/ts_build/tests/unit/auth/keyAuth.test.js +22 -2
  79. package/ts_build/tests/unit/auth/keyAuth.test.js.map +1 -1
  80. package/ts_build/tests/unit/services/KnowhowClient.test.d.ts +1 -0
  81. package/ts_build/tests/unit/services/KnowhowClient.test.js +69 -0
  82. package/ts_build/tests/unit/services/KnowhowClient.test.js.map +1 -0
  83. package/ts_build/tests/unit/services/SyncedAgentWatcher.test.d.ts +1 -0
  84. package/ts_build/tests/unit/services/SyncedAgentWatcher.test.js +78 -0
  85. package/ts_build/tests/unit/services/SyncedAgentWatcher.test.js.map +1 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tyvm/knowhow",
3
- "version": "0.0.153",
3
+ "version": "0.0.155",
4
4
  "description": "ai cli with plugins and agents",
5
5
  "main": "ts_build/src/index.js",
6
6
  "bin": {
@@ -1107,6 +1107,14 @@ export abstract class BaseAgent implements IAgent {
1107
1107
 
1108
1108
  // Typically, there's only one choice in the array, but you could have many
1109
1109
  // If you set `n` to more than 1, you will get multiple choices
1110
+ // Collect all tool calls across all choices up front.
1111
+ // This is used by detectTruncatedToolCalls so that the size heuristic
1112
+ // compares the full completion_tokens against ALL argument content,
1113
+ // rather than just the current choice's single tool call (Anthropic maps
1114
+ // each parallel tool_use block to a separate choice).
1115
+ const allResponseToolCalls = response.choices.flatMap(
1116
+ (c) => c.message.tool_calls ?? []
1117
+ );
1110
1118
  for (const choice of response.choices) {
1111
1119
  messages.push(choice.message);
1112
1120
 
@@ -1136,7 +1144,8 @@ export abstract class BaseAgent implements IAgent {
1136
1144
 
1137
1145
  const truncationWarning = this.detectTruncatedToolCalls(
1138
1146
  toolCalls,
1139
- response
1147
+ response,
1148
+ allResponseToolCalls
1140
1149
  );
1141
1150
  if (truncationWarning) {
1142
1151
  messages.push(truncationWarning as Message);
@@ -1589,11 +1598,14 @@ export abstract class BaseAgent implements IAgent {
1589
1598
  * 2. The model reported many output tokens but the total argument content received is tiny
1590
1599
  * relative to what those tokens should represent (soft/silent truncation).
1591
1600
  *
1601
+ * @param toolCalls - The tool calls from the CURRENT choice being checked.
1602
+ * @param allToolCalls - ALL tool calls across ALL choices in the response (used for the size heuristic).
1592
1603
  * Returns a warning system message if truncation is detected, or null otherwise.
1593
1604
  */
1594
1605
  detectTruncatedToolCalls(
1595
1606
  toolCalls: ToolCall[],
1596
- response: CompletionResponse
1607
+ response: CompletionResponse,
1608
+ allToolCalls?: ToolCall[]
1597
1609
  ): { role: string; content: string } | null {
1598
1610
  // Subtract thinking/reasoning tokens — they're billed as output tokens but
1599
1611
  // don't produce visible argument content, so including them inflates the heuristic.
@@ -1602,7 +1614,13 @@ export abstract class BaseAgent implements IAgent {
1602
1614
  (response?.usage?.output_tokens_details?.thinking_tokens ?? 0) +
1603
1615
  (response?.usage?.output_tokens_details?.reasoning_tokens ?? 0);
1604
1616
  const outputTokens = Math.max(0, rawOutputTokens - thinkingTokens);
1605
- const totalArgLength = toolCalls.reduce(
1617
+ // Use allToolCalls for the size heuristic so that parallel tool calls from
1618
+ // Anthropic (each mapped to a separate choice) are counted together. Without
1619
+ // this, a response with N parallel tool calls would compare the full
1620
+ // completion_tokens count against only 1/N of the actual argument content,
1621
+ // causing false-positive truncation warnings.
1622
+ const callsForSizeCheck = allToolCalls ?? toolCalls;
1623
+ const totalArgLength = callsForSizeCheck.reduce(
1606
1624
  (sum, tc) => sum + (tc.function?.arguments?.length || 0),
1607
1625
  0
1608
1626
  );
@@ -1642,6 +1660,20 @@ export abstract class BaseAgent implements IAgent {
1642
1660
  return null;
1643
1661
  }
1644
1662
 
1663
+ extractContentFromMessages(messages: Message[]): string {
1664
+ const content = messages
1665
+ .filter((m) => typeof m.content === "string" && m.content.trim())
1666
+ .map((m) => m.content);
1667
+
1668
+ const finalAnswer = messages
1669
+ .filter((m) => m.tool_calls?.length)
1670
+ .flatMap((t) => t.tool_calls)
1671
+ .filter((t) => t.function.name === "finalAnswer")
1672
+ .map((t) => t.function.arguments);
1673
+
1674
+ return [...content, ...finalAnswer].join("\n\n");
1675
+ }
1676
+
1645
1677
  async getTaskBreakdown(messages: Message[]) {
1646
1678
  if (this.taskBreakdown) {
1647
1679
  return this.taskBreakdown;
@@ -1668,29 +1700,26 @@ export abstract class BaseAgent implements IAgent {
1668
1700
  ] as Message[];
1669
1701
 
1670
1702
  const response = await this.createAgentCompletion({
1671
- model,
1672
1703
  messages: taskBreakdownMessages,
1673
1704
  tool_choice: "none",
1674
- long_ttl_cache: this.runTime() > 300_000,
1675
- ...(this.reasoningEffort !== undefined && {
1676
- reasoning_effort: this.reasoningEffort,
1677
- }),
1678
- ...(this.summarizeReasoning !== undefined && {
1679
- reasoning_summary: this.summarizeReasoning,
1680
- }),
1681
1705
  });
1682
1706
 
1707
+ const breakdownContent = this.extractContentFromMessages(
1708
+ response.choices?.map((c) => c.message)
1709
+ );
1683
1710
  // The model may return null content (e.g. when it responds with tool calls
1684
1711
  // instead of text). Guard against storing a literal null so that template
1685
1712
  // strings don't render "null".
1686
- const breakdownContent = response.choices
1687
- .map((c) => c.message.content)
1688
- .filter((s): s is string => typeof s === "string" && s.trim().length > 0)
1689
- .join("\n\n");
1713
+ if (!breakdownContent) {
1714
+ console.log(JSON.stringify(response.choices, null, 2));
1715
+ throw new Error(
1716
+ "Compaction task-breakdown request returned no text content"
1717
+ );
1718
+ }
1690
1719
 
1691
- this.taskBreakdown = breakdownContent || "";
1720
+ this.taskBreakdown = breakdownContent;
1692
1721
  this.log(`task breakdown cost: ${response.usd_cost}`);
1693
- return this.taskBreakdown || undefined;
1722
+ return this.taskBreakdown;
1694
1723
  }
1695
1724
 
1696
1725
  async compressMessages(messages: Message[]) {
@@ -1698,10 +1727,7 @@ export abstract class BaseAgent implements IAgent {
1698
1727
  // at the final assistant message keeps its tool calls, every tool response,
1699
1728
  // and any subsequent user messages together as one protocol-valid unit.
1700
1729
  let resumeIndex = messages.length - 1;
1701
- while (
1702
- resumeIndex >= 0 &&
1703
- messages[resumeIndex].role !== "assistant"
1704
- ) {
1730
+ while (resumeIndex >= 0 && messages[resumeIndex].role !== "assistant") {
1705
1731
  resumeIndex--;
1706
1732
  }
1707
1733
  const compressionEnd = resumeIndex === -1 ? messages.length : resumeIndex;
@@ -1720,7 +1746,11 @@ export abstract class BaseAgent implements IAgent {
1720
1746
  3. Next Steps - what we're about to do next to continue the user's original request.
1721
1747
  4. Tasks remaining - what tasks are left from the initial task breakdown.
1722
1748
 
1723
- ${this.taskBreakdown ? `Our initial task breakdown: ${this.taskBreakdown}` : ""}
1749
+ ${
1750
+ this.taskBreakdown
1751
+ ? `Our initial task breakdown: ${this.taskBreakdown}`
1752
+ : ""
1753
+ }
1724
1754
 
1725
1755
  This summary will replace the older history. The latest agent interaction will remain verbatim:
1726
1756
 
@@ -1739,19 +1769,33 @@ export abstract class BaseAgent implements IAgent {
1739
1769
  tool_choice: "none",
1740
1770
  });
1741
1771
 
1742
- const summaries = response.choices
1743
- .map((c) => c.message.content)
1744
- .filter((s): s is string => typeof s === "string" && s.trim().length > 0);
1745
- this.summaries.push(...summaries);
1772
+ const summary = this.extractContentFromMessages(
1773
+ response.choices?.map((c) => c.message)
1774
+ );
1775
+
1776
+ if (summary.length === 0) {
1777
+ console.log(JSON.stringify(response.choices, null, 2));
1778
+ throw new Error("Compaction summary request returned no text content");
1779
+ }
1780
+
1781
+ this.summaries.push(summary);
1746
1782
 
1747
1783
  const startMessages = [
1748
1784
  {
1749
1785
  role: "user",
1750
1786
  content: `
1751
- ${this.taskBreakdown ? `Initial task breakdown:\n ${this.taskBreakdown}` : "(No task breakdown available — summarize what you know from context above)"}
1787
+ ${
1788
+ this.taskBreakdown
1789
+ ? `Initial task breakdown:\n ${this.taskBreakdown}`
1790
+ : "(No task breakdown available — summarize what you know from context above)"
1791
+ }
1752
1792
 
1753
1793
  We have just compressed the conversation to save memory:
1754
- ${summaries.length > 0 ? JSON.stringify(summaries) : "(summary unavailable — please continue from the task breakdown above)"}
1794
+ ${
1795
+ summary.length > 0
1796
+ ? JSON.stringify(summary)
1797
+ : "(summary unavailable — please continue from the task breakdown above)"
1798
+ }
1755
1799
 
1756
1800
  Please continue the task from where we left off
1757
1801
  `,
@@ -4,8 +4,9 @@ You use the tools to read and write code, to help the developer implement featur
4
4
 
5
5
  # Completing your work
6
6
 
7
- You always call finalAnswer once you have finished implementing or answering what is requested.
8
- As an agent you will receive multiple rounds of input until you call finalAnswer.
7
+ You may answer questions and provide requested information directly in your assistant message content. Content responses are valid and visible to the user; you do not need to use a tool merely to answer a question.
8
+ Call finalAnswer only once the overall task is complete and you are ready to end the agent run. Put your final result or completion summary in that tool call.
9
+ As an agent you may receive multiple rounds of input before you call finalAnswer.
9
10
  You are not able to request feedback from the user, so proceed with your plans and the developer will contact you afterwards if they need more help.
10
11
 
11
12
  ## Modification Guidelines
@@ -31,34 +31,37 @@ function getPluginNames(): string {
31
31
  }
32
32
 
33
33
  export const includedTools = [
34
- {
35
- type: "function",
36
- function: {
37
- name: "agentCall",
38
- description:
39
- "Run another registered agent in-process as a blocking call and get its final answer back as a string. Unlike a plain llm() call, the target agent can use tools, loop, and maintain its own context. Useful as a graph node that delegates to a specialized agent (e.g. Researcher, Patcher). For long-running / parallel / detached work prefer startAgentTask.",
40
- parameters: {
41
- type: "object",
42
- positional: true,
43
- properties: {
44
- agentName: {
45
- type: "string",
46
- description:
47
- "The name of the agent to call (e.g. Researcher, Developer, Patcher).",
48
- },
49
- query: {
50
- type: "string",
51
- description: "The query / task to send to the agent.",
52
- },
53
- },
54
- required: ["agentName", "query"],
55
- },
56
- returns: {
57
- type: "string",
58
- description: "The agent's final answer text.",
59
- },
60
- },
61
- },
34
+ /*
35
+ * This doesn't use messageProcessors etc, needs rewrite
36
+ *{
37
+ * type: "function",
38
+ * function: {
39
+ * name: "agentCall",
40
+ * description:
41
+ * "Run another registered agent in-process as a blocking call and get its final answer back as a string. Unlike a plain llm() call, the target agent can use tools, loop, and maintain its own context. Useful as a graph node that delegates to a specialized agent (e.g. Researcher, Patcher). For long-running / parallel / detached work prefer startAgentTask.",
42
+ * parameters: {
43
+ * type: "object",
44
+ * positional: true,
45
+ * properties: {
46
+ * agentName: {
47
+ * type: "string",
48
+ * description:
49
+ * "The name of the agent to call (e.g. Researcher, Developer, Patcher).",
50
+ * },
51
+ * query: {
52
+ * type: "string",
53
+ * description: "The query / task to send to the agent.",
54
+ * },
55
+ * },
56
+ * required: ["agentName", "query"],
57
+ * },
58
+ * returns: {
59
+ * type: "string",
60
+ * description: "The agent's final answer text.",
61
+ * },
62
+ * },
63
+ *},
64
+ */
62
65
  {
63
66
  type: "function",
64
67
  function: {
@@ -3,7 +3,12 @@ import os from "os";
3
3
  import path from "path";
4
4
  import http from "../utils/http";
5
5
  import { getCliUserAgent } from "./browserLogin";
6
- import { keyPairExists, loadKeyPair, signMessage } from "./keyManager";
6
+ import {
7
+ getDefaultPrivateKeyPath,
8
+ keyPairExists,
9
+ loadKeyPair,
10
+ signMessage,
11
+ } from "./keyManager";
7
12
 
8
13
  const JWT_FILE_PATH = path.join(process.cwd(), ".knowhow", ".jwt");
9
14
  const DEFAULT_API_URL = process.env.KNOWHOW_API_URL || "https://api.knowhow.tyvm.ai";
@@ -62,15 +67,42 @@ export async function exchangePublicKeyJwt(
62
67
  return response.data.jwt;
63
68
  }
64
69
 
70
+ /**
71
+ * Try the configured identity first, then the default CLI identity. Project
72
+ * configs can outlive temporary/custom keys, while the globally registered
73
+ * default identity remains valid.
74
+ */
75
+ export async function exchangeAvailablePublicKeyJwt(
76
+ orgId: string,
77
+ apiUrl: string = DEFAULT_API_URL,
78
+ preferredPrivateKeyPath?: string
79
+ ): Promise<string> {
80
+ const defaultPath = getDefaultPrivateKeyPath();
81
+ const candidates = Array.from(
82
+ new Set([preferredPrivateKeyPath, defaultPath].filter((value): value is string => Boolean(value)))
83
+ ).filter((candidate) => keyPairExists(candidate));
84
+ if (candidates.length === 0) throw new Error("CLI identity is unavailable");
85
+
86
+ let lastError: unknown;
87
+ for (const candidate of candidates) {
88
+ try {
89
+ return await exchangePublicKeyJwt(orgId, apiUrl, candidate);
90
+ } catch (error) {
91
+ lastError = error;
92
+ }
93
+ }
94
+ throw lastError;
95
+ }
96
+
65
97
  export async function authenticateWithKey(
66
98
  orgId: string,
67
99
  apiUrl: string = DEFAULT_API_URL,
68
100
  privateKeyPath?: string
69
101
  ): Promise<boolean> {
70
- if (!keyPairExists(privateKeyPath)) return false;
102
+ if (!keyPairExists(privateKeyPath) && !keyPairExists()) return false;
71
103
 
72
104
  try {
73
- storeJwt(await exchangePublicKeyJwt(orgId, apiUrl, privateKeyPath));
105
+ storeJwt(await exchangeAvailablePublicKeyJwt(orgId, apiUrl, privateKeyPath));
74
106
  return true;
75
107
  } catch (error: unknown) {
76
108
  // The identity is not registered for this environment/org (or was revoked).
@@ -67,6 +67,9 @@ export class AgentModule extends BaseChatModule {
67
67
  /** Whether to request a reasoning summary from supporting models */
68
68
  private summarizeReasoning: boolean = true;
69
69
 
70
+ /** Delayed finalAnswer output, cancelled when the task's done event arrives. */
71
+ private pendingFinalAnswers = new Map<string, ReturnType<typeof setTimeout>>();
72
+
70
73
  /** Stored wire params for rewireAgentRendering */
71
74
  private _wireAgentEvents: EventService | undefined;
72
75
  private _wireTaskId: string | undefined;
@@ -514,6 +517,28 @@ export class AgentModule extends BaseChatModule {
514
517
  this.activeAgentTaskId = id;
515
518
  }
516
519
 
520
+ private scheduleFinalAnswer(taskId: string, answer: unknown): void {
521
+ this.cancelFinalAnswer(taskId);
522
+ const renderedAnswer = typeof answer === "string"
523
+ ? answer
524
+ : JSON.stringify(answer) ?? String(answer);
525
+ const timeout = setTimeout(() => {
526
+ this.pendingFinalAnswers.delete(taskId);
527
+ console.log(Marked.parse(renderedAnswer));
528
+ }, 50);
529
+ this.pendingFinalAnswers.set(taskId, timeout);
530
+ }
531
+
532
+ private cancelFinalAnswer(taskId: string): void {
533
+ const timeout = this.pendingFinalAnswers.get(taskId);
534
+ if (!timeout) {
535
+ return;
536
+ }
537
+
538
+ clearTimeout(timeout);
539
+ this.pendingFinalAnswers.delete(taskId);
540
+ }
541
+
517
542
  /**
518
543
  * Wire up agentEvents to the renderer for a given task.
519
544
  * Uses setListener with stable keys so re-calling automatically replaces old listeners.
@@ -549,16 +574,27 @@ export class AgentModule extends BaseChatModule {
549
574
  if (eventTypes.toolUsed) {
550
575
  agentEvents.setListener(
551
576
  { key: "agentModule:render:toolUsed", event: eventTypes.toolUsed },
552
- (data: any) =>
577
+ (data: any) => {
553
578
  this.renderer.render({
554
579
  type: "toolResult",
555
580
  taskId,
556
581
  agentName,
557
582
  toolCall: data.toolCall,
558
583
  result: data.functionResp,
559
- })
584
+ });
585
+ // Give the done event a chance to render the same answer first. If the
586
+ // agent continues, the tool result becomes the fallback output.
587
+ const toolName: string = data.toolCall?.function?.name ?? "";
588
+ if (toolName === "finalAnswer" || toolName.endsWith("finalAnswer")) {
589
+ this.scheduleFinalAnswer(taskId, data.functionResp);
590
+ }
591
+ }
560
592
  );
561
593
  }
594
+ agentEvents.setListener(
595
+ { key: "agentModule:render:done", event: eventTypes.done },
596
+ () => this.cancelFinalAnswer(taskId)
597
+ );
562
598
  if (eventTypes.agentSay) {
563
599
  agentEvents.setListener(
564
600
  { key: "agentModule:render:agentSay", event: eventTypes.agentSay },
@@ -589,6 +625,9 @@ export class AgentModule extends BaseChatModule {
589
625
  * Clears the active task on the renderer.
590
626
  */
591
627
  public unwireAgentRendering(): void {
628
+ if (this._wireTaskId) {
629
+ this.cancelFinalAnswer(this._wireTaskId);
630
+ }
592
631
  if (this._wireAgentEvents) {
593
632
  this._wireAgentEvents.removeManagedListenersByPrefix(
594
633
  "agentModule:render:"
@@ -681,8 +720,14 @@ export class AgentModule extends BaseChatModule {
681
720
  console.error(`Session ${sessionId} not found.`);
682
721
  return;
683
722
  }
723
+ // Session files from older runs may not contain model/provider settings.
724
+ // Prefer task metadata, which is updated from the live agent on every
725
+ // thread update and is therefore the authoritative resume configuration.
726
+ const savedSettings = await this.loadTaskSettings(
727
+ session.taskId || sessionId
728
+ );
684
729
  console.log(`\n🔄 Resuming session: ${sessionId}`);
685
- console.log(`Agent: ${session.agentName}`);
730
+ console.log(`Agent: ${savedSettings.agentName || session.agentName}`);
686
731
  console.log(`Original task: ${session.initialInput}`);
687
732
  console.log(`Status: ${session.status}`);
688
733
 
@@ -726,7 +771,8 @@ export class AgentModule extends BaseChatModule {
726
771
 
727
772
  console.log("🚀 Session resuming...");
728
773
  const context = this.chatService?.getContext();
729
- const agentName = session.agentName || context.currentAgent;
774
+ const agentName =
775
+ savedSettings.agentName || session.agentName || context.currentAgent;
730
776
  const previousAgentMode = context?.agentMode;
731
777
 
732
778
  if (!agentName) {
@@ -738,7 +784,9 @@ export class AgentModule extends BaseChatModule {
738
784
  // not reject config-defined agents here before it gets that opportunity.
739
785
 
740
786
  // Start agent with Knowhow task context and restored message history
741
- const remoteTaskId = asRemoteTaskId(session.knowhowTaskId);
787
+ const remoteTaskId = asRemoteTaskId(
788
+ savedSettings.remoteTaskId || session.knowhowTaskId
789
+ );
742
790
  const { agent, taskId } = await this.setupAgent({
743
791
  agentName,
744
792
  input: resumePrompt,
@@ -751,12 +799,12 @@ export class AgentModule extends BaseChatModule {
751
799
  taskId: session.taskId || sessionId,
752
800
  chatHistory: [],
753
801
  run: false, // Don't run yet, we need to set up event listeners first
754
- // Restore the exact model/provider/reasoning the original run used.
755
- model: session.model,
756
- provider: session.provider,
757
- reasoningEffort: session.reasoningEffort,
758
- summarizeReasoning: session.summarizeReasoning,
759
- enabledTools: session.enabledTools,
802
+ // Prefer authoritative task metadata over potentially incomplete sessions.
803
+ model: savedSettings.model || session.model,
804
+ provider: savedSettings.provider || session.provider,
805
+ reasoningEffort: savedSettings.reasoningEffort ?? session.reasoningEffort,
806
+ summarizeReasoning: savedSettings.summarizeReasoning ?? session.summarizeReasoning,
807
+ enabledTools: savedSettings.enabledTools || session.enabledTools,
760
808
  });
761
809
 
762
810
  // Match new interactive threads: local-only sessions get a fresh remote
@@ -871,6 +919,34 @@ export class AgentModule extends BaseChatModule {
871
919
  agent.restoreEnabledTools(options.enabledTools);
872
920
  }
873
921
 
922
+ // Restore persisted configuration before registering or saving this task.
923
+ // Otherwise the newly constructed agent's defaults can overwrite the
924
+ // original run settings during resume setup.
925
+ if (options.model) {
926
+ console.log("Setting model:", options.model);
927
+ agent.setModel(options.model);
928
+ agent.setModelPreferences([
929
+ { model: options.model, provider: options.provider as any },
930
+ ]);
931
+ }
932
+
933
+ if (options.provider) {
934
+ agent.setProvider(options.provider as any);
935
+ }
936
+
937
+ const effectiveReasoningEffort =
938
+ options.reasoningEffort !== undefined
939
+ ? options.reasoningEffort
940
+ : this.reasoningEffort;
941
+ if (effectiveReasoningEffort !== undefined) {
942
+ agent.setReasoningEffort(effectiveReasoningEffort);
943
+ }
944
+ agent.setSummarizeReasoning(
945
+ options.summarizeReasoning !== undefined
946
+ ? options.summarizeReasoning
947
+ : this.summarizeReasoning
948
+ );
949
+
874
950
  let done = false;
875
951
  let output = "Done";
876
952
  const taskId = options.taskId || this.sessionManager.generateTaskId(input);
@@ -907,6 +983,10 @@ export class AgentModule extends BaseChatModule {
907
983
  startTime: Date.now(),
908
984
  totalCost: 0,
909
985
  enabledTools: agent.getEnabledToolNames(),
986
+ model: agent.getModel(),
987
+ provider: agent.getProvider(),
988
+ reasoningEffort: agent.getReasoningEffort?.(),
989
+ summarizeReasoning: agent.getSummarizeReasoning?.(),
910
990
  };
911
991
 
912
992
  // Add to task registry
@@ -967,37 +1047,6 @@ export class AgentModule extends BaseChatModule {
967
1047
  agent.setParentTaskId(options.parentTaskId);
968
1048
  }
969
1049
 
970
- if (options.model) {
971
- console.log("Setting model:", options.model);
972
- agent.setModel(options.model);
973
- agent.setModelPreferences([
974
- { model: options.model, provider: options.provider as any },
975
- ]);
976
- }
977
-
978
- // Restore the provider explicitly (e.g. on resume) so the agent talks to
979
- // the same client the original run used, not the model's default provider.
980
- if (options.provider) {
981
- agent.setProvider(options.provider as any);
982
- }
983
-
984
- // Apply reasoning settings to the agent. Explicit per-task settings
985
- // (e.g. restored from a resumed task's metadata) take precedence over
986
- // the module-level CLI defaults so a /resume keeps the same reasoning
987
- // effort the original run used.
988
- const effectiveReasoningEffort =
989
- options.reasoningEffort !== undefined
990
- ? options.reasoningEffort
991
- : this.reasoningEffort;
992
- if (effectiveReasoningEffort !== undefined) {
993
- agent.setReasoningEffort(effectiveReasoningEffort);
994
- }
995
- agent.setSummarizeReasoning(
996
- options.summarizeReasoning !== undefined
997
- ? options.summarizeReasoning
998
- : this.summarizeReasoning
999
- );
1000
-
1001
1050
  // Set up message processors like in original startAgent
1002
1051
 
1003
1052
  // Register an override for askHuman to use CLI input method
@@ -1097,6 +1146,12 @@ export class AgentModule extends BaseChatModule {
1097
1146
  toolCall: data.toolCall,
1098
1147
  result: data.functionResp,
1099
1148
  });
1149
+ // Give the done event a chance to render the same answer first. If the
1150
+ // agent continues, the tool result becomes the fallback output.
1151
+ const toolName: string = data.toolCall?.function?.name ?? "";
1152
+ if (toolName === "finalAnswer" || toolName.endsWith("finalAnswer")) {
1153
+ this.scheduleFinalAnswer(taskId, data.functionResp);
1154
+ }
1100
1155
  };
1101
1156
  const agentSayHandler = (data: any) => {
1102
1157
  this.renderer.render({
@@ -1130,6 +1185,7 @@ export class AgentModule extends BaseChatModule {
1130
1185
  const taskCompleted = new Promise<string>((resolve) => {
1131
1186
  agent.agentEvents.once(agent.eventTypes.done, async (doneMsg) => {
1132
1187
  console.log("🎯 [AgentModule] Task Completed");
1188
+ this.cancelFinalAnswer(taskId);
1133
1189
  done = true;
1134
1190
  output = doneMsg || "No response from the AI";
1135
1191
  // Remove threadUpdate listener to prevent cost sharing across tasks
@@ -472,7 +472,9 @@ export class GenericAnthropicClient implements GenericClient {
472
472
  ...(thinkingParam && { thinking: thinkingParam }),
473
473
  ...(effortOutputConfig && { output_config: effortOutputConfig }),
474
474
  ...(tools.length && {
475
- tool_choice: { type: "auto" },
475
+ tool_choice: {
476
+ type: options.tool_choice ?? "auto",
477
+ },
476
478
  tools,
477
479
  }),
478
480
  }, { signal: options.signal });
@@ -123,7 +123,8 @@ export const ContextLimits: Record<string, number> = {
123
123
  [Models.anthropic.Sonnet4_0]: 200_000,
124
124
 
125
125
  // ─── Google ───────────────────────────────────────────────────────────────
126
- // Gemini 3.6 / 3.5 (text)
126
+ // Gemini 3.7 / 3.6 / 3.5 (text)
127
+ [Models.google.Gemini_37_Flash]: 1_048_576,
127
128
  [Models.google.Gemini_36_Flash]: 1_000_000,
128
129
  [Models.google.Gemini_35_Flash]: 1_000_000,
129
130
  [Models.google.Gemini_35_Flash_Lite]: 1_000_000,
@@ -178,6 +179,7 @@ export const ContextLimits: Record<string, number> = {
178
179
  [Models.google.Gemini_15_Pro]: 2_000_000,
179
180
 
180
181
  // ─── xAI ──────────────────────────────────────────────────────────────────
182
+ [Models.xai.Grok4_6]: 500_000,
181
183
  [Models.xai.Grok4_1_Fast_Reasoning]: 2_000_000,
182
184
  [Models.xai.Grok4_1_Fast_NonReasoning]: 2_000_000,
183
185
  [Models.xai.Grok_4_20_Reasoning]: 131_072,
@@ -420,7 +420,7 @@ export class GenericGeminiClient implements GenericClient {
420
420
 
421
421
  /**
422
422
  * Builds the thinkingConfig for Gemini models that support it.
423
- * - Gemini 3.x models use thinkingLevel: "minimal" | "low" | "medium" | "high"
423
+ * - Gemini 3.x models use thinkingLevel (supported levels vary by model)
424
424
  * - Gemini 2.5 models use thinkingBudget: number (0 = off, -1 = dynamic)
425
425
  *
426
426
  * Maps CompletionOptions.reasoning_effort to provider-specific values.
@@ -433,9 +433,11 @@ export class GenericGeminiClient implements GenericClient {
433
433
 
434
434
  // Gemini 3.x — use thinkingLevel
435
435
  if (GoogleThinkingLevelModels.includes(model)) {
436
+ // Gemini 3.7 Flash rejects "minimal"; low is its least intensive level.
437
+ const minimumLevel = model === Models.google.Gemini_37_Flash ? "low" : "minimal";
436
438
  const levelMap: Record<string, string> = {
437
- none: "minimal",
438
- minimal: "minimal",
439
+ none: minimumLevel,
440
+ minimal: minimumLevel,
439
441
  low: "low",
440
442
  medium: "medium",
441
443
  high: "high",
@@ -474,6 +476,7 @@ export class GenericGeminiClient implements GenericClient {
474
476
  const { systemInstruction, contents } = this.transformMessages(
475
477
  options.messages
476
478
  );
479
+ const tools = this.transformTools(options.tools);
477
480
 
478
481
  try {
479
482
  await wait(2000);
@@ -483,7 +486,16 @@ export class GenericGeminiClient implements GenericClient {
483
486
  config: {
484
487
  systemInstruction,
485
488
  thinkingConfig,
486
- tools: this.transformTools(options.tools),
489
+ tools,
490
+ ...(tools.length && {
491
+ toolConfig: {
492
+ functionCallingConfig: {
493
+ mode: options.tool_choice === "none"
494
+ ? FunctionCallingConfigMode.NONE
495
+ : FunctionCallingConfigMode.AUTO,
496
+ },
497
+ },
498
+ }),
487
499
  maxOutputTokens: options.max_tokens,
488
500
  },
489
501
  });