@tyvm/knowhow 0.0.156 → 0.0.158

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 (155) hide show
  1. package/README.md +17 -1
  2. package/package.json +1 -1
  3. package/src/agents/base/base.ts +176 -39
  4. package/src/auth/browserLogin.ts +2 -13
  5. package/src/auth/environmentAuth.ts +51 -0
  6. package/src/auth/jwtStore.ts +70 -0
  7. package/src/auth/keyAuth.ts +4 -15
  8. package/src/auth/registerIdentity.ts +8 -8
  9. package/src/chat/modules/AgentModule.ts +3 -3
  10. package/src/chat/modules/RemoteSyncModule.ts +45 -17
  11. package/src/cli.ts +2 -0
  12. package/src/clients/anthropic.ts +156 -79
  13. package/src/clients/attribution.ts +53 -0
  14. package/src/clients/http.ts +211 -84
  15. package/src/clients/index.ts +45 -13
  16. package/src/clients/openai.ts +441 -220
  17. package/src/clients/openrouter.ts +10 -3
  18. package/src/clients/pricing/fireworks.ts +32 -2
  19. package/src/clients/pricing/openai.ts +15 -1
  20. package/src/clients/pricing/types.ts +15 -0
  21. package/src/clients/responseMetadata.ts +221 -0
  22. package/src/clients/types.ts +49 -4
  23. package/src/commands/mcp.ts +1 -0
  24. package/src/commands/misc.ts +1 -1
  25. package/src/commands/remote.ts +77 -0
  26. package/src/config.ts +9 -1
  27. package/src/login.ts +54 -42
  28. package/src/remotes.ts +110 -0
  29. package/src/services/KnowhowClient.ts +12 -16
  30. package/src/services/Mcp.ts +94 -7
  31. package/src/services/McpWebsocketTransport.ts +14 -11
  32. package/src/types.ts +12 -0
  33. package/tests/unit/McpWebsocketTransport.test.ts +56 -0
  34. package/tests/unit/auth/jwtStore.test.ts +55 -0
  35. package/tests/unit/auth/registerIdentity.test.ts +8 -2
  36. package/tests/unit/clients/anthropic.responseMetadata.test.ts +81 -0
  37. package/tests/unit/clients/attribution.test.ts +185 -0
  38. package/tests/unit/clients/http.responseMetadata.test.ts +86 -0
  39. package/tests/unit/clients/openai.responseMetadata.test.ts +90 -0
  40. package/tests/unit/clients/responseMetadata.test.ts +123 -0
  41. package/tests/unit/commands/remote.test.ts +79 -0
  42. package/tests/unit/login.test.ts +64 -3
  43. package/tests/unit/services/KnowhowClient.test.ts +2 -2
  44. package/tests/unit/services/Mcp.test.ts +2 -2
  45. package/ts_build/package.json +1 -1
  46. package/ts_build/src/agents/base/base.d.ts +8 -3
  47. package/ts_build/src/agents/base/base.js +112 -26
  48. package/ts_build/src/agents/base/base.js.map +1 -1
  49. package/ts_build/src/auth/browserLogin.js +2 -7
  50. package/ts_build/src/auth/browserLogin.js.map +1 -1
  51. package/ts_build/src/auth/environmentAuth.d.ts +5 -0
  52. package/ts_build/src/auth/environmentAuth.js +48 -0
  53. package/ts_build/src/auth/environmentAuth.js.map +1 -0
  54. package/ts_build/src/auth/jwtStore.d.ts +4 -0
  55. package/ts_build/src/auth/jwtStore.js +65 -0
  56. package/ts_build/src/auth/jwtStore.js.map +1 -0
  57. package/ts_build/src/auth/keyAuth.d.ts +1 -1
  58. package/ts_build/src/auth/keyAuth.js +4 -16
  59. package/ts_build/src/auth/keyAuth.js.map +1 -1
  60. package/ts_build/src/auth/registerIdentity.js +6 -7
  61. package/ts_build/src/auth/registerIdentity.js.map +1 -1
  62. package/ts_build/src/chat/modules/AgentModule.js +3 -3
  63. package/ts_build/src/chat/modules/AgentModule.js.map +1 -1
  64. package/ts_build/src/chat/modules/RemoteSyncModule.d.ts +1 -0
  65. package/ts_build/src/chat/modules/RemoteSyncModule.js +19 -7
  66. package/ts_build/src/chat/modules/RemoteSyncModule.js.map +1 -1
  67. package/ts_build/src/cli.js +2 -0
  68. package/ts_build/src/cli.js.map +1 -1
  69. package/ts_build/src/clients/anthropic.d.ts +1 -0
  70. package/ts_build/src/clients/anthropic.js +83 -38
  71. package/ts_build/src/clients/anthropic.js.map +1 -1
  72. package/ts_build/src/clients/attribution.d.ts +2 -0
  73. package/ts_build/src/clients/attribution.js +32 -0
  74. package/ts_build/src/clients/attribution.js.map +1 -0
  75. package/ts_build/src/clients/http.js +100 -20
  76. package/ts_build/src/clients/http.js.map +1 -1
  77. package/ts_build/src/clients/index.d.ts +2 -1
  78. package/ts_build/src/clients/index.js +24 -6
  79. package/ts_build/src/clients/index.js.map +1 -1
  80. package/ts_build/src/clients/openai.d.ts +2 -1
  81. package/ts_build/src/clients/openai.js +225 -99
  82. package/ts_build/src/clients/openai.js.map +1 -1
  83. package/ts_build/src/clients/openrouter.d.ts +2 -2
  84. package/ts_build/src/clients/openrouter.js +6 -2
  85. package/ts_build/src/clients/openrouter.js.map +1 -1
  86. package/ts_build/src/clients/pricing/fireworks.d.ts +4 -0
  87. package/ts_build/src/clients/pricing/fireworks.js +14 -2
  88. package/ts_build/src/clients/pricing/fireworks.js.map +1 -1
  89. package/ts_build/src/clients/pricing/openai.js +15 -1
  90. package/ts_build/src/clients/pricing/openai.js.map +1 -1
  91. package/ts_build/src/clients/pricing/types.d.ts +6 -0
  92. package/ts_build/src/clients/pricing/types.js.map +1 -1
  93. package/ts_build/src/clients/responseMetadata.d.ts +15 -0
  94. package/ts_build/src/clients/responseMetadata.js +141 -0
  95. package/ts_build/src/clients/responseMetadata.js.map +1 -0
  96. package/ts_build/src/clients/types.d.ts +18 -1
  97. package/ts_build/src/clients/types.js +2 -1
  98. package/ts_build/src/clients/types.js.map +1 -1
  99. package/ts_build/src/commands/mcp.js.map +1 -1
  100. package/ts_build/src/commands/misc.js +1 -1
  101. package/ts_build/src/commands/misc.js.map +1 -1
  102. package/ts_build/src/commands/remote.d.ts +2 -0
  103. package/ts_build/src/commands/remote.js +70 -0
  104. package/ts_build/src/commands/remote.js.map +1 -0
  105. package/ts_build/src/config.js +8 -0
  106. package/ts_build/src/config.js.map +1 -1
  107. package/ts_build/src/login.d.ts +4 -3
  108. package/ts_build/src/login.js +34 -35
  109. package/ts_build/src/login.js.map +1 -1
  110. package/ts_build/src/remotes.d.ts +14 -0
  111. package/ts_build/src/remotes.js +117 -0
  112. package/ts_build/src/remotes.js.map +1 -0
  113. package/ts_build/src/services/KnowhowClient.d.ts +1 -1
  114. package/ts_build/src/services/KnowhowClient.js +12 -14
  115. package/ts_build/src/services/KnowhowClient.js.map +1 -1
  116. package/ts_build/src/services/Mcp.d.ts +1 -1
  117. package/ts_build/src/services/Mcp.js +68 -7
  118. package/ts_build/src/services/Mcp.js.map +1 -1
  119. package/ts_build/src/services/McpWebsocketTransport.d.ts +1 -0
  120. package/ts_build/src/services/McpWebsocketTransport.js +11 -6
  121. package/ts_build/src/services/McpWebsocketTransport.js.map +1 -1
  122. package/ts_build/src/types.d.ts +13 -0
  123. package/ts_build/src/types.js.map +1 -1
  124. package/ts_build/tests/unit/McpWebsocketTransport.test.d.ts +1 -0
  125. package/ts_build/tests/unit/McpWebsocketTransport.test.js +47 -0
  126. package/ts_build/tests/unit/McpWebsocketTransport.test.js.map +1 -0
  127. package/ts_build/tests/unit/auth/jwtStore.test.d.ts +1 -0
  128. package/ts_build/tests/unit/auth/jwtStore.test.js +46 -0
  129. package/ts_build/tests/unit/auth/jwtStore.test.js.map +1 -0
  130. package/ts_build/tests/unit/auth/registerIdentity.test.js +8 -2
  131. package/ts_build/tests/unit/auth/registerIdentity.test.js.map +1 -1
  132. package/ts_build/tests/unit/clients/anthropic.responseMetadata.test.d.ts +1 -0
  133. package/ts_build/tests/unit/clients/anthropic.responseMetadata.test.js +77 -0
  134. package/ts_build/tests/unit/clients/anthropic.responseMetadata.test.js.map +1 -0
  135. package/ts_build/tests/unit/clients/attribution.test.d.ts +1 -0
  136. package/ts_build/tests/unit/clients/attribution.test.js +153 -0
  137. package/ts_build/tests/unit/clients/attribution.test.js.map +1 -0
  138. package/ts_build/tests/unit/clients/http.responseMetadata.test.d.ts +1 -0
  139. package/ts_build/tests/unit/clients/http.responseMetadata.test.js +77 -0
  140. package/ts_build/tests/unit/clients/http.responseMetadata.test.js.map +1 -0
  141. package/ts_build/tests/unit/clients/openai.responseMetadata.test.d.ts +1 -0
  142. package/ts_build/tests/unit/clients/openai.responseMetadata.test.js +86 -0
  143. package/ts_build/tests/unit/clients/openai.responseMetadata.test.js.map +1 -0
  144. package/ts_build/tests/unit/clients/responseMetadata.test.d.ts +1 -0
  145. package/ts_build/tests/unit/clients/responseMetadata.test.js +96 -0
  146. package/ts_build/tests/unit/clients/responseMetadata.test.js.map +1 -0
  147. package/ts_build/tests/unit/commands/remote.test.d.ts +1 -0
  148. package/ts_build/tests/unit/commands/remote.test.js +70 -0
  149. package/ts_build/tests/unit/commands/remote.test.js.map +1 -0
  150. package/ts_build/tests/unit/login.test.js +44 -3
  151. package/ts_build/tests/unit/login.test.js.map +1 -1
  152. package/ts_build/tests/unit/services/KnowhowClient.test.js +2 -2
  153. package/ts_build/tests/unit/services/KnowhowClient.test.js.map +1 -1
  154. package/ts_build/tests/unit/services/Mcp.test.js +2 -2
  155. package/ts_build/tests/unit/services/Mcp.test.js.map +1 -1
@@ -0,0 +1,79 @@
1
+ import { Command } from "commander";
2
+
3
+ const getConfig = jest.fn();
4
+ const updateConfig = jest.fn();
5
+
6
+ jest.mock("../../../src/config", () => ({ getConfig, updateConfig }));
7
+
8
+ import { addRemoteCommand } from "../../../src/commands/remote";
9
+
10
+ describe("remote command", () => {
11
+ let program: Command;
12
+
13
+ beforeEach(() => {
14
+ jest.clearAllMocks();
15
+ jest.spyOn(console, "log").mockImplementation(() => undefined);
16
+ program = new Command();
17
+ program.exitOverride();
18
+ addRemoteCommand(program);
19
+ });
20
+
21
+ afterEach(() => jest.restoreAllMocks());
22
+
23
+ it("adds a named remote with an independent JWT path", async () => {
24
+ const config: any = {
25
+ orgIds: {
26
+ "http://localhost:4000": "org-local",
27
+ },
28
+ };
29
+ getConfig.mockResolvedValue(config);
30
+
31
+ await program.parseAsync([
32
+ "node",
33
+ "knowhow",
34
+ "remote",
35
+ "add",
36
+ "dev",
37
+ "https://api.dev.knowhow.tyvm.ai/",
38
+ ]);
39
+
40
+ expect(updateConfig).toHaveBeenCalledWith(config);
41
+ expect(config.remotes.dev).toEqual({
42
+ name: "dev",
43
+ apiUrl: "https://api.dev.knowhow.tyvm.ai",
44
+ jwtPath: ".knowhow/.jwt.dev",
45
+ });
46
+ expect(config.orgIds).toBeUndefined();
47
+ expect(config.remotes.local).toEqual({
48
+ name: "local",
49
+ apiUrl: "http://localhost:4000",
50
+ jwtPath: ".knowhow/.jwt.local",
51
+ orgId: "org-local",
52
+ });
53
+ });
54
+
55
+ it("selects a configured remote for subsequent commands", async () => {
56
+ const config: any = {
57
+ remotes: {
58
+ local: {
59
+ name: "local",
60
+ apiUrl: "http://localhost:4000",
61
+ jwtPath: ".knowhow/.jwt.local",
62
+ orgId: "org-local",
63
+ },
64
+ },
65
+ };
66
+ getConfig.mockResolvedValue(config);
67
+
68
+ await program.parseAsync([
69
+ "node",
70
+ "knowhow",
71
+ "remote",
72
+ "use",
73
+ "local",
74
+ ]);
75
+
76
+ expect(config.activeRemote).toBe("local");
77
+ expect(updateConfig).toHaveBeenCalledWith(config);
78
+ });
79
+ });
@@ -48,7 +48,7 @@ describe("login public-key bootstrap", () => {
48
48
  jest.spyOn(console, "warn").mockImplementation(() => undefined);
49
49
  workingDirectory = fs.mkdtempSync(path.join(os.tmpdir(), "knowhow-login-"));
50
50
  fs.mkdirSync(path.join(workingDirectory, ".knowhow"));
51
- fs.writeFileSync(path.join(workingDirectory, ".knowhow", ".jwt"), "raw-jwt");
51
+ fs.writeFileSync(path.join(workingDirectory, ".knowhow", ".jwt.api.example"), "raw-jwt");
52
52
  process.chdir(workingDirectory);
53
53
  hasKeyPair.mockReturnValue(false);
54
54
  browserLogin.mockResolvedValue(undefined);
@@ -87,7 +87,42 @@ describe("login public-key bootstrap", () => {
87
87
  "https://api.example"
88
88
  );
89
89
  expect(updateConfig).toHaveBeenCalledWith(
90
- expect.objectContaining({ orgId: "org-1", modelProviders: [{ provider: "knowhow" }] })
90
+ expect.objectContaining({
91
+ activeRemote: "api.example",
92
+ modelProviders: [{ provider: "knowhow" }],
93
+ remotes: expect.objectContaining({
94
+ "api.example": {
95
+ name: "api.example",
96
+ apiUrl: "https://api.example",
97
+ jwtPath: ".knowhow/.jwt.api.example",
98
+ orgId: "org-1",
99
+ },
100
+ }),
101
+ })
102
+ );
103
+ });
104
+
105
+ it("persists an explicit --api-url as the authenticated remote", async () => {
106
+ fs.writeFileSync(path.join(workingDirectory, ".knowhow", ".jwt.local"), "local-jwt");
107
+
108
+ await login(false, undefined, "http://localhost:4000/", "local");
109
+
110
+ expect(updateConfig).toHaveBeenCalledWith(
111
+ expect.objectContaining({
112
+ activeRemote: "local",
113
+ remotes: expect.objectContaining({
114
+ local: {
115
+ name: "local",
116
+ apiUrl: "http://localhost:4000",
117
+ jwtPath: ".knowhow/.jwt.local",
118
+ orgId: "org-1",
119
+ },
120
+ }),
121
+ })
122
+ );
123
+ expect(httpGet).toHaveBeenCalledWith(
124
+ "http://localhost:4000/api/users/me",
125
+ expect.any(Object)
91
126
  );
92
127
  });
93
128
 
@@ -119,9 +154,9 @@ describe("login public-key bootstrap", () => {
119
154
  expect(getOrCreatePublicKey).toHaveBeenCalledWith("/keys/default");
120
155
  expect(updateConfig).toHaveBeenCalledWith(
121
156
  expect.objectContaining({
122
- orgId: "org-1",
123
157
  cliIdentityPath: "/keys/default",
124
158
  modelProviders: [{ provider: "knowhow" }],
159
+ remotes: expect.objectContaining({ "api.example": expect.objectContaining({ orgId: "org-1" }) }),
125
160
  })
126
161
  );
127
162
  });
@@ -143,6 +178,32 @@ describe("login public-key bootstrap", () => {
143
178
  expect(getOrCreatePublicKey).toHaveBeenCalledWith("/keys/dev");
144
179
  });
145
180
 
181
+ it("uses the selected API's JWT org instead of an org saved by another environment", async () => {
182
+ const devJwt = `${Buffer.from("{}").toString("base64url")}.${Buffer.from(
183
+ JSON.stringify({ org: "org-dev" })
184
+ ).toString("base64url")}.signature`;
185
+ fs.writeFileSync(
186
+ path.join(workingDirectory, ".knowhow", ".jwt.api.example"),
187
+ devJwt
188
+ );
189
+ hasKeyPair.mockReturnValue(true);
190
+ getConfig.mockResolvedValue({
191
+ orgId: "org-local",
192
+ orgIds: { "http://localhost:4000": "org-local" },
193
+ modelProviders: [],
194
+ });
195
+ authenticateWithKey.mockResolvedValueOnce(true);
196
+
197
+ await expect(login(false, "/keys/default")).resolves.toBeUndefined();
198
+
199
+ expect(authenticateWithKey).toHaveBeenCalledWith(
200
+ "org-dev",
201
+ "https://api.example",
202
+ "/keys/default"
203
+ );
204
+ expect(browserLogin).not.toHaveBeenCalled();
205
+ });
206
+
146
207
  it("does not attempt key authentication without a configured organization", async () => {
147
208
  hasKeyPair.mockReturnValue(true);
148
209
  getConfig.mockResolvedValue({ modelProviders: [] });
@@ -54,7 +54,7 @@ describe("KnowhowSimpleClient identity renewal", () => {
54
54
  "https://api.example.test",
55
55
  "/tmp/identity"
56
56
  );
57
- expect(mockStoreJwt).toHaveBeenCalledWith("fresh-jwt");
57
+ expect(mockStoreJwt).toHaveBeenCalledWith("fresh-jwt", "https://api.example.test");
58
58
  expect(mockGet.mock.calls[0][1].headers.Authorization).toBe("Bearer fresh-jwt");
59
59
  });
60
60
 
@@ -77,7 +77,7 @@ describe("KnowhowSimpleClient identity renewal", () => {
77
77
  "https://api.example.test",
78
78
  "/tmp/identity"
79
79
  );
80
- expect(mockStoreJwt).toHaveBeenCalledWith("fresh-jwt");
80
+ expect(mockStoreJwt).toHaveBeenCalledWith("fresh-jwt", "https://api.example.test");
81
81
  expect(mockGet).toHaveBeenCalledTimes(1);
82
82
  expect(mockGet.mock.calls[0][1].headers.Authorization).toBe("Bearer fresh-jwt");
83
83
  });
@@ -33,11 +33,11 @@ describe("McpService JWT file transport", () => {
33
33
  fs.rmSync(temporaryDirectory, { recursive: true, force: true });
34
34
  });
35
35
 
36
- it("reads the refreshed JWT when the transport is created", () => {
36
+ it("reads the refreshed JWT when the transport is created", async () => {
37
37
  fs.writeFileSync(tokenFile, "expired-jwt");
38
38
  fs.writeFileSync(tokenFile, "fresh-jwt");
39
39
 
40
- new McpService().createTransport({
40
+ await new McpService().createTransport({
41
41
  name: "authenticated-remote",
42
42
  url: "https://api.example.test/mcp",
43
43
  authorization_token_file: tokenFile,
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tyvm/knowhow",
3
- "version": "0.0.156",
3
+ "version": "0.0.158",
4
4
  "description": "ai cli with plugins and agents",
5
5
  "main": "ts_build/src/index.js",
6
6
  "bin": {
@@ -70,6 +70,9 @@ export declare abstract class BaseAgent implements IAgent {
70
70
  protected compressThreshold: number;
71
71
  protected compressMinMessages: number;
72
72
  protected _forceCompact: boolean;
73
+ private _compactionPromise;
74
+ private _completedCompaction;
75
+ private _compactionGeneration;
73
76
  private _interruptResolve;
74
77
  private _interruptToken;
75
78
  protected threads: Message[][];
@@ -147,7 +150,7 @@ export declare abstract class BaseAgent implements IAgent {
147
150
  getTurnCount(): number;
148
151
  adjustTotalCostUsd(cost: number): void;
149
152
  getTotalCostUsd(): number;
150
- adjustTokenUsage(usage: any, messages?: Message[]): void;
153
+ adjustTokenUsage(usage: any, messages?: Message[], updateLastPromptTokens?: boolean): void;
151
154
  getTokenUsage(): {
152
155
  totalInputTokens: number;
153
156
  totalOutputTokens: number;
@@ -156,6 +159,7 @@ export declare abstract class BaseAgent implements IAgent {
156
159
  };
157
160
  protected createAgentCompletion(params: CachedChatCompletion, options?: {
158
161
  interruptValue?: CompletionResponse;
162
+ updateLastPromptTokens?: boolean;
159
163
  }): Promise<CompletionResponse>;
160
164
  startNewThread(messages: Message[]): void;
161
165
  updateCurrentThread(messages: Message[]): void;
@@ -192,6 +196,7 @@ export declare abstract class BaseAgent implements IAgent {
192
196
  content: string;
193
197
  } | null;
194
198
  extractContentFromMessages(messages: Message[]): string;
195
- getTaskBreakdown(messages: Message[]): Promise<string>;
196
- compressMessages(messages: Message[]): Promise<Message[]>;
199
+ private startBackgroundCompaction;
200
+ getTaskBreakdown(messages: Message[], cacheResult?: boolean): Promise<string>;
201
+ compressMessages(messages: Message[], onSummary?: (summary: string) => void, taskBreakdown?: string): Promise<Message[]>;
197
202
  }
@@ -44,6 +44,9 @@ let BaseAgent = class BaseAgent {
44
44
  compressThreshold = 30000;
45
45
  compressMinMessages = 30;
46
46
  _forceCompact = false;
47
+ _compactionPromise = null;
48
+ _completedCompaction = null;
49
+ _compactionGeneration = 0;
47
50
  _interruptResolve = null;
48
51
  _interruptToken = 0;
49
52
  threads = [];
@@ -141,6 +144,9 @@ let BaseAgent = class BaseAgent {
141
144
  this.maxRunTimeMs = maxRunTimeMs;
142
145
  }
143
146
  newTask(taskId) {
147
+ this._compactionGeneration++;
148
+ this._compactionPromise = null;
149
+ this._completedCompaction = null;
144
150
  this.currentThread = 0;
145
151
  this.threads = [];
146
152
  this.taskBreakdown = "";
@@ -348,7 +354,7 @@ let BaseAgent = class BaseAgent {
348
354
  getTotalCostUsd() {
349
355
  return this.totalCostUsd;
350
356
  }
351
- adjustTokenUsage(usage, messages) {
357
+ adjustTokenUsage(usage, messages, updateLastPromptTokens = true) {
352
358
  if (!usage)
353
359
  return;
354
360
  const inputTokens = usage.input_tokens ?? usage.prompt_tokens ?? 0;
@@ -359,7 +365,7 @@ let BaseAgent = class BaseAgent {
359
365
  0;
360
366
  const cacheWriteTokens = usage.cache_creation_input_tokens ?? usage.cache_write_tokens ?? 0;
361
367
  const promptTokens = inputTokens + cacheReadTokens + cacheWriteTokens;
362
- if (promptTokens > 0) {
368
+ if (updateLastPromptTokens && promptTokens > 0) {
363
369
  this.lastPromptTokens = promptTokens;
364
370
  }
365
371
  this.totalInputTokens += inputTokens;
@@ -388,7 +394,7 @@ let BaseAgent = class BaseAgent {
388
394
  };
389
395
  }
390
396
  async createAgentCompletion(params, options = {}) {
391
- const { interruptValue } = options;
397
+ const { interruptValue, updateLastPromptTokens = true } = options;
392
398
  const defaultedParams = {
393
399
  model: this.getModel(),
394
400
  tools: this.getEnabledTools(),
@@ -407,7 +413,7 @@ let BaseAgent = class BaseAgent {
407
413
  ? await this.makeInterruptible(callPromise, interruptValue)
408
414
  : await callPromise;
409
415
  this.adjustTotalCostUsd(response?.usd_cost);
410
- this.adjustTokenUsage(response?.usage, params.messages);
416
+ this.adjustTokenUsage(response?.usage, params.messages, updateLastPromptTokens);
411
417
  return response;
412
418
  }
413
419
  startNewThread(messages) {
@@ -774,25 +780,56 @@ let BaseAgent = class BaseAgent {
774
780
  this.agentEvents.emit(this.eventTypes.done, firstMessage.content);
775
781
  return firstMessage.content;
776
782
  }
783
+ const completedCompaction = this._completedCompaction;
784
+ this._completedCompaction = null;
785
+ let appliedCompaction = false;
786
+ if (completedCompaction) {
787
+ const currentPrefix = messages.slice(0, completedCompaction.compressionEnd);
788
+ const prefixIsUnchanged = currentPrefix.every((message, index) => message === completedCompaction.originalPrefix[index] ||
789
+ JSON.stringify(message) ===
790
+ JSON.stringify(completedCompaction.originalPrefix[index]));
791
+ if (currentPrefix.length === completedCompaction.compressionEnd &&
792
+ prefixIsUnchanged) {
793
+ messages = [
794
+ ...completedCompaction.replacementPrefix,
795
+ ...messages.slice(completedCompaction.compressionEnd),
796
+ ];
797
+ this.summaries.push(completedCompaction.summary);
798
+ this.startNewThread(messages);
799
+ this.lastPromptTokens = 0;
800
+ appliedCompaction = true;
801
+ }
802
+ else {
803
+ this.log("Discarding completed compaction because its source prefix changed; a later turn will retry", "warn");
804
+ }
805
+ }
777
806
  const contextTokens = this.getContextTokenCount(messages);
778
807
  const overThreshold = contextTokens > this.getCompressThreshold() &&
779
808
  messages.length > this.compressMinMessages;
780
- if (overThreshold || this._forceCompact) {
781
- const taskBreakdown = await this.getTaskBreakdown(messages);
809
+ if (!appliedCompaction &&
810
+ !this._compactionPromise &&
811
+ (overThreshold || this._forceCompact)) {
782
812
  this.log(this._forceCompact
783
- ? `Compacting messages (manual /compact request): ${contextTokens} tokens`
784
- : `Compressing messages: ${contextTokens} tokens exceeds ${this.getCompressThreshold()}`);
813
+ ? `Starting background compaction (manual /compact request): ${contextTokens} tokens`
814
+ : `Starting background compaction: ${contextTokens} tokens exceeds ${this.getCompressThreshold()}`);
785
815
  this._forceCompact = false;
786
- messages = await this.compressMessages(messages);
787
- this.startNewThread(messages);
788
- this.lastPromptTokens = 0;
816
+ this.startBackgroundCompaction(messages);
789
817
  }
790
818
  if (["assistant", "tool"].includes(messages[messages.length - 1].role)) {
791
819
  const statusMessage = this.getStatusMessage();
792
820
  this.logStatus();
793
821
  const continuation = `<Workflow>
794
- Task terminates after you call on of these tools: ${JSON.stringify(this.requiredToolNames)}.\n
795
- User likely only sees output from the required tool call.
822
+ This is an automated runtime instruction, not a new user message.
823
+ The original user request remains active. Do not say that the user's
824
+ message was empty, and do not ask the user to repeat the request.
825
+
826
+ Your previous response did not terminate the task because it did not
827
+ call one of the required tools: ${JSON.stringify(this.requiredToolNames)}.
828
+ If your previous response was intended as the final response, call the
829
+ appropriate required tool now with that response. Otherwise, continue
830
+ working on the original request and call an appropriate required tool
831
+ when the task is complete.
832
+
796
833
  <TaskStatus>${statusMessage}</TaskStatus>
797
834
  </Workflow>`;
798
835
  messages.push({
@@ -1028,12 +1065,53 @@ let BaseAgent = class BaseAgent {
1028
1065
  .map((t) => t.function.arguments);
1029
1066
  return [...content, ...finalAnswer].join("\n\n");
1030
1067
  }
1031
- async getTaskBreakdown(messages) {
1068
+ startBackgroundCompaction(messages) {
1069
+ const snapshot = messages.slice();
1070
+ let resumeIndex = snapshot.length - 1;
1071
+ while (resumeIndex >= 0 && snapshot[resumeIndex].role !== "assistant") {
1072
+ resumeIndex--;
1073
+ }
1074
+ const compressionEnd = resumeIndex === -1 ? snapshot.length : resumeIndex;
1075
+ const generation = this._compactionGeneration;
1076
+ const promise = (async () => {
1077
+ const taskBreakdown = await this.getTaskBreakdown(snapshot, false);
1078
+ if (generation !== this._compactionGeneration)
1079
+ return;
1080
+ this.taskBreakdown = taskBreakdown;
1081
+ let summary = "";
1082
+ const compactedSnapshot = await this.compressMessages(snapshot, (generatedSummary) => {
1083
+ summary = generatedSummary;
1084
+ }, taskBreakdown);
1085
+ if (generation !== this._compactionGeneration)
1086
+ return;
1087
+ const preservedSnapshotLength = snapshot.length - compressionEnd;
1088
+ this._completedCompaction = {
1089
+ compressionEnd,
1090
+ originalPrefix: snapshot.slice(0, compressionEnd),
1091
+ replacementPrefix: compactedSnapshot.slice(0, compactedSnapshot.length - preservedSnapshotLength),
1092
+ summary,
1093
+ };
1094
+ this.log("Background compaction completed; it will be applied next turn");
1095
+ })();
1096
+ this._compactionPromise = promise;
1097
+ promise
1098
+ .catch((error) => {
1099
+ if (generation === this._compactionGeneration) {
1100
+ this.log(`Background compaction failed; a later turn will retry: ${error}`, "warn");
1101
+ }
1102
+ })
1103
+ .finally(() => {
1104
+ if (this._compactionPromise === promise) {
1105
+ this._compactionPromise = null;
1106
+ }
1107
+ });
1108
+ }
1109
+ async getTaskBreakdown(messages, cacheResult = true) {
1032
1110
  if (this.taskBreakdown) {
1033
1111
  return this.taskBreakdown;
1034
1112
  }
1035
1113
  const taskPrompt = `
1036
- Analyze all previous messages.
1114
+ TASK INTERRUPT. CONTEXT COMPACTION NEEDED. Analyze all previous messages.
1037
1115
 
1038
1116
  Generate a detailed task breakdown for this conversation, include a section for the following:
1039
1117
  1. Task List
@@ -1052,17 +1130,19 @@ let BaseAgent = class BaseAgent {
1052
1130
  const response = await this.createAgentCompletion({
1053
1131
  messages: taskBreakdownMessages,
1054
1132
  tool_choice: "none",
1055
- });
1133
+ }, { updateLastPromptTokens: false });
1056
1134
  const breakdownContent = this.extractContentFromMessages(response.choices?.map((c) => c.message));
1057
1135
  if (!breakdownContent) {
1058
1136
  console.log(JSON.stringify(response.choices, null, 2));
1059
1137
  throw new Error("Compaction task-breakdown request returned no text content");
1060
1138
  }
1061
- this.taskBreakdown = breakdownContent;
1139
+ if (cacheResult) {
1140
+ this.taskBreakdown = breakdownContent;
1141
+ }
1062
1142
  this.log(`task breakdown cost: ${response.usd_cost}`);
1063
- return this.taskBreakdown;
1143
+ return breakdownContent;
1064
1144
  }
1065
- async compressMessages(messages) {
1145
+ async compressMessages(messages, onSummary, taskBreakdown = this.taskBreakdown) {
1066
1146
  let resumeIndex = messages.length - 1;
1067
1147
  while (resumeIndex >= 0 && messages[resumeIndex].role !== "assistant") {
1068
1148
  resumeIndex--;
@@ -1071,7 +1151,8 @@ let BaseAgent = class BaseAgent {
1071
1151
  const toCompress = messages.slice(0, compressionEnd);
1072
1152
  const resumeMessages = messages.slice(compressionEnd);
1073
1153
  this.log(`Compressing messages from 0 to ${compressionEnd}, resuming from ${resumeIndex}, total messages: ${messages.length}`);
1074
- const toCompressPrompt = `We are compressing our conversation to save memory.
1154
+ const toCompressPrompt = `TASK INTERRUPT. CONTEXT COMPACTION NEEDED.
1155
+ We are compressing our conversation to save memory.
1075
1156
  Please summarize the conversation so far, so that we may continue the original task with a smaller context
1076
1157
 
1077
1158
  Include the following sections:
@@ -1080,8 +1161,8 @@ let BaseAgent = class BaseAgent {
1080
1161
  3. Next Steps - what we're about to do next to continue the user's original request.
1081
1162
  4. Tasks remaining - what tasks are left from the initial task breakdown.
1082
1163
 
1083
- ${this.taskBreakdown
1084
- ? `Our initial task breakdown: ${this.taskBreakdown}`
1164
+ ${taskBreakdown
1165
+ ? `Our initial task breakdown: ${taskBreakdown}`
1085
1166
  : ""}
1086
1167
 
1087
1168
  This summary will replace the older history. The latest agent interaction will remain verbatim:
@@ -1097,19 +1178,24 @@ let BaseAgent = class BaseAgent {
1097
1178
  const response = await this.createAgentCompletion({
1098
1179
  messages: compressMessagesPayload,
1099
1180
  tool_choice: "none",
1100
- });
1181
+ }, { updateLastPromptTokens: false });
1101
1182
  const summary = this.extractContentFromMessages(response.choices?.map((c) => c.message));
1102
1183
  if (summary.length === 0) {
1103
1184
  console.log(JSON.stringify(response.choices, null, 2));
1104
1185
  throw new Error("Compaction summary request returned no text content");
1105
1186
  }
1106
- this.summaries.push(summary);
1187
+ if (onSummary) {
1188
+ onSummary(summary);
1189
+ }
1190
+ else {
1191
+ this.summaries.push(summary);
1192
+ }
1107
1193
  const startMessages = [
1108
1194
  {
1109
1195
  role: "user",
1110
1196
  content: `
1111
- ${this.taskBreakdown
1112
- ? `Initial task breakdown:\n ${this.taskBreakdown}`
1197
+ ${taskBreakdown
1198
+ ? `Initial task breakdown:\n ${taskBreakdown}`
1113
1199
  : "(No task breakdown available — summarize what you know from context above)"}
1114
1200
 
1115
1201
  We have just compressed the conversation to save memory: