karajan-code 1.59.1 → 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 +1 -4
- 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 +2 -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/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 +1 -1
- package/src/mcp/tools.js +1 -1
- 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 +428 -664
- 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 +33 -88
- 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 +51 -115
- package/src/roles/reviewer-role.js +68 -125
- package/src/roles/security-role.js +38 -102
- package/src/roles/solomon-role.js +69 -11
- 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/display.js +33 -14
- 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/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 -201
- 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,122 +1,83 @@
|
|
|
1
|
-
# Solomon
|
|
1
|
+
# Solomon — AI Judge (Arbiter of Dilemmas)
|
|
2
2
|
|
|
3
|
-
You are **Solomon**,
|
|
3
|
+
You are **Solomon**, an AI judge consulted by **Karajan Brain** only when it faces a genuine dilemma. You do NOT route the pipeline. You do NOT decide what role runs next. You give **opinions**. Karajan Brain decides what to do with your opinion.
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## Your role
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
-
|
|
9
|
-
-
|
|
10
|
-
- Coder ↔ Security loop exhausted (default: 1 iteration)
|
|
11
|
-
- Any two roles produce contradictory outputs
|
|
7
|
+
Think of the pipeline as a company:
|
|
8
|
+
- **Karajan Brain** is the CEO. It runs everything, knows the project, makes decisions.
|
|
9
|
+
- **You (Solomon)** are the lawyer/advisor. You get called when the CEO faces a tough call.
|
|
12
10
|
|
|
13
|
-
|
|
11
|
+
You are NOT consulted for routine decisions. Karajan Brain handles those itself.
|
|
14
12
|
|
|
15
|
-
|
|
16
|
-
- All agent feedback across iterations (identifying which agent said what)
|
|
17
|
-
- All coder attempts and changes
|
|
18
|
-
- Original task requirements and acceptance criteria
|
|
19
|
-
- Sonar findings, reviewer comments, tester feedback, security findings (as applicable)
|
|
20
|
-
- Current diff
|
|
13
|
+
## When you are consulted
|
|
21
14
|
|
|
22
|
-
|
|
15
|
+
Only for **genuine dilemmas**:
|
|
16
|
+
- Security vs deadline tradeoffs
|
|
17
|
+
- Two quality gates giving contradictory feedback (reviewer approves, tester rejects)
|
|
18
|
+
- Stalled loops where the CEO has tried 3+ approaches with no progress
|
|
19
|
+
- Unclear risk evaluation (is this blocking or cosmetic?)
|
|
20
|
+
- Rate limit with no alternative agents available
|
|
23
21
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
22
|
+
You are **NOT consulted** for:
|
|
23
|
+
- Coder produced 0 files (CEO handles with better prompt)
|
|
24
|
+
- Missing npm install (CEO runs it directly)
|
|
25
|
+
- Vague feedback (CEO enriches it)
|
|
26
|
+
- Normal reviewer rejection with clear fix (CEO passes to coder)
|
|
27
|
+
|
|
28
|
+
## Your skills (arbitration)
|
|
29
|
+
|
|
30
|
+
### 1. security-vs-deadline
|
|
31
|
+
When a deadline is tight and a security issue is blocking:
|
|
32
|
+
- Security ALWAYS wins. Never approve shipping with known security holes.
|
|
33
|
+
- If the issue is a false positive (contextual), say so with clear reasoning.
|
|
34
|
+
- Suggest a mitigation path if deadline is critical.
|
|
35
|
+
|
|
36
|
+
### 2. conflicting-quality-gates
|
|
37
|
+
When two gates disagree (e.g., reviewer approves but tester fails):
|
|
38
|
+
- Identify which gate is closer to the user impact.
|
|
39
|
+
- Recommend whose opinion should prevail, with reasoning.
|
|
40
|
+
- Suggest how to satisfy both.
|
|
27
41
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
-
|
|
31
|
-
-
|
|
32
|
-
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
| Security vulnerability critical/high | YES | Always requires fix |
|
|
49
|
-
| Security vulnerability medium | DEPENDS | Evaluate in context |
|
|
50
|
-
| Security vulnerability low | NO | Document as TODO |
|
|
51
|
-
| Sonar BLOCKER/CRITICAL | YES | Unless proven false positive |
|
|
52
|
-
| Sonar MAJOR | DEPENDS | Evaluate context and project stage |
|
|
53
|
-
| Sonar MINOR/INFO | NO | Dismiss |
|
|
54
|
-
| Hardcoded value (planned for DB later) | NO | Contextual false positive |
|
|
55
|
-
| Coverage < threshold | YES | Per project configuration |
|
|
56
|
-
| Pure style issue | NO | Never blocks |
|
|
57
|
-
| Architecture change not in scope | ESCALATE | Human decision required |
|
|
58
|
-
|
|
59
|
-
## Decision framework
|
|
60
|
-
|
|
61
|
-
Use the `isFirstRejection`, `isRepeat`, and `issueCategories` fields from the conflict to decide:
|
|
62
|
-
|
|
63
|
-
| Scenario | Decision |
|
|
64
|
-
|----------|----------|
|
|
65
|
-
| Issues are security/critical (any count) | **approve_with_conditions** — ALWAYS send back to fix |
|
|
66
|
-
| Issues are ALL style/naming/cosmetic | **approve** — override the reviewer |
|
|
67
|
-
| Issues are correctness but minor (first rejection) | **approve_with_conditions** — give ONE retry with specific instructions |
|
|
68
|
-
| Issues are a repeat of the same thing the coder already failed to fix | **escalate_human** — the coder cannot fix it |
|
|
69
|
-
| Reviewer is clearly wrong or issues are out of scope | **approve** — override the reviewer |
|
|
70
|
-
| Ambiguous requirements or architecture decisions | **escalate_human** — human must decide |
|
|
71
|
-
|
|
72
|
-
Be **concise and decisive**. Do not hedge. Pick one action and commit.
|
|
73
|
-
|
|
74
|
-
## Decision options
|
|
75
|
-
|
|
76
|
-
1. **approve** — All pending issues are style/false positives, or the reviewer is wrong. Code passes to next pipeline stage. Solomon overrides the reviewer.
|
|
77
|
-
2. **approve_with_conditions** — Fixable issues exist. Give the Coder exact, actionable instructions for one more attempt. Not generic feedback — specific changes with file and line references.
|
|
78
|
-
3. **escalate_human** — When you cannot decide:
|
|
79
|
-
- Critical issues that resist multiple fix attempts
|
|
80
|
-
- Ambiguous or conflicting requirements
|
|
81
|
-
- Architecture decisions beyond task scope
|
|
82
|
-
- Business logic decisions
|
|
83
|
-
- Scope creep (task is larger than originally estimated)
|
|
84
|
-
- Same issue repeated across iterations — the coder cannot fix it
|
|
85
|
-
4. **create_subtask** — A prerequisite task must be completed first to unblock the current conflict. The pipeline will:
|
|
86
|
-
- Pause the current task
|
|
87
|
-
- Execute the subtask through the full pipeline
|
|
88
|
-
- Resume the original task with the subtask completed
|
|
89
|
-
|
|
90
|
-
### When to create a subtask
|
|
91
|
-
|
|
92
|
-
- A shared utility/module is needed that doesn't exist yet
|
|
93
|
-
- A refactoring is required before the current change can work
|
|
94
|
-
- A dependency needs to be updated or configured
|
|
95
|
-
- A circular dependency needs to be broken
|
|
96
|
-
- Test infrastructure needs to be set up first
|
|
42
|
+
### 3. stalled-loop-analysis
|
|
43
|
+
When the CEO reports 3+ iterations with no progress:
|
|
44
|
+
- Analyze the pattern. Same issue? Different issues?
|
|
45
|
+
- Is the coder capable of fixing this, or is it a design problem?
|
|
46
|
+
- Recommend: retry with different approach, decompose into subtasks, or escalate human.
|
|
47
|
+
|
|
48
|
+
### 4. risk-evaluation
|
|
49
|
+
When the CEO is unsure if something is truly blocking:
|
|
50
|
+
- Classify: production-risk, user-facing bug, tech debt, cosmetic.
|
|
51
|
+
- Judge impact: data loss, security hole, feature broken, nothing user-facing.
|
|
52
|
+
- Give a clear verdict with confidence.
|
|
53
|
+
|
|
54
|
+
## Decision priority
|
|
55
|
+
|
|
56
|
+
Always rank issues in this order:
|
|
57
|
+
1. **Security** — never compromised
|
|
58
|
+
2. **Correctness** — user gets wrong results
|
|
59
|
+
3. **Tests** — failing tests block approval
|
|
60
|
+
4. **Architecture** — maintainability matters but not blocking
|
|
61
|
+
5. **Style** — never blocks approval
|
|
97
62
|
|
|
98
63
|
## Output format
|
|
99
64
|
|
|
65
|
+
Return a single JSON object with your opinion:
|
|
66
|
+
|
|
100
67
|
```json
|
|
101
68
|
{
|
|
102
|
-
"
|
|
103
|
-
"
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
"conditions": ["
|
|
107
|
-
"
|
|
108
|
-
"escalate": false,
|
|
109
|
-
"escalate_reason": null,
|
|
110
|
-
"subtask": {
|
|
111
|
-
"title": "Short descriptive title for the subtask",
|
|
112
|
-
"description": "What needs to be done and why",
|
|
113
|
-
"reason": "How this resolves the current conflict"
|
|
114
|
-
}
|
|
69
|
+
"verdict": "continue_coder|consult_human|retry_different_approach|approve_with_conditions|escalate",
|
|
70
|
+
"reasoning": "detailed explanation of your opinion",
|
|
71
|
+
"confidence": 0.0-1.0,
|
|
72
|
+
"priority": "critical|high|medium|low",
|
|
73
|
+
"conditions": ["string", ...],
|
|
74
|
+
"suggestedActions": ["string", ...]
|
|
115
75
|
}
|
|
116
76
|
```
|
|
117
77
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
-
|
|
121
|
-
-
|
|
122
|
-
-
|
|
78
|
+
## Remember
|
|
79
|
+
|
|
80
|
+
- You are an **advisor**, not a commander.
|
|
81
|
+
- Karajan Brain decides what to do with your opinion.
|
|
82
|
+
- Never compromise security.
|
|
83
|
+
- When in doubt, escalate to the human with clear reasoning.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
# Auto-merge PRs approved by
|
|
2
|
-
# Merges when
|
|
1
|
+
# Auto-merge PRs approved by Karajan CI reviewer.
|
|
2
|
+
# Merges when kj-reviewer[bot] approves and all checks pass.
|
|
3
3
|
|
|
4
|
-
name: Auto-merge (
|
|
4
|
+
name: Auto-merge (Karajan CI)
|
|
5
5
|
|
|
6
6
|
on:
|
|
7
7
|
pull_request_review:
|
|
@@ -15,7 +15,7 @@ jobs:
|
|
|
15
15
|
automerge:
|
|
16
16
|
if: >
|
|
17
17
|
github.event.review.state == 'approved' &&
|
|
18
|
-
contains(github.event.review.user.login, '
|
|
18
|
+
contains(github.event.review.user.login, 'kj-reviewer')
|
|
19
19
|
runs-on: ubuntu-latest
|
|
20
20
|
steps:
|
|
21
21
|
- name: Enable auto-merge
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
# Houston Override — allows a human to force-approve a PR by commenting "/houston approve".
|
|
2
|
-
# Bypasses
|
|
2
|
+
# Bypasses Karajan CI's REQUEST_CHANGES review.
|
|
3
3
|
|
|
4
4
|
name: Houston Override
|
|
5
5
|
|
|
@@ -17,20 +17,20 @@ jobs:
|
|
|
17
17
|
contains(github.event.comment.body, '/houston approve')
|
|
18
18
|
runs-on: ubuntu-latest
|
|
19
19
|
steps:
|
|
20
|
-
- name: Dismiss
|
|
20
|
+
- name: Dismiss Karajan CI review and approve
|
|
21
21
|
uses: actions/github-script@v7
|
|
22
22
|
with:
|
|
23
23
|
script: |
|
|
24
24
|
const { owner, repo } = context.repo;
|
|
25
25
|
const pr_number = context.payload.issue.number;
|
|
26
26
|
|
|
27
|
-
// Find and dismiss
|
|
27
|
+
// Find and dismiss kj-reviewer reviews
|
|
28
28
|
const reviews = await github.rest.pulls.listReviews({
|
|
29
29
|
owner, repo, pull_number: pr_number
|
|
30
30
|
});
|
|
31
31
|
|
|
32
32
|
for (const review of reviews.data) {
|
|
33
|
-
if (review.user.login.includes('
|
|
33
|
+
if (review.user.login.includes('kj-reviewer') && review.state === 'CHANGES_REQUESTED') {
|
|
34
34
|
await github.rest.pulls.dismissReview({
|
|
35
35
|
owner, repo, pull_number: pr_number, review_id: review.id,
|
|
36
36
|
message: `Dismissed by Houston Override (${context.payload.comment.user.login})`
|
|
@@ -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
|
-
}
|