@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.
- package/package.json +1 -1
- package/src/agents/base/base.ts +44 -26
- package/src/agents/base/prompt.ts +3 -2
- package/src/agents/tools/index.ts +1 -0
- package/src/agents/tools/list.ts +2 -0
- package/src/agents/tools/loadImageAsBase64.ts +83 -0
- package/src/auth/keyAuth.ts +40 -8
- package/src/chat/modules/AgentModule.ts +98 -42
- package/src/clients/index.ts +7 -4
- package/src/clients/openai.ts +37 -4
- package/src/commands/services.ts +23 -18
- package/src/login.ts +22 -6
- package/src/processors/Base64ImageDetector.ts +12 -11
- package/src/services/KnowhowClient.ts +6 -5
- package/src/services/watchers/FsSyncer.ts +40 -2
- package/src/services/watchers/RemoteSyncer.ts +40 -2
- package/src/tunnel.ts +29 -13
- package/src/worker.ts +62 -12
- package/tests/clients/reasoningEffort.test.ts +57 -0
- package/tests/processors/Base64ImageDetector.test.ts +32 -0
- package/tests/unit/agents/tools/loadImageAsBase64.test.ts +44 -0
- package/tests/unit/auth/keyAuth.test.ts +52 -2
- package/tests/unit/login.test.ts +39 -1
- package/tests/unit/services/KnowhowClient.test.ts +84 -0
- package/tests/unit/services/Mcp.test.ts +56 -0
- package/tests/unit/services/SyncedAgentWatcher.test.ts +89 -0
- package/tests/unit/tunnel.test.ts +51 -0
- package/ts_build/package.json +1 -1
- package/ts_build/src/agents/base/base.d.ts +1 -0
- package/ts_build/src/agents/base/base.js +27 -22
- package/ts_build/src/agents/base/base.js.map +1 -1
- package/ts_build/src/agents/base/prompt.d.ts +1 -1
- package/ts_build/src/agents/base/prompt.js +3 -2
- package/ts_build/src/agents/base/prompt.js.map +1 -1
- package/ts_build/src/agents/tools/index.d.ts +1 -0
- package/ts_build/src/agents/tools/index.js +1 -0
- package/ts_build/src/agents/tools/index.js.map +1 -1
- package/ts_build/src/agents/tools/list.js +2 -0
- package/ts_build/src/agents/tools/list.js.map +1 -1
- package/ts_build/src/agents/tools/loadImageAsBase64.d.ts +3 -0
- package/ts_build/src/agents/tools/loadImageAsBase64.js +79 -0
- package/ts_build/src/agents/tools/loadImageAsBase64.js.map +1 -0
- package/ts_build/src/auth/keyAuth.d.ts +1 -0
- package/ts_build/src/auth/keyAuth.js +24 -7
- package/ts_build/src/auth/keyAuth.js.map +1 -1
- package/ts_build/src/chat/modules/AgentModule.d.ts +3 -0
- package/ts_build/src/chat/modules/AgentModule.js +74 -34
- package/ts_build/src/chat/modules/AgentModule.js.map +1 -1
- package/ts_build/src/clients/index.js +7 -4
- package/ts_build/src/clients/index.js.map +1 -1
- package/ts_build/src/clients/openai.js +29 -4
- package/ts_build/src/clients/openai.js.map +1 -1
- package/ts_build/src/commands/services.js +15 -8
- package/ts_build/src/commands/services.js.map +1 -1
- package/ts_build/src/login.js +19 -5
- package/ts_build/src/login.js.map +1 -1
- package/ts_build/src/processors/Base64ImageDetector.js +7 -3
- package/ts_build/src/processors/Base64ImageDetector.js.map +1 -1
- package/ts_build/src/services/KnowhowClient.js +5 -3
- package/ts_build/src/services/KnowhowClient.js.map +1 -1
- package/ts_build/src/services/watchers/FsSyncer.d.ts +4 -0
- package/ts_build/src/services/watchers/FsSyncer.js +36 -2
- package/ts_build/src/services/watchers/FsSyncer.js.map +1 -1
- package/ts_build/src/services/watchers/RemoteSyncer.d.ts +4 -0
- package/ts_build/src/services/watchers/RemoteSyncer.js +36 -2
- package/ts_build/src/services/watchers/RemoteSyncer.js.map +1 -1
- package/ts_build/src/tunnel.js +19 -8
- package/ts_build/src/tunnel.js.map +1 -1
- package/ts_build/src/worker.js +47 -10
- package/ts_build/src/worker.js.map +1 -1
- package/ts_build/tests/clients/reasoningEffort.test.js +47 -0
- package/ts_build/tests/clients/reasoningEffort.test.js.map +1 -1
- package/ts_build/tests/processors/Base64ImageDetector.test.js +28 -0
- package/ts_build/tests/processors/Base64ImageDetector.test.js.map +1 -1
- package/ts_build/tests/unit/agents/tools/loadImageAsBase64.test.d.ts +1 -0
- package/ts_build/tests/unit/agents/tools/loadImageAsBase64.test.js +67 -0
- package/ts_build/tests/unit/agents/tools/loadImageAsBase64.test.js.map +1 -0
- package/ts_build/tests/unit/auth/keyAuth.test.js +43 -2
- package/ts_build/tests/unit/auth/keyAuth.test.js.map +1 -1
- package/ts_build/tests/unit/login.test.js +29 -1
- package/ts_build/tests/unit/login.test.js.map +1 -1
- package/ts_build/tests/unit/services/KnowhowClient.test.d.ts +1 -0
- package/ts_build/tests/unit/services/KnowhowClient.test.js +69 -0
- package/ts_build/tests/unit/services/KnowhowClient.test.js.map +1 -0
- package/ts_build/tests/unit/services/Mcp.test.d.ts +1 -0
- package/ts_build/tests/unit/services/Mcp.test.js +45 -0
- package/ts_build/tests/unit/services/Mcp.test.js.map +1 -0
- package/ts_build/tests/unit/services/SyncedAgentWatcher.test.d.ts +1 -0
- package/ts_build/tests/unit/services/SyncedAgentWatcher.test.js +78 -0
- package/ts_build/tests/unit/services/SyncedAgentWatcher.test.js.map +1 -0
- package/ts_build/tests/unit/tunnel.test.d.ts +1 -0
- package/ts_build/tests/unit/tunnel.test.js +47 -0
- package/ts_build/tests/unit/tunnel.test.js.map +1 -0
package/package.json
CHANGED
package/src/agents/base/base.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
${
|
|
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
|
|
1765
|
-
.map((c) => c.message
|
|
1766
|
-
|
|
1767
|
-
|
|
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(
|
|
1781
|
+
this.summaries.push(summary);
|
|
1772
1782
|
|
|
1773
1783
|
const startMessages = [
|
|
1774
1784
|
{
|
|
1775
1785
|
role: "user",
|
|
1776
1786
|
content: `
|
|
1777
|
-
${
|
|
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
|
-
${
|
|
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
|
|
8
|
-
|
|
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
|
package/src/agents/tools/list.ts
CHANGED
|
@@ -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
|
+
};
|
package/src/auth/keyAuth.ts
CHANGED
|
@@ -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 {
|
|
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
|
|
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
|
|
112
|
-
const
|
|
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,
|
|
117
|
-
fs.chmodSync(
|
|
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 =
|
|
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(
|
|
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
|
-
//
|
|
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
|
package/src/clients/index.ts
CHANGED
|
@@ -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
|
-
|
|
112
|
-
|
|
113
|
-
|
|
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
|
};
|
package/src/clients/openai.ts
CHANGED
|
@@ -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) &&
|
|
499
|
-
max_output_tokens: Math.max(options.max_tokens || 0, 16000),
|
|
498
|
+
...(OpenAiReasoningModels.includes(options.model) && {
|
|
500
499
|
reasoning: {
|
|
501
|
-
|
|
502
|
-
|
|
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
|
{
|