@tyvm/knowhow 0.0.154 → 0.0.156

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 (93) hide show
  1. package/package.json +1 -1
  2. package/src/agents/base/base.ts +44 -26
  3. package/src/agents/base/prompt.ts +3 -2
  4. package/src/agents/tools/index.ts +1 -0
  5. package/src/agents/tools/list.ts +2 -0
  6. package/src/agents/tools/loadImageAsBase64.ts +83 -0
  7. package/src/auth/keyAuth.ts +40 -8
  8. package/src/chat/modules/AgentModule.ts +98 -42
  9. package/src/clients/index.ts +7 -4
  10. package/src/clients/openai.ts +37 -4
  11. package/src/commands/services.ts +23 -18
  12. package/src/login.ts +22 -6
  13. package/src/processors/Base64ImageDetector.ts +12 -11
  14. package/src/services/KnowhowClient.ts +6 -5
  15. package/src/services/watchers/FsSyncer.ts +40 -2
  16. package/src/services/watchers/RemoteSyncer.ts +40 -2
  17. package/src/tunnel.ts +29 -13
  18. package/src/worker.ts +62 -12
  19. package/tests/clients/reasoningEffort.test.ts +57 -0
  20. package/tests/processors/Base64ImageDetector.test.ts +32 -0
  21. package/tests/unit/agents/tools/loadImageAsBase64.test.ts +44 -0
  22. package/tests/unit/auth/keyAuth.test.ts +52 -2
  23. package/tests/unit/login.test.ts +39 -1
  24. package/tests/unit/services/KnowhowClient.test.ts +84 -0
  25. package/tests/unit/services/Mcp.test.ts +56 -0
  26. package/tests/unit/services/SyncedAgentWatcher.test.ts +89 -0
  27. package/tests/unit/tunnel.test.ts +51 -0
  28. package/ts_build/package.json +1 -1
  29. package/ts_build/src/agents/base/base.d.ts +1 -0
  30. package/ts_build/src/agents/base/base.js +27 -22
  31. package/ts_build/src/agents/base/base.js.map +1 -1
  32. package/ts_build/src/agents/base/prompt.d.ts +1 -1
  33. package/ts_build/src/agents/base/prompt.js +3 -2
  34. package/ts_build/src/agents/base/prompt.js.map +1 -1
  35. package/ts_build/src/agents/tools/index.d.ts +1 -0
  36. package/ts_build/src/agents/tools/index.js +1 -0
  37. package/ts_build/src/agents/tools/index.js.map +1 -1
  38. package/ts_build/src/agents/tools/list.js +2 -0
  39. package/ts_build/src/agents/tools/list.js.map +1 -1
  40. package/ts_build/src/agents/tools/loadImageAsBase64.d.ts +3 -0
  41. package/ts_build/src/agents/tools/loadImageAsBase64.js +79 -0
  42. package/ts_build/src/agents/tools/loadImageAsBase64.js.map +1 -0
  43. package/ts_build/src/auth/keyAuth.d.ts +1 -0
  44. package/ts_build/src/auth/keyAuth.js +24 -7
  45. package/ts_build/src/auth/keyAuth.js.map +1 -1
  46. package/ts_build/src/chat/modules/AgentModule.d.ts +3 -0
  47. package/ts_build/src/chat/modules/AgentModule.js +74 -34
  48. package/ts_build/src/chat/modules/AgentModule.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/openai.js +29 -4
  52. package/ts_build/src/clients/openai.js.map +1 -1
  53. package/ts_build/src/commands/services.js +15 -8
  54. package/ts_build/src/commands/services.js.map +1 -1
  55. package/ts_build/src/login.js +19 -5
  56. package/ts_build/src/login.js.map +1 -1
  57. package/ts_build/src/processors/Base64ImageDetector.js +7 -3
  58. package/ts_build/src/processors/Base64ImageDetector.js.map +1 -1
  59. package/ts_build/src/services/KnowhowClient.js +5 -3
  60. package/ts_build/src/services/KnowhowClient.js.map +1 -1
  61. package/ts_build/src/services/watchers/FsSyncer.d.ts +4 -0
  62. package/ts_build/src/services/watchers/FsSyncer.js +36 -2
  63. package/ts_build/src/services/watchers/FsSyncer.js.map +1 -1
  64. package/ts_build/src/services/watchers/RemoteSyncer.d.ts +4 -0
  65. package/ts_build/src/services/watchers/RemoteSyncer.js +36 -2
  66. package/ts_build/src/services/watchers/RemoteSyncer.js.map +1 -1
  67. package/ts_build/src/tunnel.js +19 -8
  68. package/ts_build/src/tunnel.js.map +1 -1
  69. package/ts_build/src/worker.js +47 -10
  70. package/ts_build/src/worker.js.map +1 -1
  71. package/ts_build/tests/clients/reasoningEffort.test.js +47 -0
  72. package/ts_build/tests/clients/reasoningEffort.test.js.map +1 -1
  73. package/ts_build/tests/processors/Base64ImageDetector.test.js +28 -0
  74. package/ts_build/tests/processors/Base64ImageDetector.test.js.map +1 -1
  75. package/ts_build/tests/unit/agents/tools/loadImageAsBase64.test.d.ts +1 -0
  76. package/ts_build/tests/unit/agents/tools/loadImageAsBase64.test.js +67 -0
  77. package/ts_build/tests/unit/agents/tools/loadImageAsBase64.test.js.map +1 -0
  78. package/ts_build/tests/unit/auth/keyAuth.test.js +43 -2
  79. package/ts_build/tests/unit/auth/keyAuth.test.js.map +1 -1
  80. package/ts_build/tests/unit/login.test.js +29 -1
  81. package/ts_build/tests/unit/login.test.js.map +1 -1
  82. package/ts_build/tests/unit/services/KnowhowClient.test.d.ts +1 -0
  83. package/ts_build/tests/unit/services/KnowhowClient.test.js +69 -0
  84. package/ts_build/tests/unit/services/KnowhowClient.test.js.map +1 -0
  85. package/ts_build/tests/unit/services/Mcp.test.d.ts +1 -0
  86. package/ts_build/tests/unit/services/Mcp.test.js +45 -0
  87. package/ts_build/tests/unit/services/Mcp.test.js.map +1 -0
  88. package/ts_build/tests/unit/services/SyncedAgentWatcher.test.d.ts +1 -0
  89. package/ts_build/tests/unit/services/SyncedAgentWatcher.test.js +78 -0
  90. package/ts_build/tests/unit/services/SyncedAgentWatcher.test.js.map +1 -0
  91. package/ts_build/tests/unit/tunnel.test.d.ts +1 -0
  92. package/ts_build/tests/unit/tunnel.test.js +47 -0
  93. package/ts_build/tests/unit/tunnel.test.js.map +1 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tyvm/knowhow",
3
- "version": "0.0.154",
3
+ "version": "0.0.156",
4
4
  "description": "ai cli with plugins and agents",
5
5
  "main": "ts_build/src/index.js",
6
6
  "bin": {
@@ -1660,6 +1660,20 @@ export abstract class BaseAgent implements IAgent {
1660
1660
  return null;
1661
1661
  }
1662
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
+
1663
1677
  async getTaskBreakdown(messages: Message[]) {
1664
1678
  if (this.taskBreakdown) {
1665
1679
  return this.taskBreakdown;
@@ -1686,28 +1700,21 @@ export abstract class BaseAgent implements IAgent {
1686
1700
  ] as Message[];
1687
1701
 
1688
1702
  const response = await this.createAgentCompletion({
1689
- model,
1690
1703
  messages: taskBreakdownMessages,
1691
1704
  tool_choice: "none",
1692
- long_ttl_cache: this.runTime() > 300_000,
1693
- ...(this.reasoningEffort !== undefined && {
1694
- reasoning_effort: this.reasoningEffort,
1695
- }),
1696
- ...(this.summarizeReasoning !== undefined && {
1697
- reasoning_summary: this.summarizeReasoning,
1698
- }),
1699
1705
  });
1700
1706
 
1707
+ const breakdownContent = this.extractContentFromMessages(
1708
+ response.choices?.map((c) => c.message)
1709
+ );
1701
1710
  // The model may return null content (e.g. when it responds with tool calls
1702
1711
  // instead of text). Guard against storing a literal null so that template
1703
1712
  // strings don't render "null".
1704
- const breakdownContent = response.choices
1705
- .map((c) => c.message.content)
1706
- .filter((s): s is string => typeof s === "string" && s.trim().length > 0)
1707
- .join("\n\n");
1708
-
1709
1713
  if (!breakdownContent) {
1710
- throw new Error("Compaction task-breakdown request returned no text content");
1714
+ console.log(JSON.stringify(response.choices, null, 2));
1715
+ throw new Error(
1716
+ "Compaction task-breakdown request returned no text content"
1717
+ );
1711
1718
  }
1712
1719
 
1713
1720
  this.taskBreakdown = breakdownContent;
@@ -1720,10 +1727,7 @@ export abstract class BaseAgent implements IAgent {
1720
1727
  // at the final assistant message keeps its tool calls, every tool response,
1721
1728
  // and any subsequent user messages together as one protocol-valid unit.
1722
1729
  let resumeIndex = messages.length - 1;
1723
- while (
1724
- resumeIndex >= 0 &&
1725
- messages[resumeIndex].role !== "assistant"
1726
- ) {
1730
+ while (resumeIndex >= 0 && messages[resumeIndex].role !== "assistant") {
1727
1731
  resumeIndex--;
1728
1732
  }
1729
1733
  const compressionEnd = resumeIndex === -1 ? messages.length : resumeIndex;
@@ -1742,7 +1746,11 @@ export abstract class BaseAgent implements IAgent {
1742
1746
  3. Next Steps - what we're about to do next to continue the user's original request.
1743
1747
  4. Tasks remaining - what tasks are left from the initial task breakdown.
1744
1748
 
1745
- ${this.taskBreakdown ? `Our initial task breakdown: ${this.taskBreakdown}` : ""}
1749
+ ${
1750
+ this.taskBreakdown
1751
+ ? `Our initial task breakdown: ${this.taskBreakdown}`
1752
+ : ""
1753
+ }
1746
1754
 
1747
1755
  This summary will replace the older history. The latest agent interaction will remain verbatim:
1748
1756
 
@@ -1761,23 +1769,33 @@ export abstract class BaseAgent implements IAgent {
1761
1769
  tool_choice: "none",
1762
1770
  });
1763
1771
 
1764
- const summaries = response.choices
1765
- .map((c) => c.message.content)
1766
- .filter((s): s is string => typeof s === "string" && s.trim().length > 0);
1767
- if (summaries.length === 0) {
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));
1768
1778
  throw new Error("Compaction summary request returned no text content");
1769
1779
  }
1770
1780
 
1771
- this.summaries.push(...summaries);
1781
+ this.summaries.push(summary);
1772
1782
 
1773
1783
  const startMessages = [
1774
1784
  {
1775
1785
  role: "user",
1776
1786
  content: `
1777
- ${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
+ }
1778
1792
 
1779
1793
  We have just compressed the conversation to save memory:
1780
- ${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
+ }
1781
1799
 
1782
1800
  Please continue the task from where we left off
1783
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
@@ -13,6 +13,7 @@ export * from "./readFile";
13
13
  export * from "./scanFile";
14
14
  export * from "./textSearch";
15
15
  export * from "./visionTool";
16
+ export * from "./loadImageAsBase64";
16
17
  export * from "./writeFile";
17
18
  export * from "./fileSearch";
18
19
  export * from "./language";
@@ -20,6 +20,7 @@ import {
20
20
  stopObservingDefinition,
21
21
  } from "./observe";
22
22
  import { skillsToolDefinitions } from "./skills";
23
+ import { loadImageAsBase64Definition } from "./loadImageAsBase64";
23
24
 
24
25
  function getPluginNames(): string {
25
26
  try {
@@ -720,6 +721,7 @@ export const includedTools = [
720
721
  },
721
722
  },
722
723
  },
724
+ loadImageAsBase64Definition,
723
725
  googleSearchDefinition,
724
726
  startAgentTaskDefinition,
725
727
  replyToParentDefinition,
@@ -0,0 +1,83 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+ import { Tool } from "../../clients/types";
4
+
5
+ const MAX_IMAGE_BYTES = 15 * 1024 * 1024;
6
+
7
+ const MIME_TYPES: Record<string, string> = {
8
+ png: "image/png",
9
+ jpg: "image/jpeg",
10
+ jpeg: "image/jpeg",
11
+ gif: "image/gif",
12
+ webp: "image/webp",
13
+ bmp: "image/bmp",
14
+ svg: "image/svg+xml",
15
+ avif: "image/avif",
16
+ ico: "image/x-icon",
17
+ };
18
+
19
+ function detectMimeType(buffer: Buffer, fallback: string): string {
20
+ if (buffer.length >= 3 && buffer[0] === 0xff && buffer[1] === 0xd8 && buffer[2] === 0xff) {
21
+ return "image/jpeg";
22
+ }
23
+ if (buffer.length >= 4 && buffer[0] === 0x89 && buffer[1] === 0x50 && buffer[2] === 0x4e && buffer[3] === 0x47) {
24
+ return "image/png";
25
+ }
26
+ if (buffer.length >= 4 && buffer.subarray(0, 4).toString("ascii") === "GIF8") {
27
+ return "image/gif";
28
+ }
29
+ if (
30
+ buffer.length >= 12 &&
31
+ buffer.subarray(0, 4).toString("ascii") === "RIFF" &&
32
+ buffer.subarray(8, 12).toString("ascii") === "WEBP"
33
+ ) {
34
+ return "image/webp";
35
+ }
36
+ return fallback;
37
+ }
38
+
39
+ /** Load a reasonably sized image from the worker filesystem for UI/AI preview. */
40
+ export async function loadImageAsBase64(
41
+ filePath: string,
42
+ detail: "auto" | "low" | "high" = "auto"
43
+ ): Promise<string> {
44
+ const stats = await fs.promises.stat(filePath).catch(() => null);
45
+ if (!stats) throw new Error(`File not found: ${filePath}`);
46
+ if (!stats.isFile()) throw new Error(`Path is not a file: ${filePath}`);
47
+ if (stats.size > MAX_IMAGE_BYTES) {
48
+ throw new Error(`Image is too large to preview (${Math.ceil(stats.size / 1024 / 1024)} MB; maximum 15 MB)`);
49
+ }
50
+
51
+ const extension = path.extname(filePath).slice(1).toLowerCase();
52
+ const extensionMimeType = MIME_TYPES[extension];
53
+ if (!extensionMimeType) {
54
+ throw new Error(`Unsupported image format: ${extension || "unknown"}`);
55
+ }
56
+
57
+ const image = await fs.promises.readFile(filePath);
58
+ const mimeType = detectMimeType(image, extensionMimeType);
59
+ return JSON.stringify({
60
+ type: "image_url",
61
+ image_url: {
62
+ url: `data:${mimeType};base64,${image.toString("base64")}`,
63
+ detail,
64
+ },
65
+ });
66
+ }
67
+
68
+ export const loadImageAsBase64Definition: Tool = {
69
+ type: "function",
70
+ function: {
71
+ name: "loadImageAsBase64",
72
+ description: "Load an image from the worker filesystem and return it as a base64 data URL for previewing.",
73
+ parameters: {
74
+ type: "object",
75
+ positional: true,
76
+ properties: {
77
+ filePath: { type: "string", description: "Absolute or workspace-relative image path" },
78
+ detail: { type: "string", enum: ["auto", "low", "high"], description: "Image detail hint" },
79
+ },
80
+ required: ["filePath"],
81
+ },
82
+ },
83
+ };
@@ -3,9 +3,13 @@ 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
- const JWT_FILE_PATH = path.join(process.cwd(), ".knowhow", ".jwt");
9
13
  const DEFAULT_API_URL = process.env.KNOWHOW_API_URL || "https://api.knowhow.tyvm.ai";
10
14
 
11
15
  /**
@@ -62,15 +66,42 @@ export async function exchangePublicKeyJwt(
62
66
  return response.data.jwt;
63
67
  }
64
68
 
69
+ /**
70
+ * Try the configured identity first, then the default CLI identity. Project
71
+ * configs can outlive temporary/custom keys, while the globally registered
72
+ * default identity remains valid.
73
+ */
74
+ export async function exchangeAvailablePublicKeyJwt(
75
+ orgId: string,
76
+ apiUrl: string = DEFAULT_API_URL,
77
+ preferredPrivateKeyPath?: string
78
+ ): Promise<string> {
79
+ const defaultPath = getDefaultPrivateKeyPath();
80
+ const candidates = Array.from(
81
+ new Set([preferredPrivateKeyPath, defaultPath].filter((value): value is string => Boolean(value)))
82
+ ).filter((candidate) => keyPairExists(candidate));
83
+ if (candidates.length === 0) throw new Error("CLI identity is unavailable");
84
+
85
+ let lastError: unknown;
86
+ for (const candidate of candidates) {
87
+ try {
88
+ return await exchangePublicKeyJwt(orgId, apiUrl, candidate);
89
+ } catch (error) {
90
+ lastError = error;
91
+ }
92
+ }
93
+ throw lastError;
94
+ }
95
+
65
96
  export async function authenticateWithKey(
66
97
  orgId: string,
67
98
  apiUrl: string = DEFAULT_API_URL,
68
99
  privateKeyPath?: string
69
100
  ): Promise<boolean> {
70
- if (!keyPairExists(privateKeyPath)) return false;
101
+ if (!keyPairExists(privateKeyPath) && !keyPairExists()) return false;
71
102
 
72
103
  try {
73
- storeJwt(await exchangePublicKeyJwt(orgId, apiUrl, privateKeyPath));
104
+ storeJwt(await exchangeAvailablePublicKeyJwt(orgId, apiUrl, privateKeyPath));
74
105
  return true;
75
106
  } catch (error: unknown) {
76
107
  // The identity is not registered for this environment/org (or was revoked).
@@ -108,13 +139,14 @@ export async function registerPublicKey(
108
139
  }
109
140
 
110
141
  export function storeJwt(jwt: string): void {
111
- const directory = path.dirname(JWT_FILE_PATH);
112
- const temporaryPath = `${JWT_FILE_PATH}.${process.pid}.tmp`;
142
+ const jwtFilePath = path.join(process.cwd(), ".knowhow", ".jwt");
143
+ const directory = path.dirname(jwtFilePath);
144
+ const temporaryPath = `${jwtFilePath}.${process.pid}.tmp`;
113
145
  fs.mkdirSync(directory, { recursive: true, mode: 0o700 });
114
146
  try {
115
147
  fs.writeFileSync(temporaryPath, jwt, { mode: 0o600, flag: "wx" });
116
- fs.renameSync(temporaryPath, JWT_FILE_PATH);
117
- fs.chmodSync(JWT_FILE_PATH, 0o600);
148
+ fs.renameSync(temporaryPath, jwtFilePath);
149
+ fs.chmodSync(jwtFilePath, 0o600);
118
150
  } finally {
119
151
  fs.rmSync(temporaryPath, { force: true });
120
152
  }
@@ -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
@@ -28,8 +28,9 @@ import { GenericXAIClient } from "./xai";
28
28
  import { KnowhowGenericClient } from "./knowhow";
29
29
  import { HttpClient } from "./http";
30
30
  import { ModelProvider } from "../types";
31
- import { getConfig } from "../config";
31
+ import { getConfig, getConfigSync } from "../config";
32
32
  import { loadKnowhowJwt, KNOWHOW_API_URL } from "../services/KnowhowClient";
33
+ import { keyPairExists } from "../auth/keyManager";
33
34
  import { ContextLimits } from "./contextLimits";
34
35
  import { OpenAiTextPricing } from "./pricing/openai";
35
36
  import { AnthropicTextPricing } from "./pricing/anthropic";
@@ -108,9 +109,11 @@ const BUILT_IN_PROVIDER_REGISTRY: Record<string, ProviderRegistryEntry> = {
108
109
  qwen: { clientClass: GenericQwenClient },
109
110
  knowhow: {
110
111
  createClient: (entry: ModelProvider) => {
111
- const jwt = loadKnowhowJwt();
112
- if (!jwt) return null;
113
- return new KnowhowGenericClient(KNOWHOW_API_URL, jwt);
112
+ if (!loadKnowhowJwt()) {
113
+ const config = getConfigSync();
114
+ if (!config.orgId || (!keyPairExists(config.cliIdentityPath) && !keyPairExists())) return null;
115
+ }
116
+ return new KnowhowGenericClient(KNOWHOW_API_URL);
114
117
  },
115
118
  },
116
119
  };
@@ -495,13 +495,19 @@ export class GenericOpenAiClient implements GenericClient {
495
495
  include: ["reasoning.encrypted_content"],
496
496
  }),
497
497
  // Don't limit max_output_tokens for Responses API - codex truncates tool call arguments when limited
498
- ...(OpenAiReasoningModels.includes(options.model) && !this.isReasoningDisabled(options) && {
499
- max_output_tokens: Math.max(options.max_tokens || 0, 16000),
498
+ ...(OpenAiReasoningModels.includes(options.model) && {
500
499
  reasoning: {
501
- effort: this.resolveReasoningEffortForModel(options),
502
- ...(options.reasoning_summary && { summary: "auto" }),
500
+ ...(this.isReasoningDisabled(options)
501
+ ? { effort: "none" }
502
+ : {
503
+ effort: this.resolveReasoningEffortForModel(options),
504
+ ...(options.reasoning_summary && { summary: "auto" }),
505
+ }),
503
506
  },
504
507
  }),
508
+ ...(wantsReasoning && {
509
+ max_output_tokens: Math.max(options.max_tokens || 0, 16000),
510
+ }),
505
511
  ...(tools?.length && {
506
512
  tools,
507
513
  tool_choice: options.tool_choice ?? "auto",
@@ -582,6 +588,33 @@ export class GenericOpenAiClient implements GenericClient {
582
588
  }
583
589
  }
584
590
 
591
+ // Tool-only responses are valid agent turns. Warn only when the response
592
+ // contains neither visible text nor any callable output, and include enough
593
+ // provider metadata to diagnose incomplete or reasoning-only responses.
594
+ if (!textContent?.trim() && toolCalls.length === 0) {
595
+ const outputShape = response.output.map((item: any) => ({
596
+ type: item.type,
597
+ contentTypes: Array.isArray(item.content)
598
+ ? item.content.map((part: any) => part?.type ?? typeof part)
599
+ : [],
600
+ }));
601
+ const reasoningSummaryCount = response.output
602
+ .filter((item: any) => item.type === "reasoning")
603
+ .reduce(
604
+ (count: number, item: any) =>
605
+ count + (Array.isArray(item.summary) ? item.summary.length : 0),
606
+ 0
607
+ );
608
+ console.warn("[Responses API] Response contained no text or tool calls", {
609
+ responseId: (response as any).id,
610
+ status: (response as any).status,
611
+ incompleteDetails: (response as any).incomplete_details,
612
+ outputShape,
613
+ reasoningSummaryCount,
614
+ usage: response.usage,
615
+ });
616
+ }
617
+
585
618
  return {
586
619
  choices: [
587
620
  {