@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.
- package/README.md +17 -1
- package/package.json +1 -1
- package/src/agents/base/base.ts +176 -39
- package/src/auth/browserLogin.ts +2 -13
- package/src/auth/environmentAuth.ts +51 -0
- package/src/auth/jwtStore.ts +70 -0
- package/src/auth/keyAuth.ts +4 -15
- package/src/auth/registerIdentity.ts +8 -8
- package/src/chat/modules/AgentModule.ts +3 -3
- package/src/chat/modules/RemoteSyncModule.ts +45 -17
- package/src/cli.ts +2 -0
- package/src/clients/anthropic.ts +156 -79
- package/src/clients/attribution.ts +53 -0
- package/src/clients/http.ts +211 -84
- package/src/clients/index.ts +45 -13
- package/src/clients/openai.ts +441 -220
- package/src/clients/openrouter.ts +10 -3
- package/src/clients/pricing/fireworks.ts +32 -2
- package/src/clients/pricing/openai.ts +15 -1
- package/src/clients/pricing/types.ts +15 -0
- package/src/clients/responseMetadata.ts +221 -0
- package/src/clients/types.ts +49 -4
- package/src/commands/mcp.ts +1 -0
- package/src/commands/misc.ts +1 -1
- package/src/commands/remote.ts +77 -0
- package/src/config.ts +9 -1
- package/src/login.ts +54 -42
- package/src/remotes.ts +110 -0
- package/src/services/KnowhowClient.ts +12 -16
- package/src/services/Mcp.ts +94 -7
- package/src/services/McpWebsocketTransport.ts +14 -11
- package/src/types.ts +12 -0
- package/tests/unit/McpWebsocketTransport.test.ts +56 -0
- package/tests/unit/auth/jwtStore.test.ts +55 -0
- package/tests/unit/auth/registerIdentity.test.ts +8 -2
- package/tests/unit/clients/anthropic.responseMetadata.test.ts +81 -0
- package/tests/unit/clients/attribution.test.ts +185 -0
- package/tests/unit/clients/http.responseMetadata.test.ts +86 -0
- package/tests/unit/clients/openai.responseMetadata.test.ts +90 -0
- package/tests/unit/clients/responseMetadata.test.ts +123 -0
- package/tests/unit/commands/remote.test.ts +79 -0
- package/tests/unit/login.test.ts +64 -3
- package/tests/unit/services/KnowhowClient.test.ts +2 -2
- package/tests/unit/services/Mcp.test.ts +2 -2
- package/ts_build/package.json +1 -1
- package/ts_build/src/agents/base/base.d.ts +8 -3
- package/ts_build/src/agents/base/base.js +112 -26
- package/ts_build/src/agents/base/base.js.map +1 -1
- package/ts_build/src/auth/browserLogin.js +2 -7
- package/ts_build/src/auth/browserLogin.js.map +1 -1
- package/ts_build/src/auth/environmentAuth.d.ts +5 -0
- package/ts_build/src/auth/environmentAuth.js +48 -0
- package/ts_build/src/auth/environmentAuth.js.map +1 -0
- package/ts_build/src/auth/jwtStore.d.ts +4 -0
- package/ts_build/src/auth/jwtStore.js +65 -0
- package/ts_build/src/auth/jwtStore.js.map +1 -0
- package/ts_build/src/auth/keyAuth.d.ts +1 -1
- package/ts_build/src/auth/keyAuth.js +4 -16
- package/ts_build/src/auth/keyAuth.js.map +1 -1
- package/ts_build/src/auth/registerIdentity.js +6 -7
- package/ts_build/src/auth/registerIdentity.js.map +1 -1
- package/ts_build/src/chat/modules/AgentModule.js +3 -3
- package/ts_build/src/chat/modules/AgentModule.js.map +1 -1
- package/ts_build/src/chat/modules/RemoteSyncModule.d.ts +1 -0
- package/ts_build/src/chat/modules/RemoteSyncModule.js +19 -7
- package/ts_build/src/chat/modules/RemoteSyncModule.js.map +1 -1
- package/ts_build/src/cli.js +2 -0
- package/ts_build/src/cli.js.map +1 -1
- package/ts_build/src/clients/anthropic.d.ts +1 -0
- package/ts_build/src/clients/anthropic.js +83 -38
- package/ts_build/src/clients/anthropic.js.map +1 -1
- package/ts_build/src/clients/attribution.d.ts +2 -0
- package/ts_build/src/clients/attribution.js +32 -0
- package/ts_build/src/clients/attribution.js.map +1 -0
- package/ts_build/src/clients/http.js +100 -20
- package/ts_build/src/clients/http.js.map +1 -1
- package/ts_build/src/clients/index.d.ts +2 -1
- package/ts_build/src/clients/index.js +24 -6
- package/ts_build/src/clients/index.js.map +1 -1
- package/ts_build/src/clients/openai.d.ts +2 -1
- package/ts_build/src/clients/openai.js +225 -99
- package/ts_build/src/clients/openai.js.map +1 -1
- package/ts_build/src/clients/openrouter.d.ts +2 -2
- package/ts_build/src/clients/openrouter.js +6 -2
- package/ts_build/src/clients/openrouter.js.map +1 -1
- package/ts_build/src/clients/pricing/fireworks.d.ts +4 -0
- package/ts_build/src/clients/pricing/fireworks.js +14 -2
- package/ts_build/src/clients/pricing/fireworks.js.map +1 -1
- package/ts_build/src/clients/pricing/openai.js +15 -1
- package/ts_build/src/clients/pricing/openai.js.map +1 -1
- package/ts_build/src/clients/pricing/types.d.ts +6 -0
- package/ts_build/src/clients/pricing/types.js.map +1 -1
- package/ts_build/src/clients/responseMetadata.d.ts +15 -0
- package/ts_build/src/clients/responseMetadata.js +141 -0
- package/ts_build/src/clients/responseMetadata.js.map +1 -0
- package/ts_build/src/clients/types.d.ts +18 -1
- package/ts_build/src/clients/types.js +2 -1
- package/ts_build/src/clients/types.js.map +1 -1
- package/ts_build/src/commands/mcp.js.map +1 -1
- package/ts_build/src/commands/misc.js +1 -1
- package/ts_build/src/commands/misc.js.map +1 -1
- package/ts_build/src/commands/remote.d.ts +2 -0
- package/ts_build/src/commands/remote.js +70 -0
- package/ts_build/src/commands/remote.js.map +1 -0
- package/ts_build/src/config.js +8 -0
- package/ts_build/src/config.js.map +1 -1
- package/ts_build/src/login.d.ts +4 -3
- package/ts_build/src/login.js +34 -35
- package/ts_build/src/login.js.map +1 -1
- package/ts_build/src/remotes.d.ts +14 -0
- package/ts_build/src/remotes.js +117 -0
- package/ts_build/src/remotes.js.map +1 -0
- package/ts_build/src/services/KnowhowClient.d.ts +1 -1
- package/ts_build/src/services/KnowhowClient.js +12 -14
- package/ts_build/src/services/KnowhowClient.js.map +1 -1
- package/ts_build/src/services/Mcp.d.ts +1 -1
- package/ts_build/src/services/Mcp.js +68 -7
- package/ts_build/src/services/Mcp.js.map +1 -1
- package/ts_build/src/services/McpWebsocketTransport.d.ts +1 -0
- package/ts_build/src/services/McpWebsocketTransport.js +11 -6
- package/ts_build/src/services/McpWebsocketTransport.js.map +1 -1
- package/ts_build/src/types.d.ts +13 -0
- package/ts_build/src/types.js.map +1 -1
- package/ts_build/tests/unit/McpWebsocketTransport.test.d.ts +1 -0
- package/ts_build/tests/unit/McpWebsocketTransport.test.js +47 -0
- package/ts_build/tests/unit/McpWebsocketTransport.test.js.map +1 -0
- package/ts_build/tests/unit/auth/jwtStore.test.d.ts +1 -0
- package/ts_build/tests/unit/auth/jwtStore.test.js +46 -0
- package/ts_build/tests/unit/auth/jwtStore.test.js.map +1 -0
- package/ts_build/tests/unit/auth/registerIdentity.test.js +8 -2
- package/ts_build/tests/unit/auth/registerIdentity.test.js.map +1 -1
- package/ts_build/tests/unit/clients/anthropic.responseMetadata.test.d.ts +1 -0
- package/ts_build/tests/unit/clients/anthropic.responseMetadata.test.js +77 -0
- package/ts_build/tests/unit/clients/anthropic.responseMetadata.test.js.map +1 -0
- package/ts_build/tests/unit/clients/attribution.test.d.ts +1 -0
- package/ts_build/tests/unit/clients/attribution.test.js +153 -0
- package/ts_build/tests/unit/clients/attribution.test.js.map +1 -0
- package/ts_build/tests/unit/clients/http.responseMetadata.test.d.ts +1 -0
- package/ts_build/tests/unit/clients/http.responseMetadata.test.js +77 -0
- package/ts_build/tests/unit/clients/http.responseMetadata.test.js.map +1 -0
- package/ts_build/tests/unit/clients/openai.responseMetadata.test.d.ts +1 -0
- package/ts_build/tests/unit/clients/openai.responseMetadata.test.js +86 -0
- package/ts_build/tests/unit/clients/openai.responseMetadata.test.js.map +1 -0
- package/ts_build/tests/unit/clients/responseMetadata.test.d.ts +1 -0
- package/ts_build/tests/unit/clients/responseMetadata.test.js +96 -0
- package/ts_build/tests/unit/clients/responseMetadata.test.js.map +1 -0
- package/ts_build/tests/unit/commands/remote.test.d.ts +1 -0
- package/ts_build/tests/unit/commands/remote.test.js +70 -0
- package/ts_build/tests/unit/commands/remote.test.js.map +1 -0
- package/ts_build/tests/unit/login.test.js +44 -3
- package/ts_build/tests/unit/login.test.js.map +1 -1
- package/ts_build/tests/unit/services/KnowhowClient.test.js +2 -2
- package/ts_build/tests/unit/services/KnowhowClient.test.js.map +1 -1
- package/ts_build/tests/unit/services/Mcp.test.js +2 -2
- package/ts_build/tests/unit/services/Mcp.test.js.map +1 -1
package/README.md
CHANGED
|
@@ -49,7 +49,23 @@ If you want to paste a token manually:
|
|
|
49
49
|
knowhow login --jwt
|
|
50
50
|
```
|
|
51
51
|
|
|
52
|
-
|
|
52
|
+
Authentication belongs to a named remote. `origin` is available by default and
|
|
53
|
+
points to the production API. To use another compatible backend:
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
knowhow remote add dev https://api.dev.knowhow.tyvm.ai
|
|
57
|
+
knowhow remote use dev
|
|
58
|
+
knowhow login
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Use `knowhow remote list` to show remotes and `knowhow remote get-url` to show
|
|
62
|
+
the selected API. Each entry in `.knowhow/knowhow.json` stores its name, API URL,
|
|
63
|
+
organization ID, and JWT path. Tokens remain independent: production uses
|
|
64
|
+
`.knowhow/.jwt`, development uses `.knowhow/.jwt.dev`, localhost uses
|
|
65
|
+
`.knowhow/.jwt.local`, and arbitrary hosts receive a host-specific filename.
|
|
66
|
+
|
|
67
|
+
`KNOWHOW_API_URL` remains an explicit per-process override, which is useful when
|
|
68
|
+
you only want to select a remote for one terminal command.
|
|
53
69
|
|
|
54
70
|
### First steps after setup
|
|
55
71
|
Start the interactive chat:
|
package/package.json
CHANGED
package/src/agents/base/base.ts
CHANGED
|
@@ -49,6 +49,13 @@ type CachedChatCompletion = { messages: ChatCompletion["messages"] } & Partial<
|
|
|
49
49
|
Omit<ChatCompletion, "messages">
|
|
50
50
|
>;
|
|
51
51
|
|
|
52
|
+
interface CompletedCompaction {
|
|
53
|
+
compressionEnd: number;
|
|
54
|
+
originalPrefix: Message[];
|
|
55
|
+
replacementPrefix: Message[];
|
|
56
|
+
summary: string;
|
|
57
|
+
}
|
|
58
|
+
|
|
52
59
|
/**
|
|
53
60
|
* A source is anything that can (a) start producing data, (b) hand each datum to
|
|
54
61
|
* a callback, and (c) be torn down. This is the unified contract behind
|
|
@@ -128,6 +135,12 @@ export abstract class BaseAgent implements IAgent {
|
|
|
128
135
|
// regardless of whether the token threshold has been reached.
|
|
129
136
|
protected _forceCompact = false;
|
|
130
137
|
|
|
138
|
+
// Compaction is intentionally detached from the main agent loop. A failed
|
|
139
|
+
// attempt is cleared so a later turn can retry without ending the task.
|
|
140
|
+
private _compactionPromise: Promise<void> | null = null;
|
|
141
|
+
private _completedCompaction: CompletedCompaction | null = null;
|
|
142
|
+
private _compactionGeneration = 0;
|
|
143
|
+
|
|
131
144
|
// Interrupt support: resolves the currently awaited tool call or completion
|
|
132
145
|
private _interruptResolve: (() => void) | null = null;
|
|
133
146
|
// A monotonically-increasing token identifying the active interruptible window.
|
|
@@ -287,6 +300,10 @@ export abstract class BaseAgent implements IAgent {
|
|
|
287
300
|
}
|
|
288
301
|
|
|
289
302
|
newTask(taskId?: string) {
|
|
303
|
+
// Invalidate any detached compaction still finishing for the previous task.
|
|
304
|
+
this._compactionGeneration++;
|
|
305
|
+
this._compactionPromise = null;
|
|
306
|
+
this._completedCompaction = null;
|
|
290
307
|
this.currentThread = 0;
|
|
291
308
|
this.threads = [];
|
|
292
309
|
this.taskBreakdown = "";
|
|
@@ -571,7 +588,11 @@ export abstract class BaseAgent implements IAgent {
|
|
|
571
588
|
return this.totalCostUsd;
|
|
572
589
|
}
|
|
573
590
|
|
|
574
|
-
adjustTokenUsage(
|
|
591
|
+
adjustTokenUsage(
|
|
592
|
+
usage: any,
|
|
593
|
+
messages?: Message[],
|
|
594
|
+
updateLastPromptTokens = true
|
|
595
|
+
) {
|
|
575
596
|
if (!usage) return;
|
|
576
597
|
// Support both OpenAI-style (prompt_tokens/completion_tokens) and Anthropic-style (input_tokens/output_tokens)
|
|
577
598
|
const inputTokens = usage.input_tokens ?? usage.prompt_tokens ?? 0;
|
|
@@ -593,7 +614,7 @@ export abstract class BaseAgent implements IAgent {
|
|
|
593
614
|
// (i.e. when there are input tokens) so a usage-less interrupt stub doesn't
|
|
594
615
|
// reset it to 0.
|
|
595
616
|
const promptTokens = inputTokens + cacheReadTokens + cacheWriteTokens;
|
|
596
|
-
if (promptTokens > 0) {
|
|
617
|
+
if (updateLastPromptTokens && promptTokens > 0) {
|
|
597
618
|
this.lastPromptTokens = promptTokens;
|
|
598
619
|
}
|
|
599
620
|
|
|
@@ -641,9 +662,10 @@ export abstract class BaseAgent implements IAgent {
|
|
|
641
662
|
params: CachedChatCompletion,
|
|
642
663
|
options: {
|
|
643
664
|
interruptValue?: CompletionResponse;
|
|
665
|
+
updateLastPromptTokens?: boolean;
|
|
644
666
|
} = {}
|
|
645
667
|
): Promise<CompletionResponse> {
|
|
646
|
-
const { interruptValue } = options;
|
|
668
|
+
const { interruptValue, updateLastPromptTokens = true } = options;
|
|
647
669
|
|
|
648
670
|
// If you change the tools, or any of this, you bust cache
|
|
649
671
|
// only safe to change messages
|
|
@@ -668,7 +690,11 @@ export abstract class BaseAgent implements IAgent {
|
|
|
668
690
|
: await callPromise;
|
|
669
691
|
|
|
670
692
|
this.adjustTotalCostUsd(response?.usd_cost);
|
|
671
|
-
this.adjustTokenUsage(
|
|
693
|
+
this.adjustTokenUsage(
|
|
694
|
+
response?.usage,
|
|
695
|
+
params.messages,
|
|
696
|
+
updateLastPromptTokens
|
|
697
|
+
);
|
|
672
698
|
|
|
673
699
|
return response;
|
|
674
700
|
}
|
|
@@ -1276,28 +1302,59 @@ export abstract class BaseAgent implements IAgent {
|
|
|
1276
1302
|
// prompt token count from the last completion response over the
|
|
1277
1303
|
// whitespace-based estimate — so it compares real tokens against the
|
|
1278
1304
|
// real (token-based) compress threshold.
|
|
1305
|
+
const completedCompaction = this._completedCompaction;
|
|
1306
|
+
this._completedCompaction = null;
|
|
1307
|
+
let appliedCompaction = false;
|
|
1308
|
+
|
|
1309
|
+
if (completedCompaction) {
|
|
1310
|
+
const currentPrefix = messages.slice(
|
|
1311
|
+
0,
|
|
1312
|
+
completedCompaction.compressionEnd
|
|
1313
|
+
);
|
|
1314
|
+
const prefixIsUnchanged = currentPrefix.every(
|
|
1315
|
+
(message, index) =>
|
|
1316
|
+
message === completedCompaction.originalPrefix[index] ||
|
|
1317
|
+
JSON.stringify(message) ===
|
|
1318
|
+
JSON.stringify(completedCompaction.originalPrefix[index])
|
|
1319
|
+
);
|
|
1320
|
+
|
|
1321
|
+
if (
|
|
1322
|
+
currentPrefix.length === completedCompaction.compressionEnd &&
|
|
1323
|
+
prefixIsUnchanged
|
|
1324
|
+
) {
|
|
1325
|
+
messages = [
|
|
1326
|
+
...completedCompaction.replacementPrefix,
|
|
1327
|
+
...messages.slice(completedCompaction.compressionEnd),
|
|
1328
|
+
];
|
|
1329
|
+
this.summaries.push(completedCompaction.summary);
|
|
1330
|
+
this.startNewThread(messages);
|
|
1331
|
+
this.lastPromptTokens = 0;
|
|
1332
|
+
appliedCompaction = true;
|
|
1333
|
+
} else {
|
|
1334
|
+
this.log(
|
|
1335
|
+
"Discarding completed compaction because its source prefix changed; a later turn will retry",
|
|
1336
|
+
"warn"
|
|
1337
|
+
);
|
|
1338
|
+
}
|
|
1339
|
+
}
|
|
1340
|
+
|
|
1279
1341
|
const contextTokens = this.getContextTokenCount(messages);
|
|
1280
1342
|
const overThreshold =
|
|
1281
1343
|
contextTokens > this.getCompressThreshold() &&
|
|
1282
1344
|
messages.length > this.compressMinMessages;
|
|
1283
1345
|
|
|
1284
|
-
if (
|
|
1285
|
-
|
|
1346
|
+
if (
|
|
1347
|
+
!appliedCompaction &&
|
|
1348
|
+
!this._compactionPromise &&
|
|
1349
|
+
(overThreshold || this._forceCompact)
|
|
1350
|
+
) {
|
|
1286
1351
|
this.log(
|
|
1287
1352
|
this._forceCompact
|
|
1288
|
-
? `
|
|
1289
|
-
: `
|
|
1353
|
+
? `Starting background compaction (manual /compact request): ${contextTokens} tokens`
|
|
1354
|
+
: `Starting background compaction: ${contextTokens} tokens exceeds ${this.getCompressThreshold()}`
|
|
1290
1355
|
);
|
|
1291
1356
|
this._forceCompact = false;
|
|
1292
|
-
|
|
1293
|
-
this.startNewThread(messages);
|
|
1294
|
-
// Reset the real-token tracker. compressMessages() ran a completion over
|
|
1295
|
-
// the older history, so lastPromptTokens reflects the compression input
|
|
1296
|
-
// rather than the newly reconstructed thread. Zeroing it makes
|
|
1297
|
-
// getContextTokenCount() estimate the new thread until the next regular
|
|
1298
|
-
// completion reports fresh usage, preventing an immediate false
|
|
1299
|
-
// re-compaction on the following iteration.
|
|
1300
|
-
this.lastPromptTokens = 0;
|
|
1357
|
+
this.startBackgroundCompaction(messages);
|
|
1301
1358
|
}
|
|
1302
1359
|
|
|
1303
1360
|
if (["assistant", "tool"].includes(messages[messages.length - 1].role)) {
|
|
@@ -1307,10 +1364,19 @@ export abstract class BaseAgent implements IAgent {
|
|
|
1307
1364
|
this.logStatus();
|
|
1308
1365
|
|
|
1309
1366
|
const continuation = `<Workflow>
|
|
1310
|
-
|
|
1367
|
+
This is an automated runtime instruction, not a new user message.
|
|
1368
|
+
The original user request remains active. Do not say that the user's
|
|
1369
|
+
message was empty, and do not ask the user to repeat the request.
|
|
1370
|
+
|
|
1371
|
+
Your previous response did not terminate the task because it did not
|
|
1372
|
+
call one of the required tools: ${JSON.stringify(
|
|
1311
1373
|
this.requiredToolNames
|
|
1312
|
-
)}
|
|
1313
|
-
|
|
1374
|
+
)}.
|
|
1375
|
+
If your previous response was intended as the final response, call the
|
|
1376
|
+
appropriate required tool now with that response. Otherwise, continue
|
|
1377
|
+
working on the original request and call an appropriate required tool
|
|
1378
|
+
when the task is complete.
|
|
1379
|
+
|
|
1314
1380
|
<TaskStatus>${statusMessage}</TaskStatus>
|
|
1315
1381
|
</Workflow>`;
|
|
1316
1382
|
|
|
@@ -1674,13 +1740,67 @@ export abstract class BaseAgent implements IAgent {
|
|
|
1674
1740
|
return [...content, ...finalAnswer].join("\n\n");
|
|
1675
1741
|
}
|
|
1676
1742
|
|
|
1677
|
-
|
|
1743
|
+
private startBackgroundCompaction(messages: Message[]): void {
|
|
1744
|
+
const snapshot = messages.slice();
|
|
1745
|
+
let resumeIndex = snapshot.length - 1;
|
|
1746
|
+
while (resumeIndex >= 0 && snapshot[resumeIndex].role !== "assistant") {
|
|
1747
|
+
resumeIndex--;
|
|
1748
|
+
}
|
|
1749
|
+
const compressionEnd =
|
|
1750
|
+
resumeIndex === -1 ? snapshot.length : resumeIndex;
|
|
1751
|
+
const generation = this._compactionGeneration;
|
|
1752
|
+
|
|
1753
|
+
const promise = (async () => {
|
|
1754
|
+
const taskBreakdown = await this.getTaskBreakdown(snapshot, false);
|
|
1755
|
+
if (generation !== this._compactionGeneration) return;
|
|
1756
|
+
this.taskBreakdown = taskBreakdown;
|
|
1757
|
+
let summary = "";
|
|
1758
|
+
const compactedSnapshot = await this.compressMessages(
|
|
1759
|
+
snapshot,
|
|
1760
|
+
(generatedSummary) => {
|
|
1761
|
+
summary = generatedSummary;
|
|
1762
|
+
},
|
|
1763
|
+
taskBreakdown
|
|
1764
|
+
);
|
|
1765
|
+
if (generation !== this._compactionGeneration) return;
|
|
1766
|
+
|
|
1767
|
+
const preservedSnapshotLength = snapshot.length - compressionEnd;
|
|
1768
|
+
this._completedCompaction = {
|
|
1769
|
+
compressionEnd,
|
|
1770
|
+
originalPrefix: snapshot.slice(0, compressionEnd),
|
|
1771
|
+
replacementPrefix: compactedSnapshot.slice(
|
|
1772
|
+
0,
|
|
1773
|
+
compactedSnapshot.length - preservedSnapshotLength
|
|
1774
|
+
),
|
|
1775
|
+
summary,
|
|
1776
|
+
};
|
|
1777
|
+
this.log("Background compaction completed; it will be applied next turn");
|
|
1778
|
+
})();
|
|
1779
|
+
|
|
1780
|
+
this._compactionPromise = promise;
|
|
1781
|
+
promise
|
|
1782
|
+
.catch((error) => {
|
|
1783
|
+
if (generation === this._compactionGeneration) {
|
|
1784
|
+
this.log(
|
|
1785
|
+
`Background compaction failed; a later turn will retry: ${error}`,
|
|
1786
|
+
"warn"
|
|
1787
|
+
);
|
|
1788
|
+
}
|
|
1789
|
+
})
|
|
1790
|
+
.finally(() => {
|
|
1791
|
+
if (this._compactionPromise === promise) {
|
|
1792
|
+
this._compactionPromise = null;
|
|
1793
|
+
}
|
|
1794
|
+
});
|
|
1795
|
+
}
|
|
1796
|
+
|
|
1797
|
+
async getTaskBreakdown(messages: Message[], cacheResult = true) {
|
|
1678
1798
|
if (this.taskBreakdown) {
|
|
1679
1799
|
return this.taskBreakdown;
|
|
1680
1800
|
}
|
|
1681
1801
|
|
|
1682
1802
|
const taskPrompt = `
|
|
1683
|
-
Analyze all previous messages.
|
|
1803
|
+
TASK INTERRUPT. CONTEXT COMPACTION NEEDED. Analyze all previous messages.
|
|
1684
1804
|
|
|
1685
1805
|
Generate a detailed task breakdown for this conversation, include a section for the following:
|
|
1686
1806
|
1. Task List
|
|
@@ -1699,10 +1819,13 @@ export abstract class BaseAgent implements IAgent {
|
|
|
1699
1819
|
},
|
|
1700
1820
|
] as Message[];
|
|
1701
1821
|
|
|
1702
|
-
const response = await this.createAgentCompletion(
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1822
|
+
const response = await this.createAgentCompletion(
|
|
1823
|
+
{
|
|
1824
|
+
messages: taskBreakdownMessages,
|
|
1825
|
+
tool_choice: "none",
|
|
1826
|
+
},
|
|
1827
|
+
{ updateLastPromptTokens: false }
|
|
1828
|
+
);
|
|
1706
1829
|
|
|
1707
1830
|
const breakdownContent = this.extractContentFromMessages(
|
|
1708
1831
|
response.choices?.map((c) => c.message)
|
|
@@ -1717,12 +1840,18 @@ export abstract class BaseAgent implements IAgent {
|
|
|
1717
1840
|
);
|
|
1718
1841
|
}
|
|
1719
1842
|
|
|
1720
|
-
|
|
1843
|
+
if (cacheResult) {
|
|
1844
|
+
this.taskBreakdown = breakdownContent;
|
|
1845
|
+
}
|
|
1721
1846
|
this.log(`task breakdown cost: ${response.usd_cost}`);
|
|
1722
|
-
return
|
|
1847
|
+
return breakdownContent;
|
|
1723
1848
|
}
|
|
1724
1849
|
|
|
1725
|
-
async compressMessages(
|
|
1850
|
+
async compressMessages(
|
|
1851
|
+
messages: Message[],
|
|
1852
|
+
onSummary?: (summary: string) => void,
|
|
1853
|
+
taskBreakdown = this.taskBreakdown
|
|
1854
|
+
) {
|
|
1726
1855
|
// Preserve the latest agent interaction exactly. Starting the resumed thread
|
|
1727
1856
|
// at the final assistant message keeps its tool calls, every tool response,
|
|
1728
1857
|
// and any subsequent user messages together as one protocol-valid unit.
|
|
@@ -1737,7 +1866,8 @@ export abstract class BaseAgent implements IAgent {
|
|
|
1737
1866
|
this.log(
|
|
1738
1867
|
`Compressing messages from 0 to ${compressionEnd}, resuming from ${resumeIndex}, total messages: ${messages.length}`
|
|
1739
1868
|
);
|
|
1740
|
-
const toCompressPrompt = `
|
|
1869
|
+
const toCompressPrompt = `TASK INTERRUPT. CONTEXT COMPACTION NEEDED.
|
|
1870
|
+
We are compressing our conversation to save memory.
|
|
1741
1871
|
Please summarize the conversation so far, so that we may continue the original task with a smaller context
|
|
1742
1872
|
|
|
1743
1873
|
Include the following sections:
|
|
@@ -1747,8 +1877,8 @@ export abstract class BaseAgent implements IAgent {
|
|
|
1747
1877
|
4. Tasks remaining - what tasks are left from the initial task breakdown.
|
|
1748
1878
|
|
|
1749
1879
|
${
|
|
1750
|
-
|
|
1751
|
-
? `Our initial task breakdown: ${
|
|
1880
|
+
taskBreakdown
|
|
1881
|
+
? `Our initial task breakdown: ${taskBreakdown}`
|
|
1752
1882
|
: ""
|
|
1753
1883
|
}
|
|
1754
1884
|
|
|
@@ -1764,10 +1894,13 @@ export abstract class BaseAgent implements IAgent {
|
|
|
1764
1894
|
},
|
|
1765
1895
|
] as Message[];
|
|
1766
1896
|
|
|
1767
|
-
const response = await this.createAgentCompletion(
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1897
|
+
const response = await this.createAgentCompletion(
|
|
1898
|
+
{
|
|
1899
|
+
messages: compressMessagesPayload,
|
|
1900
|
+
tool_choice: "none",
|
|
1901
|
+
},
|
|
1902
|
+
{ updateLastPromptTokens: false }
|
|
1903
|
+
);
|
|
1771
1904
|
|
|
1772
1905
|
const summary = this.extractContentFromMessages(
|
|
1773
1906
|
response.choices?.map((c) => c.message)
|
|
@@ -1778,15 +1911,19 @@ export abstract class BaseAgent implements IAgent {
|
|
|
1778
1911
|
throw new Error("Compaction summary request returned no text content");
|
|
1779
1912
|
}
|
|
1780
1913
|
|
|
1781
|
-
|
|
1914
|
+
if (onSummary) {
|
|
1915
|
+
onSummary(summary);
|
|
1916
|
+
} else {
|
|
1917
|
+
this.summaries.push(summary);
|
|
1918
|
+
}
|
|
1782
1919
|
|
|
1783
1920
|
const startMessages = [
|
|
1784
1921
|
{
|
|
1785
1922
|
role: "user",
|
|
1786
1923
|
content: `
|
|
1787
1924
|
${
|
|
1788
|
-
|
|
1789
|
-
? `Initial task breakdown:\n ${
|
|
1925
|
+
taskBreakdown
|
|
1926
|
+
? `Initial task breakdown:\n ${taskBreakdown}`
|
|
1790
1927
|
: "(No task breakdown available — summarize what you know from context above)"
|
|
1791
1928
|
}
|
|
1792
1929
|
|
package/src/auth/browserLogin.ts
CHANGED
|
@@ -8,6 +8,7 @@ import * as path from "path";
|
|
|
8
8
|
import { KNOWHOW_API_URL } from "../services/KnowhowClient";
|
|
9
9
|
import { Spinner } from "./spinner";
|
|
10
10
|
import { BrowserLoginError } from "./errors";
|
|
11
|
+
import { storeJwtForApi } from "./jwtStore";
|
|
11
12
|
|
|
12
13
|
// TypeScript interfaces for the CLI Login API
|
|
13
14
|
|
|
@@ -199,19 +200,7 @@ export class BrowserLoginService {
|
|
|
199
200
|
* Securely stores the JWT token to the file system
|
|
200
201
|
*/
|
|
201
202
|
private async storeJwt(jwt: string): Promise<void> {
|
|
202
|
-
|
|
203
|
-
const jwtFile = `${configDir}/.jwt`;
|
|
204
|
-
|
|
205
|
-
// Ensure directory exists
|
|
206
|
-
if (!fs.existsSync(configDir)) {
|
|
207
|
-
fs.mkdirSync(configDir, { recursive: true });
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
// Write JWT to file
|
|
211
|
-
fs.writeFileSync(jwtFile, jwt, { mode: 0o600 });
|
|
212
|
-
|
|
213
|
-
// Ensure file has correct permissions (readable only by owner)
|
|
214
|
-
fs.chmodSync(jwtFile, 0o600);
|
|
203
|
+
storeJwtForApi(jwt, this.baseUrl);
|
|
215
204
|
}
|
|
216
205
|
|
|
217
206
|
/**
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { Config } from "../types";
|
|
2
|
+
import { loadJwtFromDisk } from "./jwtStore";
|
|
3
|
+
import { createRemote, findRemoteForApi, registerRemote } from "../remotes";
|
|
4
|
+
|
|
5
|
+
/** Stable config key for authentication state owned by one API deployment. */
|
|
6
|
+
export function getApiEnvironmentKey(apiUrl: string): string {
|
|
7
|
+
try {
|
|
8
|
+
const url = new URL(apiUrl);
|
|
9
|
+
return `${url.protocol}//${url.host}`.toLowerCase();
|
|
10
|
+
} catch {
|
|
11
|
+
return apiUrl.replace(/\/+$/, "").toLowerCase();
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** Read an organization claim without trusting the JWT for authorization. */
|
|
16
|
+
export function getOrgIdFromJwt(jwt: string): string | undefined {
|
|
17
|
+
try {
|
|
18
|
+
const payload = JSON.parse(
|
|
19
|
+
Buffer.from(jwt.split(".")[1] ?? "", "base64url").toString("utf8")
|
|
20
|
+
) as Record<string, unknown>;
|
|
21
|
+
const orgId = payload.org ?? payload.orgId ?? payload.org_id ?? payload.organizationId;
|
|
22
|
+
return typeof orgId === "string" && orgId ? orgId : undefined;
|
|
23
|
+
} catch {
|
|
24
|
+
return undefined;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Resolve the org for the selected API without leaking another environment's org. */
|
|
29
|
+
export function getOrgIdForApi(config: Config, apiUrl: string): string | undefined {
|
|
30
|
+
const remote = findRemoteForApi(config, apiUrl);
|
|
31
|
+
if (remote?.orgId) return remote.orgId;
|
|
32
|
+
|
|
33
|
+
// Read old orgIds only until this API has been represented as a remote.
|
|
34
|
+
const environmentKey = getApiEnvironmentKey(apiUrl);
|
|
35
|
+
const configured = config.orgIds?.[environmentKey];
|
|
36
|
+
if (configured) return configured;
|
|
37
|
+
|
|
38
|
+
const tokenOrgId = getOrgIdFromJwt(loadJwtFromDisk(apiUrl));
|
|
39
|
+
if (tokenOrgId) return tokenOrgId;
|
|
40
|
+
|
|
41
|
+
return config.remotes || (config.orgIds && Object.keys(config.orgIds).length > 0)
|
|
42
|
+
? undefined
|
|
43
|
+
: config.orgId;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Record authentication state on the remote that owns it. */
|
|
47
|
+
export function setOrgIdForApi(config: Config, apiUrl: string, orgId: string): void {
|
|
48
|
+
const remote = findRemoteForApi(config, apiUrl) ?? createRemote("origin", apiUrl);
|
|
49
|
+
registerRemote(config, { ...remote, orgId });
|
|
50
|
+
delete config.orgId;
|
|
51
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { findRemoteForApiSync, resolveJwtPath } from "../remotes";
|
|
4
|
+
|
|
5
|
+
const PRODUCTION_API_HOST = "api.knowhow.tyvm.ai";
|
|
6
|
+
|
|
7
|
+
/** Return a readable, environment-specific JWT filename for an API URL. */
|
|
8
|
+
export function getJwtFileName(apiUrl: string): string {
|
|
9
|
+
try {
|
|
10
|
+
const { hostname, port } = new URL(apiUrl);
|
|
11
|
+
const normalizedHost = hostname.toLowerCase();
|
|
12
|
+
|
|
13
|
+
if (normalizedHost === PRODUCTION_API_HOST) return ".jwt";
|
|
14
|
+
if (
|
|
15
|
+
normalizedHost === "localhost" ||
|
|
16
|
+
normalizedHost === "::1" ||
|
|
17
|
+
normalizedHost.startsWith("127.")
|
|
18
|
+
) {
|
|
19
|
+
return ".jwt.local";
|
|
20
|
+
}
|
|
21
|
+
if (normalizedHost === "api.dev.knowhow.tyvm.ai" || normalizedHost.includes(".dev.")) {
|
|
22
|
+
return ".jwt.dev";
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const environment = `${normalizedHost}${port ? `-${port}` : ""}`
|
|
26
|
+
.replace(/[^a-z0-9.-]+/g, "-")
|
|
27
|
+
.replace(/^-+|-+$/g, "");
|
|
28
|
+
return environment ? `.jwt.${environment}` : ".jwt";
|
|
29
|
+
} catch {
|
|
30
|
+
return ".jwt";
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function getJwtFilePath(
|
|
35
|
+
apiUrl: string,
|
|
36
|
+
configDir = path.join(process.cwd(), ".knowhow")
|
|
37
|
+
): string {
|
|
38
|
+
return path.join(configDir, getJwtFileName(apiUrl));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Load the JWT selected for an environment. The optional legacy path provides
|
|
43
|
+
* the directory for configs that historically pointed at `.jwt`.
|
|
44
|
+
*/
|
|
45
|
+
export function loadJwtFromDisk(apiUrl: string, legacyPath?: string): string {
|
|
46
|
+
const basePath = legacyPath ?? path.join(process.cwd(), ".knowhow", ".jwt");
|
|
47
|
+
const configuredPath = findRemoteForApiSync(apiUrl)?.jwtPath;
|
|
48
|
+
const jwtPath = configuredPath
|
|
49
|
+
? resolveJwtPath(configuredPath)
|
|
50
|
+
: path.join(path.dirname(basePath), getJwtFileName(apiUrl));
|
|
51
|
+
return fs.existsSync(jwtPath)
|
|
52
|
+
? fs.readFileSync(jwtPath, "utf8").trim()
|
|
53
|
+
: "";
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Atomically store a JWT in the file selected for the API environment. */
|
|
57
|
+
export function storeJwtForApi(jwt: string, apiUrl: string, configuredPath?: string): string {
|
|
58
|
+
const jwtFilePath = configuredPath ? resolveJwtPath(configuredPath) : getJwtFilePath(apiUrl);
|
|
59
|
+
const directory = path.dirname(jwtFilePath);
|
|
60
|
+
const temporaryPath = `${jwtFilePath}.${process.pid}.tmp`;
|
|
61
|
+
fs.mkdirSync(directory, { recursive: true, mode: 0o700 });
|
|
62
|
+
try {
|
|
63
|
+
fs.writeFileSync(temporaryPath, jwt, { mode: 0o600, flag: "wx" });
|
|
64
|
+
fs.renameSync(temporaryPath, jwtFilePath);
|
|
65
|
+
fs.chmodSync(jwtFilePath, 0o600);
|
|
66
|
+
} finally {
|
|
67
|
+
fs.rmSync(temporaryPath, { force: true });
|
|
68
|
+
}
|
|
69
|
+
return jwtFilePath;
|
|
70
|
+
}
|
package/src/auth/keyAuth.ts
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
import fs from "fs";
|
|
2
1
|
import os from "os";
|
|
3
|
-
import path from "path";
|
|
4
2
|
import http from "../utils/http";
|
|
5
3
|
import { getCliUserAgent } from "./browserLogin";
|
|
4
|
+
import { storeJwtForApi } from "./jwtStore";
|
|
6
5
|
import {
|
|
7
6
|
getDefaultPrivateKeyPath,
|
|
8
7
|
keyPairExists,
|
|
@@ -101,7 +100,7 @@ export async function authenticateWithKey(
|
|
|
101
100
|
if (!keyPairExists(privateKeyPath) && !keyPairExists()) return false;
|
|
102
101
|
|
|
103
102
|
try {
|
|
104
|
-
storeJwt(await exchangeAvailablePublicKeyJwt(orgId, apiUrl, privateKeyPath));
|
|
103
|
+
storeJwt(await exchangeAvailablePublicKeyJwt(orgId, apiUrl, privateKeyPath), apiUrl);
|
|
105
104
|
return true;
|
|
106
105
|
} catch (error: unknown) {
|
|
107
106
|
// The identity is not registered for this environment/org (or was revoked).
|
|
@@ -138,16 +137,6 @@ export async function registerPublicKey(
|
|
|
138
137
|
);
|
|
139
138
|
}
|
|
140
139
|
|
|
141
|
-
export function storeJwt(jwt: string): void {
|
|
142
|
-
|
|
143
|
-
const directory = path.dirname(jwtFilePath);
|
|
144
|
-
const temporaryPath = `${jwtFilePath}.${process.pid}.tmp`;
|
|
145
|
-
fs.mkdirSync(directory, { recursive: true, mode: 0o700 });
|
|
146
|
-
try {
|
|
147
|
-
fs.writeFileSync(temporaryPath, jwt, { mode: 0o600, flag: "wx" });
|
|
148
|
-
fs.renameSync(temporaryPath, jwtFilePath);
|
|
149
|
-
fs.chmodSync(jwtFilePath, 0o600);
|
|
150
|
-
} finally {
|
|
151
|
-
fs.rmSync(temporaryPath, { force: true });
|
|
152
|
-
}
|
|
140
|
+
export function storeJwt(jwt: string, apiUrl: string = DEFAULT_API_URL): void {
|
|
141
|
+
storeJwtForApi(jwt, apiUrl);
|
|
153
142
|
}
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import fs from "fs";
|
|
2
1
|
import path from "path";
|
|
3
2
|
import { getOrCreatePublicKey } from "./keyManager";
|
|
4
3
|
import { registerPublicKey, exchangePublicKeyJwt, storeJwt } from "./keyAuth";
|
|
5
4
|
import { getConfig, updateConfig } from "../config";
|
|
5
|
+
import { getJwtFilePath, loadJwtFromDisk } from "./jwtStore";
|
|
6
|
+
import { setOrgIdForApi } from "./environmentAuth";
|
|
6
7
|
|
|
7
8
|
export interface RegisterIdentityOptions {
|
|
8
9
|
/** Optional Ed25519 private-key path. Defaults to ~/.knowhow/keys/id_ed25519. */
|
|
@@ -36,30 +37,29 @@ export function extractOrgIdFromJwt(jwt: string): string {
|
|
|
36
37
|
}
|
|
37
38
|
|
|
38
39
|
/**
|
|
39
|
-
* Register a CLI identity using the JWT
|
|
40
|
-
*
|
|
40
|
+
* Register a CLI identity using the JWT stored for the selected API, then
|
|
41
|
+
* replace that JWT with one obtained via key authentication.
|
|
41
42
|
* This is intentionally generic: authorization scope is derived by the backend
|
|
42
43
|
* from the verified source JWT.
|
|
43
44
|
*/
|
|
44
45
|
export async function registerIdentity(options: RegisterIdentityOptions = {}): Promise<void> {
|
|
45
46
|
const apiUrl = options.apiUrl ?? process.env.KNOWHOW_API_URL ?? "https://api.knowhow.tyvm.ai";
|
|
46
|
-
const jwtPath =
|
|
47
|
-
|
|
48
|
-
const sourceJwt = fs.readFileSync(jwtPath, "utf8").trim();
|
|
47
|
+
const jwtPath = getJwtFilePath(apiUrl);
|
|
48
|
+
const sourceJwt = loadJwtFromDisk(apiUrl);
|
|
49
49
|
if (!sourceJwt) throw new Error(`JWT file is empty: ${jwtPath}`);
|
|
50
50
|
|
|
51
51
|
const orgId = extractOrgIdFromJwt(sourceJwt);
|
|
52
52
|
const keyPair = getOrCreatePublicKey(options.identityPath);
|
|
53
53
|
await registerPublicKey(sourceJwt, keyPair.publicKeyBase64, apiUrl);
|
|
54
54
|
const identityJwt = await exchangePublicKeyJwt(orgId, apiUrl, options.identityPath);
|
|
55
|
-
storeJwt(identityJwt);
|
|
55
|
+
storeJwt(identityJwt, apiUrl);
|
|
56
56
|
|
|
57
57
|
const config = await getConfig();
|
|
58
58
|
if (!config.modelProviders) config.modelProviders = [];
|
|
59
59
|
if (!config.modelProviders.some((provider: { provider: string }) => provider.provider === "knowhow")) {
|
|
60
60
|
config.modelProviders.push({ provider: "knowhow" });
|
|
61
61
|
}
|
|
62
|
-
config
|
|
62
|
+
setOrgIdForApi(config, apiUrl, orgId);
|
|
63
63
|
config.cliIdentityPath = path.resolve(options.identityPath ?? keyPair.privateKeyPath);
|
|
64
64
|
await updateConfig(config);
|
|
65
65
|
|
|
@@ -810,7 +810,7 @@ export class AgentModule extends BaseChatModule {
|
|
|
810
810
|
// Match new interactive threads: local-only sessions get a fresh remote
|
|
811
811
|
// task when auto-sync is enabled.
|
|
812
812
|
if (!remoteTaskId && this.remoteSyncModule?.isAutoSyncEnabled()) {
|
|
813
|
-
|
|
813
|
+
this.remoteSyncModule.syncTaskInBackground(taskId);
|
|
814
814
|
}
|
|
815
815
|
|
|
816
816
|
// After resume finishes, revert to normal chat (non-agent mode) so the
|
|
@@ -1471,7 +1471,7 @@ export class AgentModule extends BaseChatModule {
|
|
|
1471
1471
|
!remoteTaskId &&
|
|
1472
1472
|
this.remoteSyncModule?.isAutoSyncEnabled()
|
|
1473
1473
|
) {
|
|
1474
|
-
|
|
1474
|
+
this.remoteSyncModule.syncTaskInBackground(result.taskId);
|
|
1475
1475
|
}
|
|
1476
1476
|
|
|
1477
1477
|
await this.attachedAgentChatLoop(
|
|
@@ -1566,7 +1566,7 @@ export class AgentModule extends BaseChatModule {
|
|
|
1566
1566
|
|
|
1567
1567
|
// If auto-sync is enabled, push this task to the remote KnowHow app
|
|
1568
1568
|
if (this.remoteSyncModule?.isAutoSyncEnabled()) {
|
|
1569
|
-
|
|
1569
|
+
this.remoteSyncModule.syncTaskInBackground(taskId);
|
|
1570
1570
|
}
|
|
1571
1571
|
|
|
1572
1572
|
await this.attachedAgentChatLoop(taskId, agent, formattedPrompt);
|