karajan-code 1.59.0 → 2.0.0
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 +2 -0
- package/package.json +1 -1
- package/scripts/install.js +18 -15
- package/src/agents/aider-agent.js +0 -7
- package/src/agents/claude-agent.js +34 -6
- package/src/agents/codex-agent.js +2 -6
- package/src/agents/gemini-agent.js +0 -4
- package/src/agents/opencode-agent.js +0 -4
- package/src/{becaria → ci}/dispatch.js +13 -13
- package/src/{becaria → ci}/pr-diff.js +1 -1
- package/src/cli.js +3 -2
- package/src/commands/agents.js +0 -1
- package/src/commands/doctor.js +22 -40
- package/src/commands/init.js +8 -8
- package/src/commands/report.js +0 -13
- package/src/commands/review.js +17 -17
- package/src/commands/run.js +1 -2
- package/src/commands/status.js +1 -7
- package/src/config.js +10 -52
- package/src/domains/domain-loader.js +85 -4
- package/src/git/automation.js +9 -9
- package/src/guards/output-guard.js +0 -17
- package/src/mcp/handlers/direct-handlers.js +1 -1
- package/src/mcp/handlers/run-handler.js +1 -2
- package/src/mcp/sovereignty-guard.js +2 -2
- package/src/mcp/tools.js +3 -2
- package/src/orchestrator/brain-coordinator.js +165 -0
- package/src/orchestrator/ci-integration.js +139 -0
- package/src/orchestrator/config-init.js +435 -0
- package/src/orchestrator/direct-actions.js +186 -0
- package/src/orchestrator/feedback-enrichment.js +140 -0
- package/src/orchestrator/feedback-queue.js +152 -0
- package/src/orchestrator/flow-control.js +159 -0
- package/src/orchestrator/post-loop-stages.js +20 -15
- package/src/orchestrator/preflight-checks.js +7 -3
- package/src/orchestrator/role-output-compressor.js +190 -0
- package/src/orchestrator/session-journal.js +344 -0
- package/src/orchestrator/solomon-escalation.js +38 -5
- package/src/orchestrator/solomon-rules.js +32 -33
- package/src/orchestrator/stages/reviewer-stage.js +16 -5
- package/src/orchestrator/stages/sonar-stage.js +24 -1
- package/src/orchestrator/verification-gate.js +146 -0
- package/src/orchestrator.js +435 -661
- package/src/planning-game/client.js +2 -2
- package/src/planning-game/pipeline-adapter.js +1 -1
- package/src/prompts/coder.js +17 -3
- package/src/prompts/reviewer.js +2 -2
- package/src/prompts/rtk-snippet.js +3 -5
- package/src/review/diff-generator.js +66 -17
- package/src/review/snapshot-diff.js +122 -0
- package/src/review/tdd-policy.js +4 -2
- package/src/roles/agent-role.js +135 -0
- package/src/roles/architect-role.js +61 -108
- package/src/roles/audit-role.js +25 -71
- package/src/roles/coder-role.js +32 -57
- package/src/roles/discover-role.js +30 -71
- package/src/roles/hu-reviewer-role.js +51 -99
- package/src/roles/impeccable-role.js +34 -91
- package/src/roles/index.js +2 -0
- package/src/roles/karajan-brain-role.js +134 -0
- package/src/roles/planner-role.js +40 -75
- package/src/roles/refactorer-role.js +22 -64
- package/src/roles/researcher-role.js +52 -118
- package/src/roles/reviewer-role.js +69 -126
- package/src/roles/security-role.js +38 -102
- package/src/roles/solomon-role.js +71 -15
- package/src/roles/tester-role.js +121 -95
- package/src/roles/triage-role.js +50 -111
- package/src/session-store.js +1 -1
- package/src/skills/skill-detector.js +11 -0
- package/src/sonar/config-resolver.js +20 -1
- package/src/sonar/credentials.js +17 -0
- package/src/sonar/project-key.js +1 -1
- package/src/sonar/scanner.js +1 -1
- package/src/utils/banner.js +66 -0
- package/src/utils/display.js +35 -17
- package/src/utils/injection-guard.js +23 -8
- package/src/utils/logger.js +6 -1
- package/src/utils/process.js +3 -4
- package/src/utils/role-assigner.js +131 -0
- package/src/utils/update-check.js +5 -1
- package/src/utils/welcome.js +3 -8
- package/src/webperf/devtools-detect.js +70 -23
- package/templates/roles/coder.md +7 -0
- package/templates/roles/karajan-brain.md +95 -0
- package/templates/roles/solomon.md +67 -106
- package/templates/workflows/automerge.yml +4 -4
- package/templates/workflows/houston-override.yml +4 -4
- package/templates/workflows/{becaria-gateway.yml → kj-ci-gateway.yml} +12 -12
- package/src/proxy/adapters/anthropic.js +0 -131
- package/src/proxy/adapters/gemini.js +0 -106
- package/src/proxy/adapters/index.js +0 -51
- package/src/proxy/adapters/openai.js +0 -71
- package/src/proxy/compression/ai-compressor.js +0 -209
- package/src/proxy/compression/dedup-cache.js +0 -100
- package/src/proxy/compression/deterministic/bash-build.js +0 -60
- package/src/proxy/compression/deterministic/bash-git.js +0 -101
- package/src/proxy/compression/deterministic/bash-infra.js +0 -64
- package/src/proxy/compression/deterministic/bash-misc.js +0 -53
- package/src/proxy/compression/deterministic/bash-pkg.js +0 -49
- package/src/proxy/compression/deterministic/bash-test.js +0 -61
- package/src/proxy/compression/deterministic/glob.js +0 -26
- package/src/proxy/compression/deterministic/grep.js +0 -53
- package/src/proxy/compression/deterministic/index.js +0 -58
- package/src/proxy/compression/deterministic/read.js +0 -22
- package/src/proxy/compression/deterministic/utils.js +0 -33
- package/src/proxy/compression/pipeline.js +0 -270
- package/src/proxy/middleware/prompt-injector.js +0 -165
- package/src/proxy/middleware/response-interceptor.js +0 -223
- package/src/proxy/progress-monitor.js +0 -98
- package/src/proxy/proxy-lifecycle.js +0 -198
- package/src/proxy/proxy-server.js +0 -239
- package/src/proxy/security.js +0 -45
- /package/src/{becaria → ci}/index.js +0 -0
- /package/src/{becaria → ci}/repo.js +0 -0
|
@@ -1,17 +1,17 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Karajan CI Gateway — publishes PR comments and reviews from repository_dispatch events.
|
|
2
2
|
# Triggered by Karajan Code pipeline via `gh api repos/{owner}/{repo}/dispatches`.
|
|
3
3
|
#
|
|
4
4
|
# Required secrets:
|
|
5
|
-
#
|
|
6
|
-
#
|
|
5
|
+
# KJ_CI_APP_ID — GitHub App ID for kj-reviewer
|
|
6
|
+
# KJ_CI_PRIVATE_KEY — PEM private key for the GitHub App
|
|
7
7
|
|
|
8
|
-
name:
|
|
8
|
+
name: Karajan CI Gateway
|
|
9
9
|
|
|
10
10
|
on:
|
|
11
11
|
repository_dispatch:
|
|
12
12
|
types:
|
|
13
|
-
-
|
|
14
|
-
-
|
|
13
|
+
- kj-comment
|
|
14
|
+
- kj-review
|
|
15
15
|
|
|
16
16
|
permissions:
|
|
17
17
|
contents: read
|
|
@@ -25,11 +25,11 @@ jobs:
|
|
|
25
25
|
id: app-token
|
|
26
26
|
uses: actions/create-github-app-token@v1
|
|
27
27
|
with:
|
|
28
|
-
app-id: ${{ secrets.
|
|
29
|
-
private-key: ${{ secrets.
|
|
28
|
+
app-id: ${{ secrets.KJ_CI_APP_ID }}
|
|
29
|
+
private-key: ${{ secrets.KJ_CI_PRIVATE_KEY }}
|
|
30
30
|
|
|
31
31
|
- name: Post Comment
|
|
32
|
-
if: github.event.action == '
|
|
32
|
+
if: github.event.action == 'kj-comment'
|
|
33
33
|
uses: actions/github-script@v7
|
|
34
34
|
with:
|
|
35
35
|
github-token: ${{ steps.app-token.outputs.token }}
|
|
@@ -39,11 +39,11 @@ jobs:
|
|
|
39
39
|
owner: context.repo.owner,
|
|
40
40
|
repo: context.repo.repo,
|
|
41
41
|
issue_number: pr_number,
|
|
42
|
-
body: `**${agent}** (via
|
|
42
|
+
body: `**${agent}** (via Karajan CI)\n\n${body}`
|
|
43
43
|
});
|
|
44
44
|
|
|
45
45
|
- name: Submit Review
|
|
46
|
-
if: github.event.action == '
|
|
46
|
+
if: github.event.action == 'kj-review'
|
|
47
47
|
uses: actions/github-script@v7
|
|
48
48
|
with:
|
|
49
49
|
github-token: ${{ steps.app-token.outputs.token }}
|
|
@@ -54,5 +54,5 @@ jobs:
|
|
|
54
54
|
repo: context.repo.repo,
|
|
55
55
|
pull_number: pr_number,
|
|
56
56
|
event: event,
|
|
57
|
-
body: `**${agent}** (via
|
|
57
|
+
body: `**${agent}** (via Karajan CI)\n\n${body}`
|
|
58
58
|
});
|
|
@@ -1,131 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Anthropic message format adapter.
|
|
3
|
-
*
|
|
4
|
-
* Anthropic messages API uses an array of messages where each message has a
|
|
5
|
-
* `content` array. Tool results appear as content blocks with
|
|
6
|
-
* `type: "tool_result"`. Each tool_result block contains an `id` (matching
|
|
7
|
-
* the prior tool_use), optional `tool_use_id`, and nested `content` that
|
|
8
|
-
* can be a string or an array of content blocks (text, image, etc.).
|
|
9
|
-
*
|
|
10
|
-
* @module
|
|
11
|
-
*/
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* Extract tool results from Anthropic-format messages.
|
|
15
|
-
*
|
|
16
|
-
* @param {Array<object>} messages - Anthropic messages array
|
|
17
|
-
* @returns {Array<{id: string, toolName: string, text: string, turnIndex: number}>}
|
|
18
|
-
*/
|
|
19
|
-
export function extractToolResults(messages) {
|
|
20
|
-
const results = [];
|
|
21
|
-
|
|
22
|
-
for (let i = 0; i < messages.length; i++) {
|
|
23
|
-
const msg = messages[i];
|
|
24
|
-
if (!msg || !Array.isArray(msg.content)) continue;
|
|
25
|
-
|
|
26
|
-
// Collect tool_use names from assistant messages so we can map ids to names
|
|
27
|
-
const toolNameMap = buildToolNameMap(messages, i);
|
|
28
|
-
|
|
29
|
-
for (const block of msg.content) {
|
|
30
|
-
if (block.type !== "tool_result") continue;
|
|
31
|
-
|
|
32
|
-
const id = block.tool_use_id || block.id || "";
|
|
33
|
-
const text = extractText(block);
|
|
34
|
-
const toolName = toolNameMap.get(id) || "unknown";
|
|
35
|
-
|
|
36
|
-
results.push({ id, toolName, text, turnIndex: i });
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
return results;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* Rebuild messages replacing tool_result text with compressed versions.
|
|
45
|
-
*
|
|
46
|
-
* @param {Array<object>} messages - Original Anthropic messages array
|
|
47
|
-
* @param {Map<string, string>|Record<string, string>} compressedMap - id → compressed text
|
|
48
|
-
* @returns {Array<object>} Deep-cloned messages with replacements applied
|
|
49
|
-
*/
|
|
50
|
-
export function rebuildMessages(messages, compressedMap) {
|
|
51
|
-
const map = compressedMap instanceof Map ? compressedMap : new Map(Object.entries(compressedMap));
|
|
52
|
-
return messages.map((msg) => {
|
|
53
|
-
if (!msg || !Array.isArray(msg.content)) return msg;
|
|
54
|
-
|
|
55
|
-
const newContent = msg.content.map((block) => {
|
|
56
|
-
if (block.type !== "tool_result") return block;
|
|
57
|
-
|
|
58
|
-
const id = block.tool_use_id || block.id || "";
|
|
59
|
-
if (!map.has(id)) return block;
|
|
60
|
-
|
|
61
|
-
const compressed = map.get(id);
|
|
62
|
-
return replaceText(block, compressed);
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
return { ...msg, content: newContent };
|
|
66
|
-
});
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
// ── helpers ──────────────────────────────────────────────────────────
|
|
70
|
-
|
|
71
|
-
/**
|
|
72
|
-
* Build a map of tool_use id → tool name by scanning assistant messages
|
|
73
|
-
* that precede or are at the given turn index.
|
|
74
|
-
*/
|
|
75
|
-
function buildToolNameMap(messages, upToIndex) {
|
|
76
|
-
const map = new Map();
|
|
77
|
-
for (let i = 0; i <= upToIndex; i++) {
|
|
78
|
-
const msg = messages[i];
|
|
79
|
-
if (!msg || msg.role !== "assistant" || !Array.isArray(msg.content)) continue;
|
|
80
|
-
for (const block of msg.content) {
|
|
81
|
-
if (block.type === "tool_use" && block.id) {
|
|
82
|
-
map.set(block.id, block.name || "unknown");
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
return map;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
/**
|
|
90
|
-
* Extract plain text from a tool_result block.
|
|
91
|
-
* Content can be a string, an array of {type:"text", text} blocks, or absent.
|
|
92
|
-
*/
|
|
93
|
-
function extractText(block) {
|
|
94
|
-
if (typeof block.content === "string") return block.content;
|
|
95
|
-
if (Array.isArray(block.content)) {
|
|
96
|
-
return block.content
|
|
97
|
-
.filter((c) => c.type === "text")
|
|
98
|
-
.map((c) => c.text)
|
|
99
|
-
.join("\n");
|
|
100
|
-
}
|
|
101
|
-
return "";
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
/**
|
|
105
|
-
* Return a new tool_result block with text replaced.
|
|
106
|
-
* Preserves the content shape (string vs array).
|
|
107
|
-
*/
|
|
108
|
-
function replaceText(block, newText) {
|
|
109
|
-
if (typeof block.content === "string") {
|
|
110
|
-
return { ...block, content: newText };
|
|
111
|
-
}
|
|
112
|
-
if (Array.isArray(block.content)) {
|
|
113
|
-
// Replace only text blocks, keep images etc.
|
|
114
|
-
const newContent = block.content.map((c) =>
|
|
115
|
-
c.type === "text" ? { ...c, text: newText } : c,
|
|
116
|
-
);
|
|
117
|
-
// If there were text blocks, replace the first and remove the rest
|
|
118
|
-
let seenText = false;
|
|
119
|
-
const filtered = newContent.filter((c) => {
|
|
120
|
-
if (c.type !== "text") return true;
|
|
121
|
-
if (!seenText) {
|
|
122
|
-
seenText = true;
|
|
123
|
-
return true;
|
|
124
|
-
}
|
|
125
|
-
return false;
|
|
126
|
-
});
|
|
127
|
-
return { ...block, content: filtered };
|
|
128
|
-
}
|
|
129
|
-
// No content field — add it as string
|
|
130
|
-
return { ...block, content: newText };
|
|
131
|
-
}
|
|
@@ -1,106 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Gemini message format adapter.
|
|
3
|
-
*
|
|
4
|
-
* Gemini API uses `contents[]` where each content has `parts[]`.
|
|
5
|
-
* Function responses appear as parts with a `functionResponse` field:
|
|
6
|
-
* `{ functionResponse: { name: "...", response: { content: "..." } } }`
|
|
7
|
-
*
|
|
8
|
-
* Gemini does not use explicit IDs for function calls/responses. Instead,
|
|
9
|
-
* the function name serves as the correlation key. When multiple calls to
|
|
10
|
-
* the same function exist, we generate synthetic IDs combining turnIndex
|
|
11
|
-
* and part index for disambiguation.
|
|
12
|
-
*
|
|
13
|
-
* @module
|
|
14
|
-
*/
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* Extract tool results from Gemini-format messages (contents array).
|
|
18
|
-
*
|
|
19
|
-
* @param {Array<object>} messages - Gemini contents array
|
|
20
|
-
* @returns {Array<{id: string, toolName: string, text: string, turnIndex: number}>}
|
|
21
|
-
*/
|
|
22
|
-
export function extractToolResults(messages) {
|
|
23
|
-
const results = [];
|
|
24
|
-
|
|
25
|
-
for (let i = 0; i < messages.length; i++) {
|
|
26
|
-
const content = messages[i];
|
|
27
|
-
if (!content || !Array.isArray(content.parts)) continue;
|
|
28
|
-
|
|
29
|
-
for (let p = 0; p < content.parts.length; p++) {
|
|
30
|
-
const part = content.parts[p];
|
|
31
|
-
if (!part.functionResponse) continue;
|
|
32
|
-
|
|
33
|
-
const fr = part.functionResponse;
|
|
34
|
-
const toolName = fr.name || "unknown";
|
|
35
|
-
const text = extractResponseText(fr.response);
|
|
36
|
-
// Synthetic ID: turn index + part index (Gemini has no explicit IDs)
|
|
37
|
-
const id = `gemini-${i}-${p}`;
|
|
38
|
-
|
|
39
|
-
results.push({ id, toolName, text, turnIndex: i });
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
return results;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
/**
|
|
47
|
-
* Rebuild messages replacing functionResponse content with compressed versions.
|
|
48
|
-
*
|
|
49
|
-
* @param {Array<object>} messages - Original Gemini contents array
|
|
50
|
-
* @param {Map<string, string>|Record<string, string>} compressedMap - id → compressed text
|
|
51
|
-
* @returns {Array<object>} Deep-cloned messages with replacements applied
|
|
52
|
-
*/
|
|
53
|
-
export function rebuildMessages(messages, compressedMap) {
|
|
54
|
-
const map = compressedMap instanceof Map ? compressedMap : new Map(Object.entries(compressedMap));
|
|
55
|
-
|
|
56
|
-
return messages.map((content, i) => {
|
|
57
|
-
if (!content || !Array.isArray(content.parts)) return content;
|
|
58
|
-
|
|
59
|
-
const newParts = content.parts.map((part, p) => {
|
|
60
|
-
if (!part.functionResponse) return part;
|
|
61
|
-
|
|
62
|
-
const id = `gemini-${i}-${p}`;
|
|
63
|
-
if (!map.has(id)) return part;
|
|
64
|
-
|
|
65
|
-
const compressed = map.get(id);
|
|
66
|
-
return {
|
|
67
|
-
...part,
|
|
68
|
-
functionResponse: {
|
|
69
|
-
...part.functionResponse,
|
|
70
|
-
response: replaceResponseText(part.functionResponse.response, compressed),
|
|
71
|
-
},
|
|
72
|
-
};
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
return { ...content, parts: newParts };
|
|
76
|
-
});
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
// ── helpers ──────────────────────────────────────────────────────────
|
|
80
|
-
|
|
81
|
-
/**
|
|
82
|
-
* Extract text from a Gemini function response object.
|
|
83
|
-
* The response can have `content` (string), `result` (string),
|
|
84
|
-
* or be a nested object that we JSON-serialize.
|
|
85
|
-
*/
|
|
86
|
-
function extractResponseText(response) {
|
|
87
|
-
if (!response) return "";
|
|
88
|
-
if (typeof response === "string") return response;
|
|
89
|
-
if (typeof response.content === "string") return response.content;
|
|
90
|
-
if (typeof response.result === "string") return response.result;
|
|
91
|
-
// For structured responses, serialize to JSON
|
|
92
|
-
return JSON.stringify(response);
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
/**
|
|
96
|
-
* Replace text in a Gemini function response object.
|
|
97
|
-
* Preserves the original shape (content field, result field, etc.).
|
|
98
|
-
*/
|
|
99
|
-
function replaceResponseText(response, newText) {
|
|
100
|
-
if (!response) return { content: newText };
|
|
101
|
-
if (typeof response === "string") return newText;
|
|
102
|
-
if (typeof response.content === "string") return { ...response, content: newText };
|
|
103
|
-
if (typeof response.result === "string") return { ...response, result: newText };
|
|
104
|
-
// Default: replace with content field
|
|
105
|
-
return { content: newText };
|
|
106
|
-
}
|
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Provider adapter registry.
|
|
3
|
-
*
|
|
4
|
-
* Each adapter implements the contract:
|
|
5
|
-
* extractToolResults(messages) → [{id, toolName, text, turnIndex}]
|
|
6
|
-
* rebuildMessages(messages, compressedMap) → messages
|
|
7
|
-
*
|
|
8
|
-
* @module
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
import * as anthropic from "./anthropic.js";
|
|
12
|
-
import * as openai from "./openai.js";
|
|
13
|
-
import * as gemini from "./gemini.js";
|
|
14
|
-
|
|
15
|
-
const adapters = {
|
|
16
|
-
anthropic,
|
|
17
|
-
openai,
|
|
18
|
-
gemini,
|
|
19
|
-
};
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* Hostname → provider mapping.
|
|
23
|
-
* @type {Record<string, string>}
|
|
24
|
-
*/
|
|
25
|
-
const HOST_MAP = {
|
|
26
|
-
"api.anthropic.com": "anthropic",
|
|
27
|
-
"api.openai.com": "openai",
|
|
28
|
-
"generativelanguage.googleapis.com": "gemini",
|
|
29
|
-
};
|
|
30
|
-
|
|
31
|
-
/**
|
|
32
|
-
* Get the adapter for a given provider name.
|
|
33
|
-
*
|
|
34
|
-
* @param {string} provider - "anthropic" | "openai" | "gemini"
|
|
35
|
-
* @returns {object|null} Adapter with extractToolResults and rebuildMessages, or null
|
|
36
|
-
*/
|
|
37
|
-
export function getAdapter(provider) {
|
|
38
|
-
return adapters[provider] || null;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* Detect the provider from a hostname.
|
|
43
|
-
*
|
|
44
|
-
* @param {string} hostname - e.g. "api.anthropic.com", "api.openai.com:443"
|
|
45
|
-
* @returns {string} Provider name or "unknown"
|
|
46
|
-
*/
|
|
47
|
-
export function detectProvider(hostname) {
|
|
48
|
-
// Strip port if present
|
|
49
|
-
const host = (hostname || "").split(":")[0];
|
|
50
|
-
return HOST_MAP[host] || "unknown";
|
|
51
|
-
}
|
|
@@ -1,71 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* OpenAI message format adapter.
|
|
3
|
-
*
|
|
4
|
-
* OpenAI Chat Completions API uses messages with `role: "tool"`.
|
|
5
|
-
* Each tool message has a `tool_call_id` linking back to the assistant's
|
|
6
|
-
* `tool_calls` array, and a `content` string with the tool output.
|
|
7
|
-
*
|
|
8
|
-
* @module
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* Extract tool results from OpenAI-format messages.
|
|
13
|
-
*
|
|
14
|
-
* @param {Array<object>} messages - OpenAI messages array
|
|
15
|
-
* @returns {Array<{id: string, toolName: string, text: string, turnIndex: number}>}
|
|
16
|
-
*/
|
|
17
|
-
export function extractToolResults(messages) {
|
|
18
|
-
const results = [];
|
|
19
|
-
const toolNameMap = buildToolNameMap(messages);
|
|
20
|
-
|
|
21
|
-
for (let i = 0; i < messages.length; i++) {
|
|
22
|
-
const msg = messages[i];
|
|
23
|
-
if (!msg || msg.role !== "tool") continue;
|
|
24
|
-
|
|
25
|
-
const id = msg.tool_call_id || "";
|
|
26
|
-
const text = typeof msg.content === "string" ? msg.content : "";
|
|
27
|
-
const toolName = toolNameMap.get(id) || "unknown";
|
|
28
|
-
|
|
29
|
-
results.push({ id, toolName, text, turnIndex: i });
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
return results;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* Rebuild messages replacing tool message content with compressed versions.
|
|
37
|
-
*
|
|
38
|
-
* @param {Array<object>} messages - Original OpenAI messages array
|
|
39
|
-
* @param {Map<string, string>|Record<string, string>} compressedMap - id → compressed text
|
|
40
|
-
* @returns {Array<object>} Deep-cloned messages with replacements applied
|
|
41
|
-
*/
|
|
42
|
-
export function rebuildMessages(messages, compressedMap) {
|
|
43
|
-
const map = compressedMap instanceof Map ? compressedMap : new Map(Object.entries(compressedMap));
|
|
44
|
-
return messages.map((msg) => {
|
|
45
|
-
if (!msg || msg.role !== "tool") return msg;
|
|
46
|
-
|
|
47
|
-
const id = msg.tool_call_id || "";
|
|
48
|
-
if (!map.has(id)) return msg;
|
|
49
|
-
|
|
50
|
-
return { ...msg, content: map.get(id) };
|
|
51
|
-
});
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
// ── helpers ──────────────────────────────────────────────────────────
|
|
55
|
-
|
|
56
|
-
/**
|
|
57
|
-
* Build a map of tool_call id → function name by scanning assistant messages.
|
|
58
|
-
* Assistant messages contain `tool_calls: [{id, function: {name, arguments}}]`.
|
|
59
|
-
*/
|
|
60
|
-
function buildToolNameMap(messages) {
|
|
61
|
-
const map = new Map();
|
|
62
|
-
for (const msg of messages) {
|
|
63
|
-
if (!msg || msg.role !== "assistant" || !Array.isArray(msg.tool_calls)) continue;
|
|
64
|
-
for (const call of msg.tool_calls) {
|
|
65
|
-
if (call.id && call.function) {
|
|
66
|
-
map.set(call.id, call.function.name || "unknown");
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
return map;
|
|
71
|
-
}
|
|
@@ -1,209 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Optional AI-powered compression for content that remains large
|
|
3
|
-
* after deterministic compression.
|
|
4
|
-
* Calls a cheap LLM model to summarize old tool results while
|
|
5
|
-
* preserving key data (file paths, error messages, numbers, status codes).
|
|
6
|
-
*
|
|
7
|
-
* Disabled by default. Enable via config: { enabled: true, provider, apiKey }.
|
|
8
|
-
* Zero external dependencies — uses vanilla Node.js https module.
|
|
9
|
-
*/
|
|
10
|
-
import https from "node:https";
|
|
11
|
-
|
|
12
|
-
// ---------------------------------------------------------------------------
|
|
13
|
-
// Provider definitions
|
|
14
|
-
// ---------------------------------------------------------------------------
|
|
15
|
-
const PROVIDERS = {
|
|
16
|
-
anthropic: {
|
|
17
|
-
hostname: "api.anthropic.com",
|
|
18
|
-
path: "/v1/messages",
|
|
19
|
-
defaultModel: "claude-3-5-haiku-20241022",
|
|
20
|
-
costPerInputToken: 0.25 / 1_000_000,
|
|
21
|
-
costPerOutputToken: 1.25 / 1_000_000,
|
|
22
|
-
buildRequest(text, model, apiKey, maxOutputTokens) {
|
|
23
|
-
const body = JSON.stringify({
|
|
24
|
-
model,
|
|
25
|
-
max_tokens: maxOutputTokens,
|
|
26
|
-
messages: [{ role: "user", content: buildPrompt(text) }]
|
|
27
|
-
});
|
|
28
|
-
return {
|
|
29
|
-
hostname: this.hostname,
|
|
30
|
-
path: this.path,
|
|
31
|
-
method: "POST",
|
|
32
|
-
headers: {
|
|
33
|
-
"Content-Type": "application/json",
|
|
34
|
-
"x-api-key": apiKey,
|
|
35
|
-
"anthropic-version": "2023-06-01"
|
|
36
|
-
},
|
|
37
|
-
body
|
|
38
|
-
};
|
|
39
|
-
},
|
|
40
|
-
parseResponse(data) {
|
|
41
|
-
const content = data.content?.[0]?.text ?? "";
|
|
42
|
-
const input = data.usage?.input_tokens ?? 0;
|
|
43
|
-
const output = data.usage?.output_tokens ?? 0;
|
|
44
|
-
return { text: content, tokensUsed: { input, output } };
|
|
45
|
-
}
|
|
46
|
-
},
|
|
47
|
-
|
|
48
|
-
openai: {
|
|
49
|
-
hostname: "api.openai.com",
|
|
50
|
-
path: "/v1/chat/completions",
|
|
51
|
-
defaultModel: "gpt-4o-mini",
|
|
52
|
-
costPerInputToken: 0.15 / 1_000_000,
|
|
53
|
-
costPerOutputToken: 0.6 / 1_000_000,
|
|
54
|
-
buildRequest(text, model, apiKey, maxOutputTokens) {
|
|
55
|
-
const body = JSON.stringify({
|
|
56
|
-
model,
|
|
57
|
-
max_tokens: maxOutputTokens,
|
|
58
|
-
messages: [
|
|
59
|
-
{ role: "system", content: SYSTEM_PROMPT },
|
|
60
|
-
{ role: "user", content: text }
|
|
61
|
-
]
|
|
62
|
-
});
|
|
63
|
-
return {
|
|
64
|
-
hostname: this.hostname,
|
|
65
|
-
path: this.path,
|
|
66
|
-
method: "POST",
|
|
67
|
-
headers: {
|
|
68
|
-
"Content-Type": "application/json",
|
|
69
|
-
Authorization: `Bearer ${apiKey}`
|
|
70
|
-
},
|
|
71
|
-
body
|
|
72
|
-
};
|
|
73
|
-
},
|
|
74
|
-
parseResponse(data) {
|
|
75
|
-
const content = data.choices?.[0]?.message?.content ?? "";
|
|
76
|
-
const input = data.usage?.prompt_tokens ?? 0;
|
|
77
|
-
const output = data.usage?.completion_tokens ?? 0;
|
|
78
|
-
return { text: content, tokensUsed: { input, output } };
|
|
79
|
-
}
|
|
80
|
-
},
|
|
81
|
-
|
|
82
|
-
gemini: {
|
|
83
|
-
hostname: "generativelanguage.googleapis.com",
|
|
84
|
-
defaultModel: "gemini-2.0-flash-lite",
|
|
85
|
-
costPerInputToken: 0.075 / 1_000_000,
|
|
86
|
-
costPerOutputToken: 0.3 / 1_000_000,
|
|
87
|
-
buildRequest(text, model, apiKey, maxOutputTokens) {
|
|
88
|
-
const path = `/v1beta/models/${model}:generateContent?key=${apiKey}`;
|
|
89
|
-
const body = JSON.stringify({
|
|
90
|
-
contents: [{ parts: [{ text: buildPrompt(text) }] }],
|
|
91
|
-
generationConfig: { maxOutputTokens }
|
|
92
|
-
});
|
|
93
|
-
return {
|
|
94
|
-
hostname: this.hostname,
|
|
95
|
-
path,
|
|
96
|
-
method: "POST",
|
|
97
|
-
headers: { "Content-Type": "application/json" },
|
|
98
|
-
body
|
|
99
|
-
};
|
|
100
|
-
},
|
|
101
|
-
parseResponse(data) {
|
|
102
|
-
const content = data.candidates?.[0]?.content?.parts?.[0]?.text ?? "";
|
|
103
|
-
const input = data.usageMetadata?.promptTokenCount ?? 0;
|
|
104
|
-
const output = data.usageMetadata?.candidatesTokenCount ?? 0;
|
|
105
|
-
return { text: content, tokensUsed: { input, output } };
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
};
|
|
109
|
-
|
|
110
|
-
// ---------------------------------------------------------------------------
|
|
111
|
-
// Prompt
|
|
112
|
-
// ---------------------------------------------------------------------------
|
|
113
|
-
const SYSTEM_PROMPT =
|
|
114
|
-
"You are a compression assistant. Summarize the following tool output concisely " +
|
|
115
|
-
"while preserving ALL: file paths, error messages, numbers, status codes, " +
|
|
116
|
-
"exit codes, and key technical details. Remove boilerplate, repeated lines, " +
|
|
117
|
-
"and verbose formatting. Output only the summary, no preamble.";
|
|
118
|
-
|
|
119
|
-
function buildPrompt(text) {
|
|
120
|
-
return `${SYSTEM_PROMPT}\n\n---\n${text}`;
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
// ---------------------------------------------------------------------------
|
|
124
|
-
// HTTPS helper
|
|
125
|
-
// ---------------------------------------------------------------------------
|
|
126
|
-
function httpsRequest({ hostname, path, method, headers, body }) {
|
|
127
|
-
return new Promise((resolve, reject) => {
|
|
128
|
-
const req = https.request({ hostname, path, method, headers }, (res) => {
|
|
129
|
-
const chunks = [];
|
|
130
|
-
res.on("data", (chunk) => chunks.push(chunk));
|
|
131
|
-
res.on("end", () => {
|
|
132
|
-
const raw = Buffer.concat(chunks).toString();
|
|
133
|
-
if (res.statusCode < 200 || res.statusCode >= 300) {
|
|
134
|
-
reject(new Error(`HTTP ${res.statusCode}: ${raw}`));
|
|
135
|
-
return;
|
|
136
|
-
}
|
|
137
|
-
try {
|
|
138
|
-
resolve(JSON.parse(raw));
|
|
139
|
-
} catch (err) {
|
|
140
|
-
reject(new Error(`Invalid JSON response: ${raw.slice(0, 200)}`));
|
|
141
|
-
}
|
|
142
|
-
});
|
|
143
|
-
res.on("error", reject);
|
|
144
|
-
});
|
|
145
|
-
req.on("error", reject);
|
|
146
|
-
if (body) req.write(body);
|
|
147
|
-
req.end();
|
|
148
|
-
});
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
// ---------------------------------------------------------------------------
|
|
152
|
-
// Public API
|
|
153
|
-
// ---------------------------------------------------------------------------
|
|
154
|
-
|
|
155
|
-
/**
|
|
156
|
-
* Check whether AI compression can be used with the given config.
|
|
157
|
-
* @param {{ enabled?: boolean, provider?: string, apiKey?: string }} config
|
|
158
|
-
* @returns {boolean}
|
|
159
|
-
*/
|
|
160
|
-
export function isAICompressionAvailable(config) {
|
|
161
|
-
if (!config || !config.enabled) return false;
|
|
162
|
-
const provider = config.provider ?? "anthropic";
|
|
163
|
-
if (!PROVIDERS[provider]) return false;
|
|
164
|
-
return Boolean(config.apiKey);
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
/**
|
|
168
|
-
* Compress text using a cheap LLM model.
|
|
169
|
-
* @param {string} text - text to compress
|
|
170
|
-
* @param {{ model?: string, apiKey?: string, provider?: string, maxOutputTokens?: number, enabled?: boolean }} options
|
|
171
|
-
* @returns {Promise<{ compressed: string, tokensUsed: { input: number, output: number }, cost: number }>}
|
|
172
|
-
*/
|
|
173
|
-
export async function compressWithAI(text, options = {}) {
|
|
174
|
-
const { enabled = false, provider: providerName = "anthropic", apiKey, maxOutputTokens = 1024 } = options;
|
|
175
|
-
|
|
176
|
-
// Disabled mode — return original text unchanged
|
|
177
|
-
if (!enabled || !apiKey) {
|
|
178
|
-
return { compressed: text, tokensUsed: { input: 0, output: 0 }, cost: 0 };
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
const provider = PROVIDERS[providerName];
|
|
182
|
-
if (!provider) {
|
|
183
|
-
return { compressed: text, tokensUsed: { input: 0, output: 0 }, cost: 0 };
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
const model = options.model ?? provider.defaultModel;
|
|
187
|
-
|
|
188
|
-
try {
|
|
189
|
-
const reqOpts = provider.buildRequest(text, model, apiKey, maxOutputTokens);
|
|
190
|
-
const data = await httpsRequest(reqOpts);
|
|
191
|
-
const { text: compressed, tokensUsed } = provider.parseResponse(data);
|
|
192
|
-
|
|
193
|
-
if (!compressed) {
|
|
194
|
-
return { compressed: text, tokensUsed: { input: 0, output: 0 }, cost: 0 };
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
const cost =
|
|
198
|
-
tokensUsed.input * provider.costPerInputToken +
|
|
199
|
-
tokensUsed.output * provider.costPerOutputToken;
|
|
200
|
-
|
|
201
|
-
return { compressed, tokensUsed, cost };
|
|
202
|
-
} catch {
|
|
203
|
-
// Graceful degradation — return original text on any failure
|
|
204
|
-
return { compressed: text, tokensUsed: { input: 0, output: 0 }, cost: 0 };
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
/** Exported for testing */
|
|
209
|
-
export { PROVIDERS };
|