@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
@@ -10,6 +10,7 @@ import {
10
10
  import {
11
11
  GoogleThinkingBudgetModels,
12
12
  GoogleThinkingLevelModels,
13
+ Models,
13
14
  } from "../../src/types";
14
15
  import { GenericXAIClient } from "../../src/clients/xai";
15
16
 
@@ -44,15 +45,75 @@ describe("reasoning effort values", () => {
44
45
  expect(client.resolveReasoningEffortForModel(options("unknown", "max"))).toBe("xhigh");
45
46
  });
46
47
 
48
+ it("explicitly disables reasoning in Responses API requests", async () => {
49
+ const client = new GenericOpenAiClient("fake-key");
50
+ const create = jest.fn().mockResolvedValue({
51
+ id: "resp_disabled",
52
+ status: "completed",
53
+ output: [{ type: "message", content: [{ type: "output_text", text: "summary" }] }],
54
+ usage: { input_tokens: 1, output_tokens: 1 },
55
+ });
56
+ (client.client as any).responses.create = create;
57
+
58
+ await client.createChatResponse({
59
+ model: Models.openai.GPT_56_Sol,
60
+ messages: [{ role: "user", content: "summarize" }],
61
+ reasoning_effort: "none",
62
+ });
63
+
64
+ expect(create).toHaveBeenCalledWith(
65
+ expect.objectContaining({ reasoning: { effort: "none" } })
66
+ );
67
+ });
68
+
69
+ it("diagnoses empty Responses output but not valid tool-only output", async () => {
70
+ const client = new GenericOpenAiClient("fake-key");
71
+ const warn = jest.spyOn(console, "warn").mockImplementation(() => undefined);
72
+ const create = jest.fn()
73
+ .mockResolvedValueOnce({
74
+ id: "resp_empty",
75
+ status: "incomplete",
76
+ incomplete_details: { reason: "max_output_tokens" },
77
+ output: [{ type: "reasoning", summary: [] }],
78
+ usage: { input_tokens: 2, output_tokens: 3 },
79
+ })
80
+ .mockResolvedValueOnce({
81
+ id: "resp_tool",
82
+ status: "completed",
83
+ output: [{
84
+ type: "function_call", call_id: "call_1", name: "readFile", arguments: "{}",
85
+ }],
86
+ usage: { input_tokens: 2, output_tokens: 3 },
87
+ });
88
+ (client.client as any).responses.create = create;
89
+ const request = {
90
+ model: Models.openai.GPT_56_Sol,
91
+ messages: [{ role: "user" as const, content: "continue" }],
92
+ };
93
+
94
+ await client.createChatResponse(request);
95
+ expect(warn).toHaveBeenCalledWith(
96
+ "[Responses API] Response contained no text or tool calls",
97
+ expect.objectContaining({ responseId: "resp_empty", status: "incomplete" })
98
+ );
99
+ warn.mockClear();
100
+ await client.createChatResponse(request);
101
+ expect(warn).not.toHaveBeenCalled();
102
+ warn.mockRestore();
103
+ });
104
+
47
105
  it("maps all values to valid Gemini thinking levels and budgets", () => {
48
106
  const client = new GenericGeminiClient("fake-key");
49
- const levelModel = GoogleThinkingLevelModels[0];
107
+ const levelModel = GoogleThinkingLevelModels.find(
108
+ (model) => model !== Models.google.Gemini_37_Flash
109
+ )!;
50
110
  const budgetModel = GoogleThinkingBudgetModels[0];
51
111
 
52
112
  expect(client.buildThinkingConfig(options(levelModel, "none"))).toEqual({ thinkingLevel: "minimal" });
53
113
  expect(client.buildThinkingConfig(options(levelModel, "minimal"))).toEqual({ thinkingLevel: "minimal" });
54
114
  expect(client.buildThinkingConfig(options(levelModel, "xhigh"))).toEqual({ thinkingLevel: "high" });
55
115
  expect(client.buildThinkingConfig(options(levelModel, "max"))).toEqual({ thinkingLevel: "high" });
116
+ expect(client.buildThinkingConfig(options(Models.google.Gemini_37_Flash, "minimal"))).toEqual({ thinkingLevel: "low" });
56
117
  expect(client.buildThinkingConfig(options(budgetModel, "none"))).toEqual({ thinkingBudget: 0 });
57
118
  expect(client.buildThinkingConfig(options(budgetModel, "minimal"))).toEqual({ thinkingBudget: 512 });
58
119
  expect(client.buildThinkingConfig(options(budgetModel, "medium"))).toEqual({ thinkingBudget: 8192 });
@@ -14,6 +14,7 @@ jest.mock("../../../src/utils/http", () => ({
14
14
  }));
15
15
 
16
16
  jest.mock("../../../src/auth/keyManager", () => ({
17
+ getDefaultPrivateKeyPath: () => "/keys/default",
17
18
  keyPairExists,
18
19
  loadKeyPair,
19
20
  signMessage,
@@ -25,6 +26,7 @@ jest.mock("../../../src/auth/browserLogin", () => ({
25
26
 
26
27
  import {
27
28
  authenticateWithKey,
29
+ exchangeAvailablePublicKeyJwt,
28
30
  exchangePublicKeyJwt,
29
31
  hasKeyPair,
30
32
  registerPublicKey,
@@ -69,14 +71,41 @@ describe("keyAuth", () => {
69
71
  ]);
70
72
  });
71
73
 
74
+ it("uses the registered default identity when a configured identity is rejected", async () => {
75
+ post
76
+ .mockRejectedValueOnce({ status: 401 })
77
+ .mockResolvedValueOnce({
78
+ data: {
79
+ challengeId: "challenge_2",
80
+ message: "default key challenge",
81
+ expiresAt: "2099-01-01T00:00:00.000Z",
82
+ },
83
+ status: 201,
84
+ })
85
+ .mockResolvedValueOnce({ data: { jwt: "default-key-jwt" }, status: 200 });
86
+
87
+ await expect(
88
+ exchangeAvailablePublicKeyJwt("org-1", "https://work.example", "/keys/stale")
89
+ ).resolves.toBe("default-key-jwt");
90
+
91
+ expect(loadKeyPair.mock.calls).toEqual([
92
+ ["/keys/stale"],
93
+ ["/keys/default"],
94
+ ]);
95
+ expect(signMessage).toHaveBeenCalledWith(
96
+ "default key challenge",
97
+ "/keys/default"
98
+ );
99
+ });
100
+
72
101
  it("falls back when the key is unavailable without signing a challenge", async () => {
73
- post.mockRejectedValueOnce({ status: 401 });
102
+ post.mockRejectedValue({ status: 401 });
74
103
 
75
104
  await expect(
76
105
  authenticateWithKey("org-1", "https://work.example", "/keys/work")
77
106
  ).resolves.toBe(false);
78
107
 
79
- expect(post).toHaveBeenCalledTimes(1);
108
+ expect(post).toHaveBeenCalledTimes(2);
80
109
  expect(signMessage).not.toHaveBeenCalled();
81
110
  });
82
111
 
@@ -0,0 +1,84 @@
1
+ import fs from "fs";
2
+
3
+ const mockGet = jest.fn();
4
+ const mockExchangePublicKeyJwt = jest.fn();
5
+ const mockStoreJwt = jest.fn();
6
+
7
+ jest.mock("../../../src/utils/http", () => {
8
+ const actual = jest.requireActual("../../../src/utils/http");
9
+ const mockedHttp = {
10
+ get: (...args: unknown[]) => mockGet(...args),
11
+ };
12
+ return {
13
+ __esModule: true,
14
+ ...actual,
15
+ http: mockedHttp,
16
+ default: mockedHttp,
17
+ };
18
+ });
19
+
20
+ jest.mock("../../../src/config", () => ({
21
+ getConfigSync: jest.fn().mockReturnValue({
22
+ orgId: "org-1",
23
+ cliIdentityPath: "/tmp/identity",
24
+ }),
25
+ }));
26
+
27
+ jest.mock("../../../src/auth/keyManager", () => ({
28
+ keyPairExists: jest.fn().mockReturnValue(true),
29
+ }));
30
+
31
+ jest.mock("../../../src/auth/keyAuth", () => ({
32
+ exchangeAvailablePublicKeyJwt: (...args: unknown[]) => mockExchangePublicKeyJwt(...args),
33
+ storeJwt: (...args: unknown[]) => mockStoreJwt(...args),
34
+ }));
35
+
36
+ import { KnowhowSimpleClient } from "../../../src/services/KnowhowClient";
37
+
38
+ describe("KnowhowSimpleClient identity renewal", () => {
39
+ afterEach(() => {
40
+ jest.restoreAllMocks();
41
+ jest.clearAllMocks();
42
+ });
43
+
44
+ it("obtains a JWT from the registered identity before loading models when no JWT exists", async () => {
45
+ jest.spyOn(fs, "existsSync").mockReturnValue(false);
46
+ mockExchangePublicKeyJwt.mockResolvedValue("fresh-jwt");
47
+ mockGet.mockResolvedValueOnce({ data: { data: [] } });
48
+
49
+ const client = new KnowhowSimpleClient("https://api.example.test");
50
+ await client.getModels();
51
+
52
+ expect(mockExchangePublicKeyJwt).toHaveBeenCalledWith(
53
+ "org-1",
54
+ "https://api.example.test",
55
+ "/tmp/identity"
56
+ );
57
+ expect(mockStoreJwt).toHaveBeenCalledWith("fresh-jwt");
58
+ expect(mockGet.mock.calls[0][1].headers.Authorization).toBe("Bearer fresh-jwt");
59
+ });
60
+
61
+ it("replaces an expired JWT with one signed by the registered identity before loading models", async () => {
62
+ const expiredJwt = [
63
+ Buffer.from('{"alg":"none"}').toString("base64url"),
64
+ Buffer.from('{"exp":0}').toString("base64url"),
65
+ "old-signature",
66
+ ].join(".");
67
+ jest.spyOn(fs, "existsSync").mockReturnValue(true);
68
+ jest.spyOn(fs, "readFileSync").mockReturnValue(expiredJwt);
69
+ mockExchangePublicKeyJwt.mockResolvedValue("fresh-jwt");
70
+ mockGet.mockResolvedValueOnce({ data: { data: [] } });
71
+
72
+ const client = new KnowhowSimpleClient("https://api.example.test");
73
+ await client.getModels();
74
+
75
+ expect(mockExchangePublicKeyJwt).toHaveBeenCalledWith(
76
+ "org-1",
77
+ "https://api.example.test",
78
+ "/tmp/identity"
79
+ );
80
+ expect(mockStoreJwt).toHaveBeenCalledWith("fresh-jwt");
81
+ expect(mockGet).toHaveBeenCalledTimes(1);
82
+ expect(mockGet.mock.calls[0][1].headers.Authorization).toBe("Bearer fresh-jwt");
83
+ });
84
+ });
@@ -0,0 +1,89 @@
1
+ import { FsSyncedAgentWatcher } from "../../../src/services/watchers/FsSyncer";
2
+ import { WebSyncedAgentWatcher } from "../../../src/services/watchers/RemoteSyncer";
3
+
4
+ const retained = { role: "assistant", content: "before compaction" };
5
+ const oldThread = [
6
+ { role: "system", content: "instructions" },
7
+ { role: "user", content: "task" },
8
+ retained,
9
+ ];
10
+ const compactedThread = [
11
+ { role: "system", content: "instructions" },
12
+ { role: "user", content: "compressed summary" },
13
+ retained,
14
+ { role: "assistant", content: "after compaction" },
15
+ ];
16
+
17
+ function seedCursor(watcher: any): void {
18
+ watcher.taskId = "task-1";
19
+ watcher.agentName = "Developer";
20
+ watcher.lastThreadIndex = 0;
21
+ watcher.lastThreadLength = oldThread.length;
22
+ watcher.lastMessageJson = JSON.stringify(retained);
23
+ }
24
+
25
+ describe("synced agent watcher thread rollover", () => {
26
+ it("continues rendering after filesystem agent compaction", async () => {
27
+ const watcher: any = new FsSyncedAgentWatcher();
28
+ seedCursor(watcher);
29
+ watcher.readMetadata = jest.fn().mockResolvedValue({
30
+ threads: [oldThread, compactedThread],
31
+ status: "running",
32
+ });
33
+
34
+ const messages: string[] = [];
35
+ watcher.agentEvents.on(watcher.eventTypes.agentSay, (data: any) => {
36
+ messages.push(data.message);
37
+ });
38
+
39
+ await watcher.onMetadataChanged();
40
+
41
+ expect(messages).toEqual(["after compaction"]);
42
+ expect(watcher.lastThreadIndex).toBe(1);
43
+ expect(watcher.lastThreadLength).toBe(compactedThread.length);
44
+ });
45
+
46
+ it("continues rendering after web agent compaction", async () => {
47
+ const client = {
48
+ getTaskDetails: jest.fn().mockResolvedValue({
49
+ data: {
50
+ threads: [oldThread, compactedThread],
51
+ status: "running",
52
+ },
53
+ }),
54
+ };
55
+ const watcher: any = new WebSyncedAgentWatcher(client as any);
56
+ seedCursor(watcher);
57
+
58
+ const messages: string[] = [];
59
+ watcher.agentEvents.on(watcher.eventTypes.agentSay, (data: any) => {
60
+ messages.push(data.message);
61
+ });
62
+
63
+ await watcher.onPoll();
64
+
65
+ expect(messages).toEqual(["after compaction"]);
66
+ expect(watcher.lastThreadIndex).toBe(1);
67
+ expect(watcher.lastThreadLength).toBe(compactedThread.length);
68
+ });
69
+
70
+ it("resets a cursor when a thread shrinks without changing index", async () => {
71
+ const watcher: any = new FsSyncedAgentWatcher();
72
+ seedCursor(watcher);
73
+ watcher.lastThreadLength = 10;
74
+ watcher.readMetadata = jest.fn().mockResolvedValue({
75
+ threads: [compactedThread],
76
+ status: "running",
77
+ });
78
+
79
+ const messages: string[] = [];
80
+ watcher.agentEvents.on(watcher.eventTypes.agentSay, (data: any) => {
81
+ messages.push(data.message);
82
+ });
83
+
84
+ await watcher.onMetadataChanged();
85
+
86
+ expect(messages).toEqual(["after compaction"]);
87
+ expect(watcher.lastThreadLength).toBe(compactedThread.length);
88
+ });
89
+ });
@@ -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": {
@@ -187,10 +187,11 @@ export declare abstract class BaseAgent implements IAgent {
187
187
  stopObserving(id?: string, reason?: string): string;
188
188
  getMessagesLength(messages: Message[]): number;
189
189
  getContextTokenCount(messages: Message[]): number;
190
- detectTruncatedToolCalls(toolCalls: ToolCall[], response: CompletionResponse): {
190
+ detectTruncatedToolCalls(toolCalls: ToolCall[], response: CompletionResponse, allToolCalls?: ToolCall[]): {
191
191
  role: string;
192
192
  content: string;
193
193
  } | null;
194
+ extractContentFromMessages(messages: Message[]): string;
194
195
  getTaskBreakdown(messages: Message[]): Promise<string>;
195
196
  compressMessages(messages: Message[]): Promise<Message[]>;
196
197
  }
@@ -691,6 +691,7 @@ let BaseAgent = class BaseAgent {
691
691
  }
692
692
  }
693
693
  this.log("agent response cost: " + response?.usd_cost);
694
+ const allResponseToolCalls = response.choices.flatMap((c) => c.message.tool_calls ?? []);
694
695
  for (const choice of response.choices) {
695
696
  messages.push(choice.message);
696
697
  messages = await this.messageProcessor.processMessages(messages, "post_call");
@@ -700,7 +701,7 @@ let BaseAgent = class BaseAgent {
700
701
  messages = await this.messageProcessor.processMessages(messages, "pre_tools");
701
702
  const toolCalls = messages[messages.length - 1].tool_calls;
702
703
  this.updateCurrentThread(messages);
703
- const truncationWarning = this.detectTruncatedToolCalls(toolCalls, response);
704
+ const truncationWarning = this.detectTruncatedToolCalls(toolCalls, response, allResponseToolCalls);
704
705
  if (truncationWarning) {
705
706
  messages.push(truncationWarning);
706
707
  this.updateCurrentThread(messages);
@@ -984,12 +985,13 @@ let BaseAgent = class BaseAgent {
984
985
  }
985
986
  return this.getMessagesLength(messages);
986
987
  }
987
- detectTruncatedToolCalls(toolCalls, response) {
988
+ detectTruncatedToolCalls(toolCalls, response, allToolCalls) {
988
989
  const rawOutputTokens = response?.usage?.completion_tokens || 0;
989
990
  const thinkingTokens = (response?.usage?.output_tokens_details?.thinking_tokens ?? 0) +
990
991
  (response?.usage?.output_tokens_details?.reasoning_tokens ?? 0);
991
992
  const outputTokens = Math.max(0, rawOutputTokens - thinkingTokens);
992
- const totalArgLength = toolCalls.reduce((sum, tc) => sum + (tc.function?.arguments?.length || 0), 0);
993
+ const callsForSizeCheck = allToolCalls ?? toolCalls;
994
+ const totalArgLength = callsForSizeCheck.reduce((sum, tc) => sum + (tc.function?.arguments?.length || 0), 0);
993
995
  const expectedArgChars = outputTokens * 4;
994
996
  const suspiciouslySmallArgs = outputTokens > 1000 && totalArgLength < expectedArgChars * 0.1;
995
997
  for (const toolCall of toolCalls) {
@@ -1015,6 +1017,17 @@ let BaseAgent = class BaseAgent {
1015
1017
  }
1016
1018
  return null;
1017
1019
  }
1020
+ extractContentFromMessages(messages) {
1021
+ const content = messages
1022
+ .filter((m) => typeof m.content === "string" && m.content.trim())
1023
+ .map((m) => m.content);
1024
+ const finalAnswer = messages
1025
+ .filter((m) => m.tool_calls?.length)
1026
+ .flatMap((t) => t.tool_calls)
1027
+ .filter((t) => t.function.name === "finalAnswer")
1028
+ .map((t) => t.function.arguments);
1029
+ return [...content, ...finalAnswer].join("\n\n");
1030
+ }
1018
1031
  async getTaskBreakdown(messages) {
1019
1032
  if (this.taskBreakdown) {
1020
1033
  return this.taskBreakdown;
@@ -1037,29 +1050,21 @@ let BaseAgent = class BaseAgent {
1037
1050
  },
1038
1051
  ];
1039
1052
  const response = await this.createAgentCompletion({
1040
- model,
1041
1053
  messages: taskBreakdownMessages,
1042
1054
  tool_choice: "none",
1043
- long_ttl_cache: this.runTime() > 300_000,
1044
- ...(this.reasoningEffort !== undefined && {
1045
- reasoning_effort: this.reasoningEffort,
1046
- }),
1047
- ...(this.summarizeReasoning !== undefined && {
1048
- reasoning_summary: this.summarizeReasoning,
1049
- }),
1050
1055
  });
1051
- const breakdownContent = response.choices
1052
- .map((c) => c.message.content)
1053
- .filter((s) => typeof s === "string" && s.trim().length > 0)
1054
- .join("\n\n");
1055
- this.taskBreakdown = breakdownContent || "";
1056
+ const breakdownContent = this.extractContentFromMessages(response.choices?.map((c) => c.message));
1057
+ if (!breakdownContent) {
1058
+ console.log(JSON.stringify(response.choices, null, 2));
1059
+ throw new Error("Compaction task-breakdown request returned no text content");
1060
+ }
1061
+ this.taskBreakdown = breakdownContent;
1056
1062
  this.log(`task breakdown cost: ${response.usd_cost}`);
1057
- return this.taskBreakdown || undefined;
1063
+ return this.taskBreakdown;
1058
1064
  }
1059
1065
  async compressMessages(messages) {
1060
1066
  let resumeIndex = messages.length - 1;
1061
- while (resumeIndex >= 0 &&
1062
- messages[resumeIndex].role !== "assistant") {
1067
+ while (resumeIndex >= 0 && messages[resumeIndex].role !== "assistant") {
1063
1068
  resumeIndex--;
1064
1069
  }
1065
1070
  const compressionEnd = resumeIndex === -1 ? messages.length : resumeIndex;
@@ -1075,7 +1080,9 @@ let BaseAgent = class BaseAgent {
1075
1080
  3. Next Steps - what we're about to do next to continue the user's original request.
1076
1081
  4. Tasks remaining - what tasks are left from the initial task breakdown.
1077
1082
 
1078
- ${this.taskBreakdown ? `Our initial task breakdown: ${this.taskBreakdown}` : ""}
1083
+ ${this.taskBreakdown
1084
+ ? `Our initial task breakdown: ${this.taskBreakdown}`
1085
+ : ""}
1079
1086
 
1080
1087
  This summary will replace the older history. The latest agent interaction will remain verbatim:
1081
1088
 
@@ -1091,18 +1098,24 @@ let BaseAgent = class BaseAgent {
1091
1098
  messages: compressMessagesPayload,
1092
1099
  tool_choice: "none",
1093
1100
  });
1094
- const summaries = response.choices
1095
- .map((c) => c.message.content)
1096
- .filter((s) => typeof s === "string" && s.trim().length > 0);
1097
- this.summaries.push(...summaries);
1101
+ const summary = this.extractContentFromMessages(response.choices?.map((c) => c.message));
1102
+ if (summary.length === 0) {
1103
+ console.log(JSON.stringify(response.choices, null, 2));
1104
+ throw new Error("Compaction summary request returned no text content");
1105
+ }
1106
+ this.summaries.push(summary);
1098
1107
  const startMessages = [
1099
1108
  {
1100
1109
  role: "user",
1101
1110
  content: `
1102
- ${this.taskBreakdown ? `Initial task breakdown:\n ${this.taskBreakdown}` : "(No task breakdown available — summarize what you know from context above)"}
1111
+ ${this.taskBreakdown
1112
+ ? `Initial task breakdown:\n ${this.taskBreakdown}`
1113
+ : "(No task breakdown available — summarize what you know from context above)"}
1103
1114
 
1104
1115
  We have just compressed the conversation to save memory:
1105
- ${summaries.length > 0 ? JSON.stringify(summaries) : "(summary unavailable — please continue from the task breakdown above)"}
1116
+ ${summary.length > 0
1117
+ ? JSON.stringify(summary)
1118
+ : "(summary unavailable — please continue from the task breakdown above)"}
1106
1119
 
1107
1120
  Please continue the task from where we left off
1108
1121
  `,