@skroyc/librarian 0.1.0 → 0.2.1
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 +4 -16
- package/dist/agents/context-schema.d.ts +1 -1
- package/dist/agents/context-schema.d.ts.map +1 -1
- package/dist/agents/context-schema.js +5 -2
- package/dist/agents/context-schema.js.map +1 -1
- package/dist/agents/react-agent.d.ts.map +1 -1
- package/dist/agents/react-agent.js +63 -170
- package/dist/agents/react-agent.js.map +1 -1
- package/dist/agents/tool-runtime.d.ts.map +1 -1
- package/dist/cli.d.ts +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +53 -49
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts +1 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +115 -69
- package/dist/config.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +246 -150
- package/dist/index.js.map +1 -1
- package/dist/tools/file-finding.tool.d.ts +1 -1
- package/dist/tools/file-finding.tool.d.ts.map +1 -1
- package/dist/tools/file-finding.tool.js +70 -130
- package/dist/tools/file-finding.tool.js.map +1 -1
- package/dist/tools/file-listing.tool.d.ts +7 -1
- package/dist/tools/file-listing.tool.d.ts.map +1 -1
- package/dist/tools/file-listing.tool.js +96 -80
- package/dist/tools/file-listing.tool.js.map +1 -1
- package/dist/tools/file-reading.tool.d.ts +4 -1
- package/dist/tools/file-reading.tool.d.ts.map +1 -1
- package/dist/tools/file-reading.tool.js +107 -45
- package/dist/tools/file-reading.tool.js.map +1 -1
- package/dist/tools/grep-content.tool.d.ts +13 -1
- package/dist/tools/grep-content.tool.d.ts.map +1 -1
- package/dist/tools/grep-content.tool.js +186 -144
- package/dist/tools/grep-content.tool.js.map +1 -1
- package/dist/utils/error-utils.d.ts +9 -0
- package/dist/utils/error-utils.d.ts.map +1 -0
- package/dist/utils/error-utils.js +61 -0
- package/dist/utils/error-utils.js.map +1 -0
- package/dist/utils/file-utils.d.ts +1 -0
- package/dist/utils/file-utils.d.ts.map +1 -1
- package/dist/utils/file-utils.js +81 -9
- package/dist/utils/file-utils.js.map +1 -1
- package/dist/utils/format-utils.d.ts +25 -0
- package/dist/utils/format-utils.d.ts.map +1 -0
- package/dist/utils/format-utils.js +111 -0
- package/dist/utils/format-utils.js.map +1 -0
- package/dist/utils/gitignore-service.d.ts +10 -0
- package/dist/utils/gitignore-service.d.ts.map +1 -0
- package/dist/utils/gitignore-service.js +91 -0
- package/dist/utils/gitignore-service.js.map +1 -0
- package/dist/utils/logger.d.ts +2 -2
- package/dist/utils/logger.d.ts.map +1 -1
- package/dist/utils/logger.js +35 -34
- package/dist/utils/logger.js.map +1 -1
- package/dist/utils/path-utils.js +3 -3
- package/dist/utils/path-utils.js.map +1 -1
- package/package.json +1 -1
- package/src/agents/context-schema.ts +5 -2
- package/src/agents/react-agent.ts +694 -784
- package/src/agents/tool-runtime.ts +4 -4
- package/src/cli.ts +95 -57
- package/src/config.ts +192 -90
- package/src/index.ts +402 -180
- package/src/tools/file-finding.tool.ts +198 -310
- package/src/tools/file-listing.tool.ts +245 -202
- package/src/tools/file-reading.tool.ts +225 -138
- package/src/tools/grep-content.tool.ts +387 -307
- package/src/utils/error-utils.ts +95 -0
- package/src/utils/file-utils.ts +104 -19
- package/src/utils/format-utils.ts +190 -0
- package/src/utils/gitignore-service.ts +123 -0
- package/src/utils/logger.ts +112 -77
- package/src/utils/path-utils.ts +3 -3
|
@@ -1,253 +1,139 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { mkdir, rm } from "node:fs/promises";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { Readable } from "node:stream";
|
|
6
|
+
import { ChatAnthropic } from "@langchain/anthropic";
|
|
7
|
+
import { HumanMessage } from "@langchain/core/messages";
|
|
8
|
+
import { ChatGoogleGenerativeAI } from "@langchain/google-genai";
|
|
9
|
+
import { ChatOpenAI } from "@langchain/openai";
|
|
1
10
|
import {
|
|
2
|
-
createAgent,
|
|
3
11
|
anthropicPromptCachingMiddleware,
|
|
12
|
+
createAgent,
|
|
13
|
+
type DynamicStructuredTool,
|
|
4
14
|
todoListMiddleware,
|
|
5
|
-
tool as createTool,
|
|
6
|
-
type DynamicStructuredTool
|
|
7
15
|
} from "langchain";
|
|
8
16
|
import type { z } from "zod";
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
import { ChatOpenAI } from "@langchain/openai";
|
|
14
|
-
import { ChatAnthropic } from "@langchain/anthropic";
|
|
15
|
-
import { ChatGoogleGenerativeAI } from "@langchain/google-genai";
|
|
16
|
-
import { HumanMessage } from "@langchain/core/messages";
|
|
17
|
+
import { findTool } from "../tools/file-finding.tool.js";
|
|
18
|
+
import { listTool } from "../tools/file-listing.tool.js";
|
|
19
|
+
import { viewTool } from "../tools/file-reading.tool.js";
|
|
20
|
+
import { grepTool } from "../tools/grep-content.tool.js";
|
|
17
21
|
import { logger } from "../utils/logger.js";
|
|
18
|
-
import os from "node:os";
|
|
19
|
-
import { mkdir, rm } from "node:fs/promises";
|
|
20
|
-
import path from "node:path";
|
|
21
|
-
import { spawn } from "node:child_process";
|
|
22
|
-
import { Readable } from "node:stream";
|
|
23
22
|
import type { AgentContext } from "./context-schema.js";
|
|
24
23
|
|
|
25
24
|
/**
|
|
26
25
|
* Configuration interface for ReactAgent
|
|
27
26
|
*/
|
|
28
27
|
export interface ReactAgentConfig {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
28
|
+
/** AI provider configuration including type, API key, and optional model/base URL */
|
|
29
|
+
aiProvider: {
|
|
30
|
+
type:
|
|
31
|
+
| "openai"
|
|
32
|
+
| "anthropic"
|
|
33
|
+
| "google"
|
|
34
|
+
| "openai-compatible"
|
|
35
|
+
| "anthropic-compatible"
|
|
36
|
+
| "claude-code"
|
|
37
|
+
| "gemini-cli";
|
|
38
|
+
apiKey: string;
|
|
39
|
+
model?: string;
|
|
40
|
+
baseURL?: string;
|
|
41
|
+
};
|
|
42
|
+
/** Working directory where the agent operates */
|
|
43
|
+
workingDir: string;
|
|
44
|
+
/** Optional technology context for dynamic system prompt construction */
|
|
45
|
+
technology?: {
|
|
46
|
+
name: string;
|
|
47
|
+
repository: string;
|
|
48
|
+
branch: string;
|
|
49
|
+
};
|
|
50
|
+
/** Optional context schema for runtime context validation */
|
|
51
|
+
contextSchema?: z.ZodType | undefined;
|
|
53
52
|
}
|
|
54
53
|
|
|
55
54
|
export class ReactAgent {
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
- Evidence-based conclusions backed by specific source citations
|
|
99
|
-
- Clear and accessible technical communication
|
|
100
|
-
- Intellectual honesty about knowledge boundaries
|
|
55
|
+
private readonly aiModel?:
|
|
56
|
+
| ChatOpenAI
|
|
57
|
+
| ChatAnthropic
|
|
58
|
+
| ChatGoogleGenerativeAI;
|
|
59
|
+
private readonly tools: DynamicStructuredTool[];
|
|
60
|
+
private agent?: ReturnType<typeof createAgent>;
|
|
61
|
+
private readonly config: ReactAgentConfig;
|
|
62
|
+
private readonly contextSchema?: z.ZodType | undefined;
|
|
63
|
+
|
|
64
|
+
constructor(config: ReactAgentConfig) {
|
|
65
|
+
this.config = config;
|
|
66
|
+
this.contextSchema = config.contextSchema;
|
|
67
|
+
|
|
68
|
+
if (
|
|
69
|
+
config.aiProvider.type !== "claude-code" &&
|
|
70
|
+
config.aiProvider.type !== "gemini-cli"
|
|
71
|
+
) {
|
|
72
|
+
this.aiModel = this.createAIModel(config.aiProvider);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Initialize tools - modernized tool pattern
|
|
76
|
+
this.tools = [listTool, viewTool, grepTool, findTool];
|
|
77
|
+
|
|
78
|
+
logger.info("AGENT", "Initializing ReactAgent", {
|
|
79
|
+
aiProviderType: config.aiProvider.type,
|
|
80
|
+
model: config.aiProvider.model,
|
|
81
|
+
workingDir: config.workingDir.replace(os.homedir(), "~"),
|
|
82
|
+
toolCount: this.tools.length,
|
|
83
|
+
hasContextSchema: !!this.contextSchema,
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Creates a dynamic system prompt based on current configuration and technology context
|
|
89
|
+
* @returns A context-aware system prompt string
|
|
90
|
+
*/
|
|
91
|
+
createDynamicSystemPrompt(): string {
|
|
92
|
+
const { workingDir, technology } = this.config;
|
|
93
|
+
|
|
94
|
+
// Dynamic system prompt generation code
|
|
95
|
+
let prompt = `
|
|
96
|
+
You are a **Codebase Investigator**. Your mission is to provide technical insights grounded in source code evidence. You approach every query as a methodical investigation, prioritizing verification over assumptions and ensuring every conclusion is backed by specific file citations.
|
|
101
97
|
|
|
102
98
|
# Instructions
|
|
103
99
|
|
|
104
100
|
## Investigation Protocol
|
|
105
101
|
|
|
106
|
-
**
|
|
107
|
-
-
|
|
108
|
-
-
|
|
109
|
-
-
|
|
110
|
-
|
|
111
|
-
**INVESTIGATION RULE 2 - Methodology:**
|
|
112
|
-
- Start by mapping the codebase structure (directories, key files)
|
|
113
|
-
- Trace how components connect through imports, exports, and function calls
|
|
114
|
-
- Validate assumptions by reading actual implementations
|
|
115
|
-
- Build your answer from verified source evidence, not assumptions
|
|
116
|
-
|
|
117
|
-
**INVESTIGATION RULE 3 - User Focus:**
|
|
118
|
-
- Prioritize complete answers over asking follow-up questions
|
|
119
|
-
- Provide context that helps users understand patterns, not just individual functions
|
|
120
|
-
- Bridge the gap between code behavior and practical application
|
|
121
|
-
|
|
122
|
-
## Verification Threshold
|
|
123
|
-
|
|
124
|
-
**DECISION RULE 1 - Action Threshold:**
|
|
125
|
-
- If seeing a file would improve your answer, read it immediately—do not ask the user first
|
|
126
|
-
- If asked about an unseen component, investigate it before responding
|
|
127
|
-
|
|
128
|
-
**DECISION RULE 2 - Confidence Check:**
|
|
129
|
-
- Before finalizing any answer, verify: "Am I relying on external libraries or modules I haven't confirmed in this codebase?"
|
|
130
|
-
- If yes: either read the local source or explicitly state the limitation
|
|
131
|
-
|
|
132
|
-
**DECISION RULE 3 - Ambiguity Protocol:**
|
|
133
|
-
- When multiple interpretations exist, state the uncertainty
|
|
134
|
-
- Provide the most likely answer with supporting evidence
|
|
135
|
-
- Note alternative possibilities and their conditions
|
|
102
|
+
- **Evidence First**: Every claim must be tied to specific source evidence in the sandboxed directory. Read files rather than speculate.
|
|
103
|
+
- **Methodical Mapping**: Start by understanding the codebase structure and component connections (imports, exports, calls).
|
|
104
|
+
- **Execution Over Inquiry**: Prioritize completing the investigation and providing a comprehensive answer over asking follow-up questions.
|
|
105
|
+
- **Pattern Recognition**: Bridge the gap between raw code behavior and practical application, helping users understand general patterns.
|
|
136
106
|
|
|
137
|
-
##
|
|
107
|
+
## Verification Standards
|
|
138
108
|
|
|
139
|
-
**
|
|
140
|
-
-
|
|
141
|
-
- Do not settle on the first explanation you find
|
|
109
|
+
- **Autonomous Action**: If a file would improve your answer, read it immediately. Do not ask for permission.
|
|
110
|
+
- **Internal Validation**: Distinguish between confirmed local implementations and external library behavior. Verify local code before assuming standard library defaults.
|
|
142
111
|
|
|
143
|
-
|
|
144
|
-
- Use file reads to confirm which explanation matches reality
|
|
145
|
-
- Look for contradictory evidence in other files
|
|
112
|
+
## Critical Thinking
|
|
146
113
|
|
|
147
|
-
**
|
|
148
|
-
-
|
|
149
|
-
-
|
|
150
|
-
-
|
|
114
|
+
- **Exhaustive Exploration**: Never declare something missing without checking configuration files, related directories, and alternative naming patterns. If initial searches fail, pivot your strategy.
|
|
115
|
+
- **Hypothesis Testing**: For complex logic, consider multiple explanations. Use file reads to confirm which matches reality and look for contradictory evidence.
|
|
116
|
+
- **Self-Correction**: Regularly challenge your own findings. If new evidence contradicts your planned response, update your mental model immediately.
|
|
117
|
+
- **Handling Ambiguity**: When multiple interpretations exist, state the uncertainty and provide the most likely answer with supporting evidence.
|
|
151
118
|
|
|
152
|
-
##
|
|
119
|
+
## Evidence & Citations
|
|
153
120
|
|
|
154
|
-
**
|
|
155
|
-
-
|
|
156
|
-
-
|
|
121
|
+
- **Source of Truth**: The local working directory is the definitive truth. If external documentation contradicts local code, the local code is correct.
|
|
122
|
+
- **Mandatory Citations**: Every technical claim must cite specific repository-relative file paths (e.g., \`src/utils/logger.ts\`) and, where possible, line numbers or function names. Vague references are insufficient.
|
|
123
|
+
- **Knowledge Gaps**: If information is missing from the directory, explicitly state what you couldn't find before providing general industry standard alternatives.
|
|
124
|
+
- **Fact vs. Inference**: Distinguish clearly between verified code behavior (citing files) and inferred patterns or conventions.
|
|
157
125
|
|
|
158
|
-
|
|
159
|
-
- When initial searches fail, expand your approach
|
|
160
|
-
- Check configuration files, related directories, or alternative naming patterns
|
|
161
|
-
- Never declare something missing without exhaustive exploration
|
|
126
|
+
## Thoroughness & Constraints
|
|
162
127
|
|
|
163
|
-
**
|
|
164
|
-
-
|
|
165
|
-
-
|
|
128
|
+
- **Complete Coverage**: Address every part of the user's question, explaining both specific implementations and the general patterns they follow.
|
|
129
|
+
- **Contextual Awareness**: Always consider configuration files (e.g., \`package.json\`, \`.env\`, yaml configs) that might affect the behavior of the code you are analyzing.
|
|
130
|
+
- **Graceful Failure**: If tools fail or files are inaccessible, document the issue clearly. Attempt alternative discovery methods (e.g., searching for related terms) before acknowledging a gap in evidence.
|
|
166
131
|
|
|
167
|
-
##
|
|
132
|
+
## Output Standards
|
|
168
133
|
|
|
169
|
-
**
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
**SCOPE 2 - Supporting Context:**
|
|
173
|
-
- Language documentation explains expected behavior
|
|
174
|
-
- Configuration files set constraints and options
|
|
175
|
-
- Use these to interpret what you find
|
|
176
|
-
|
|
177
|
-
**SCOPE 3 - Inferred Patterns:**
|
|
178
|
-
- Consistent patterns across files suggest conventions
|
|
179
|
-
- Use patterns to guide interpretation, not as definitive proof
|
|
180
|
-
|
|
181
|
-
**NOTE:** If external documentation contradicts local code, the local code is always correct for this repository.
|
|
182
|
-
|
|
183
|
-
## Citation Standards Protocol
|
|
184
|
-
|
|
185
|
-
**CITATION RULE 1 - Evidence Requirement:**
|
|
186
|
-
- Every technical claim must cite specific file paths and, where possible, line numbers or function names
|
|
187
|
-
- Vague references like "the code" or "this file" are insufficient
|
|
188
|
-
|
|
189
|
-
**CITATION RULE 2 - Acknowledgment Protocol:**
|
|
190
|
-
- When information is not found in the directory, explicitly state: "Based on the accessible files, I cannot find [X], but typically [Y] applies."
|
|
191
|
-
|
|
192
|
-
**CITATION RULE 3 - Confidence Calibration:**
|
|
193
|
-
- Distinguish between verified facts (citing files) and inferred patterns (noting the distinction)
|
|
194
|
-
- Never present inference as fact without clear labeling
|
|
195
|
-
|
|
196
|
-
## Thoroughness Verification System
|
|
197
|
-
|
|
198
|
-
**VERIFICATION RULE 1 - Configuration Check:**
|
|
199
|
-
- Have you considered all config files that might affect this behavior?
|
|
200
|
-
- Do not explain code in isolation from its configuration context
|
|
201
|
-
|
|
202
|
-
**VERIFICATION RULE 2 - Principle Coverage:**
|
|
203
|
-
- Does your answer explain both the specific case AND the general pattern?
|
|
204
|
-
- Help users apply this knowledge beyond the immediate example
|
|
205
|
-
|
|
206
|
-
**VERIFICATION RULE 3 - Question Coverage:**
|
|
207
|
-
- Have you addressed every part of the user's question?
|
|
208
|
-
- Note any intentional limitations or scope boundaries
|
|
209
|
-
|
|
210
|
-
## Failure Response System
|
|
211
|
-
|
|
212
|
-
**RESPONSE RULE 1 - Temporary Failures:**
|
|
213
|
-
- Timeouts and transient issues warrant retry (max 3 attempts)
|
|
214
|
-
- After retries exhaust, document the access issue
|
|
215
|
-
|
|
216
|
-
**RESPONSE RULE 2 - Permanent Failures:**
|
|
217
|
-
- Missing files, permission issues: stop retrying immediately
|
|
218
|
-
- Attempt alternative discovery methods or acknowledge the gap
|
|
219
|
-
|
|
220
|
-
**RESPONSE RULE 3 - Best Effort Resolution:**
|
|
221
|
-
- For obfuscated, missing, or inaccessible code:
|
|
222
|
-
- Provide answers grounded in standard practices
|
|
223
|
-
- Explicitly note confidence levels and knowledge boundaries
|
|
224
|
-
|
|
225
|
-
## Response Integrity Standard
|
|
226
|
-
|
|
227
|
-
**INTEGRITY RULE 1 - No Premature Responses:**
|
|
228
|
-
- Complete your full investigation before answering
|
|
229
|
-
- Resist the urge to respond before verification
|
|
230
|
-
|
|
231
|
-
**INTEGRITY RULE 2 - Evidence Compilation:**
|
|
232
|
-
- Gather all relevant file evidence before synthesizing
|
|
233
|
-
- Confirm no stone has been left unturned
|
|
234
|
-
|
|
235
|
-
**INTEGRITY RULE 3 - Final Validation:**
|
|
236
|
-
- Deliver your answer only when:
|
|
237
|
-
- All tools have been exhausted
|
|
238
|
-
- Evidence supports your conclusions
|
|
239
|
-
- You can cite specific sources for every claim
|
|
240
|
-
|
|
241
|
-
**INTEGRITY RULE 4 - Developer Consumption Focus (Default Behavior):**
|
|
242
|
-
- Frame explanations around how a developer WOULD USE this code, not how they might EXTEND it
|
|
243
|
-
- Focus on APIs, parameters, return values, and integration patterns
|
|
244
|
-
- Provide usage examples that show calling code, not implementation code
|
|
245
|
-
- When explaining implementation details, contextualize them for consumption use cases
|
|
246
|
-
|
|
247
|
-
**EXCEPTION - Architecture/Extension Queries:**
|
|
248
|
-
- ONLY deviate from the consumption focus when the user explicitly asks for it
|
|
249
|
-
- Examples: "What is the architecture of X?", "How can we extend X?", "How is X structured?"
|
|
250
|
-
- In these cases, provide architectural perspective as requested
|
|
134
|
+
- **Developer Consumption Focus**: By default, frame explanations around how a developer would **use** the code (APIs, parameters, usage examples) rather than how it is implemented internally. Provide usage examples showing calling code, not implementation logic.
|
|
135
|
+
- **Architectural Exception**: Only provide deep architectural or implementation analysis if the user explicitly asks for it (e.g., "How is X structured?", "Explain the architecture of Y").
|
|
136
|
+
- **Integrity**: Complete your full investigation and verify all evidence before delivering your final answer. Ensure citations support every claim.
|
|
251
137
|
|
|
252
138
|
# Reasoning Steps
|
|
253
139
|
|
|
@@ -345,584 +231,608 @@ Remember: ALL tool calls MUST be executed using absolute path in \`[WORKING_DIRE
|
|
|
345
231
|
|
|
346
232
|
---
|
|
347
233
|
|
|
348
|
-
**
|
|
234
|
+
**Final Reminder: Every claim must be backed by evidence. If you haven't verified it in the code, don't say it.**
|
|
349
235
|
`;
|
|
350
236
|
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
237
|
+
// Add technology context if available
|
|
238
|
+
if (technology) {
|
|
239
|
+
prompt = prompt.replace(
|
|
240
|
+
"<context_block>",
|
|
241
|
+
`You have been provided the **${technology.name}** repository.
|
|
356
242
|
Repository: ${technology.repository}
|
|
357
243
|
Your Working Directory: ${workingDir}
|
|
358
244
|
|
|
359
|
-
Remember that ALL tool calls MUST be executed using absolute path in \`${workingDir}
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
Remember that ALL tool calls MUST be executed using absolute path in \`${workingDir}
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
245
|
+
Remember that ALL tool calls MUST be executed using absolute path in \`${workingDir}\``
|
|
246
|
+
);
|
|
247
|
+
prompt = prompt.replace("</context_block>", "");
|
|
248
|
+
} else {
|
|
249
|
+
prompt = prompt.replace(
|
|
250
|
+
"<context_block>",
|
|
251
|
+
`You have been provided several related repositories to work with grouped in the following working directory: ${workingDir}
|
|
252
|
+
|
|
253
|
+
Remember that ALL tool calls MUST be executed using absolute path in \`${workingDir}\``
|
|
254
|
+
);
|
|
255
|
+
prompt = prompt.replace("</context_block>", "");
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
logger.debug("AGENT", "Dynamic system prompt generated", {
|
|
259
|
+
hasTechnologyContext: !!technology,
|
|
260
|
+
promptLength: prompt.length,
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
return prompt;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
private async createGeminiTempDir(): Promise<string> {
|
|
267
|
+
const tempDir = path.join(os.tmpdir(), `librarian-gemini-${Date.now()}`);
|
|
268
|
+
await mkdir(tempDir, { recursive: true });
|
|
269
|
+
return tempDir;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
private async setupGeminiConfig(
|
|
273
|
+
tempDir: string,
|
|
274
|
+
systemPrompt: string,
|
|
275
|
+
_model: string
|
|
276
|
+
): Promise<{ systemPromptPath: string; settingsPath: string }> {
|
|
277
|
+
const systemPromptPath = path.join(tempDir, "system.md");
|
|
278
|
+
const settingsPath = path.join(tempDir, "settings.json");
|
|
279
|
+
|
|
280
|
+
await Bun.write(systemPromptPath, systemPrompt);
|
|
281
|
+
|
|
282
|
+
const settings = {
|
|
283
|
+
tools: {
|
|
284
|
+
core: ["list_directory", "read_file", "glob", "search_file_content"],
|
|
285
|
+
autoAccept: true,
|
|
286
|
+
},
|
|
287
|
+
mcpServers: {},
|
|
288
|
+
mcp: {
|
|
289
|
+
excluded: ["*"],
|
|
290
|
+
},
|
|
291
|
+
experimental: {
|
|
292
|
+
enableAgents: false,
|
|
293
|
+
},
|
|
294
|
+
output: {
|
|
295
|
+
format: "json",
|
|
296
|
+
},
|
|
297
|
+
};
|
|
298
|
+
await Bun.write(settingsPath, JSON.stringify(settings, null, 2));
|
|
299
|
+
|
|
300
|
+
return { systemPromptPath, settingsPath };
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
private buildGeminiEnv(
|
|
304
|
+
tempDir: string,
|
|
305
|
+
model: string
|
|
306
|
+
): Record<string, string | undefined> {
|
|
307
|
+
const settingsPath = path.join(tempDir, "settings.json");
|
|
308
|
+
const systemPromptPath = path.join(tempDir, "system.md");
|
|
309
|
+
|
|
310
|
+
return {
|
|
311
|
+
...Bun.env,
|
|
312
|
+
GEMINI_SYSTEM_MD: systemPromptPath,
|
|
313
|
+
GEMINI_CLI_SYSTEM_DEFAULTS_PATH: settingsPath,
|
|
314
|
+
GEMINI_CLI_SYSTEM_SETTINGS_PATH: settingsPath,
|
|
315
|
+
GEMINI_MODEL: model,
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
private async cleanupGeminiTempDir(tempDir: string): Promise<void> {
|
|
320
|
+
try {
|
|
321
|
+
await rm(tempDir, { recursive: true, force: true });
|
|
322
|
+
} catch (err) {
|
|
323
|
+
logger.warn("AGENT", "Failed to cleanup Gemini temp files", {
|
|
324
|
+
error: err,
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
private async *streamClaudeCli(
|
|
330
|
+
query: string,
|
|
331
|
+
context?: AgentContext
|
|
332
|
+
): AsyncGenerator<string, void, unknown> {
|
|
333
|
+
const workingDir = context?.workingDir || this.config.workingDir;
|
|
334
|
+
const systemPrompt = this.createDynamicSystemPrompt();
|
|
335
|
+
|
|
336
|
+
const args = [
|
|
337
|
+
"-p",
|
|
338
|
+
query,
|
|
339
|
+
"--system-prompt",
|
|
340
|
+
systemPrompt,
|
|
341
|
+
"--tools",
|
|
342
|
+
"Read,Glob,Grep",
|
|
343
|
+
"--dangerously-skip-permissions",
|
|
344
|
+
"--output-format",
|
|
345
|
+
"stream-json",
|
|
346
|
+
];
|
|
347
|
+
|
|
348
|
+
const env = {
|
|
349
|
+
...Bun.env,
|
|
350
|
+
CLAUDE_PROJECT_DIR: workingDir,
|
|
351
|
+
...(this.config.aiProvider.model && {
|
|
352
|
+
ANTHROPIC_MODEL: this.config.aiProvider.model,
|
|
353
|
+
}),
|
|
354
|
+
};
|
|
355
|
+
|
|
356
|
+
logger.debug("AGENT", "Spawning Claude CLI", {
|
|
357
|
+
args: args.map((a) => (a.length > 100 ? `${a.substring(0, 100)}...` : a)),
|
|
358
|
+
workingDir,
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
const proc = spawn("claude", args, {
|
|
362
|
+
cwd: workingDir,
|
|
363
|
+
env,
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
let buffer = "";
|
|
367
|
+
|
|
368
|
+
if (!proc.stdout) {
|
|
369
|
+
throw new Error("Failed to capture Claude CLI output");
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
const readable = Readable.from(proc.stdout);
|
|
373
|
+
|
|
374
|
+
for await (const chunk of readable) {
|
|
375
|
+
buffer += chunk.toString();
|
|
376
|
+
const lines = buffer.split("\n");
|
|
377
|
+
buffer = lines.pop() || "";
|
|
378
|
+
|
|
379
|
+
for (const line of lines) {
|
|
380
|
+
if (!line.trim()) {
|
|
381
|
+
continue;
|
|
382
|
+
}
|
|
383
|
+
try {
|
|
384
|
+
const data = JSON.parse(line);
|
|
385
|
+
// Filter for text content blocks in the stream
|
|
386
|
+
if (data.type === "text" && data.content) {
|
|
387
|
+
yield data.content;
|
|
388
|
+
} else if (data.type === "content_block_delta" && data.delta?.text) {
|
|
389
|
+
yield data.delta.text;
|
|
390
|
+
} else if (data.type === "message" && Array.isArray(data.content)) {
|
|
391
|
+
// Final message might come as a whole
|
|
392
|
+
for (const block of data.content) {
|
|
393
|
+
if (block.type === "text" && block.text) {
|
|
394
|
+
yield block.text;
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
} catch {
|
|
399
|
+
// Silent fail for non-JSON or partial lines
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// Wait for process to exit
|
|
405
|
+
await new Promise<void>((resolve, reject) => {
|
|
406
|
+
proc.on("exit", (code) => {
|
|
407
|
+
if (code === 0) {
|
|
408
|
+
resolve();
|
|
409
|
+
} else {
|
|
410
|
+
reject(new Error(`Claude CLI exited with code ${code}`));
|
|
411
|
+
}
|
|
412
|
+
});
|
|
413
|
+
proc.on("error", reject);
|
|
414
|
+
});
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
private async *streamGeminiCli(
|
|
418
|
+
query: string,
|
|
419
|
+
context?: AgentContext
|
|
420
|
+
): AsyncGenerator<string, void, unknown> {
|
|
421
|
+
const workingDir = context?.workingDir || this.config.workingDir;
|
|
422
|
+
const systemPrompt = this.createDynamicSystemPrompt();
|
|
423
|
+
|
|
424
|
+
const tempDir = await this.createGeminiTempDir();
|
|
425
|
+
const model = this.config.aiProvider.model || "gemini-2.5-flash";
|
|
426
|
+
|
|
427
|
+
try {
|
|
428
|
+
await this.setupGeminiConfig(tempDir, systemPrompt, model);
|
|
429
|
+
|
|
430
|
+
const args = [
|
|
431
|
+
"gemini",
|
|
432
|
+
"-p",
|
|
433
|
+
query,
|
|
434
|
+
"--output-format",
|
|
435
|
+
"stream-json",
|
|
436
|
+
"--yolo",
|
|
437
|
+
];
|
|
438
|
+
|
|
439
|
+
const env = this.buildGeminiEnv(tempDir, model);
|
|
440
|
+
|
|
441
|
+
logger.debug("AGENT", "Spawning Gemini CLI", {
|
|
442
|
+
args,
|
|
443
|
+
workingDir,
|
|
444
|
+
model,
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
const proc = Bun.spawn(args, {
|
|
448
|
+
cwd: workingDir,
|
|
449
|
+
env,
|
|
450
|
+
stdout: "pipe",
|
|
451
|
+
stderr: "pipe",
|
|
452
|
+
});
|
|
453
|
+
|
|
454
|
+
const reader = proc.stdout.getReader();
|
|
455
|
+
let buffer = "";
|
|
456
|
+
|
|
457
|
+
while (true) {
|
|
458
|
+
const { done, value } = await reader.read();
|
|
459
|
+
if (done) {
|
|
460
|
+
break;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
buffer += new TextDecoder().decode(value);
|
|
464
|
+
const lines = buffer.split("\n");
|
|
465
|
+
buffer = lines.pop() || "";
|
|
466
|
+
|
|
467
|
+
for (const line of lines) {
|
|
468
|
+
if (!line.trim()) {
|
|
469
|
+
continue;
|
|
470
|
+
}
|
|
471
|
+
try {
|
|
472
|
+
const data = JSON.parse(line);
|
|
473
|
+
const text = this.parseGeminiStreamLine(data);
|
|
474
|
+
if (text) {
|
|
475
|
+
yield text;
|
|
476
|
+
}
|
|
477
|
+
} catch {
|
|
478
|
+
// Silent fail for non-JSON or partial lines
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
const exitCode = await proc.exited;
|
|
484
|
+
if (exitCode !== 0) {
|
|
485
|
+
throw new Error(`Gemini CLI exited with code ${exitCode}`);
|
|
486
|
+
}
|
|
487
|
+
} finally {
|
|
488
|
+
await this.cleanupGeminiTempDir(tempDir);
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
private parseGeminiStreamLine(data: unknown): string | null {
|
|
493
|
+
if (
|
|
494
|
+
data &&
|
|
495
|
+
typeof data === "object" &&
|
|
496
|
+
"type" in data &&
|
|
497
|
+
"role" in data &&
|
|
498
|
+
"content" in data
|
|
499
|
+
) {
|
|
500
|
+
const typedData = data as { type: string; role: string; content: string };
|
|
501
|
+
if (
|
|
502
|
+
typedData.type === "message" &&
|
|
503
|
+
typedData.role === "assistant" &&
|
|
504
|
+
typedData.content
|
|
505
|
+
) {
|
|
506
|
+
return typedData.content;
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
return null;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
private createAIModel(
|
|
513
|
+
aiProvider: ReactAgentConfig["aiProvider"]
|
|
514
|
+
): ChatOpenAI | ChatAnthropic | ChatGoogleGenerativeAI {
|
|
515
|
+
const { type, apiKey, model, baseURL } = aiProvider;
|
|
516
|
+
|
|
517
|
+
logger.debug("AGENT", "Creating AI model instance", {
|
|
518
|
+
type,
|
|
519
|
+
model,
|
|
520
|
+
hasBaseURL: !!baseURL,
|
|
521
|
+
});
|
|
522
|
+
|
|
523
|
+
switch (type) {
|
|
524
|
+
case "openai":
|
|
525
|
+
return new ChatOpenAI({
|
|
526
|
+
apiKey,
|
|
527
|
+
modelName: model || "gpt-5.2",
|
|
528
|
+
});
|
|
529
|
+
case "openai-compatible":
|
|
530
|
+
return new ChatOpenAI({
|
|
531
|
+
apiKey,
|
|
532
|
+
modelName: model || "gpt-5.2",
|
|
533
|
+
configuration: {
|
|
534
|
+
baseURL: baseURL || "https://api.openai.com/v1",
|
|
535
|
+
},
|
|
536
|
+
});
|
|
537
|
+
case "anthropic":
|
|
538
|
+
return new ChatAnthropic({
|
|
539
|
+
apiKey,
|
|
540
|
+
modelName: model || "claude-sonnet-4-5",
|
|
541
|
+
});
|
|
542
|
+
case "anthropic-compatible":
|
|
543
|
+
if (!baseURL) {
|
|
544
|
+
throw new Error(
|
|
545
|
+
"baseURL is required for anthropic-compatible provider"
|
|
546
|
+
);
|
|
547
|
+
}
|
|
548
|
+
if (!model) {
|
|
549
|
+
throw new Error(
|
|
550
|
+
"model is required for anthropic-compatible provider"
|
|
551
|
+
);
|
|
552
|
+
}
|
|
553
|
+
return new ChatAnthropic({
|
|
554
|
+
apiKey,
|
|
555
|
+
modelName: model,
|
|
556
|
+
anthropicApiUrl: baseURL,
|
|
557
|
+
});
|
|
558
|
+
case "google":
|
|
559
|
+
return new ChatGoogleGenerativeAI({
|
|
560
|
+
apiKey,
|
|
561
|
+
model: model || "gemini-3-flash-preview",
|
|
562
|
+
});
|
|
563
|
+
default:
|
|
564
|
+
logger.error(
|
|
565
|
+
"AGENT",
|
|
566
|
+
"Unsupported AI provider type",
|
|
567
|
+
new Error(`Unsupported AI provider type: ${type}`),
|
|
568
|
+
{ type }
|
|
569
|
+
);
|
|
570
|
+
throw new Error(`Unsupported AI provider type: ${type}`);
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
initialize(): Promise<void> {
|
|
575
|
+
if (
|
|
576
|
+
this.config.aiProvider.type === "claude-code" ||
|
|
577
|
+
this.config.aiProvider.type === "gemini-cli"
|
|
578
|
+
) {
|
|
579
|
+
logger.info(
|
|
580
|
+
"AGENT",
|
|
581
|
+
`${this.config.aiProvider.type} CLI mode initialized (skipping LangChain setup)`
|
|
582
|
+
);
|
|
583
|
+
return Promise.resolve();
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
if (!this.aiModel) {
|
|
587
|
+
throw new Error("AI model not created for non-CLI provider");
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
// Create the agent using LangChain's createAgent function with dynamic system prompt
|
|
591
|
+
this.agent = createAgent({
|
|
592
|
+
model: this.aiModel,
|
|
593
|
+
tools: this.tools,
|
|
594
|
+
systemPrompt: this.createDynamicSystemPrompt(),
|
|
595
|
+
middleware: [
|
|
596
|
+
todoListMiddleware(),
|
|
597
|
+
...(this.config.aiProvider.type === "anthropic" ||
|
|
598
|
+
this.config.aiProvider.type === "anthropic-compatible"
|
|
599
|
+
? [anthropicPromptCachingMiddleware()]
|
|
600
|
+
: []),
|
|
601
|
+
],
|
|
602
|
+
});
|
|
603
|
+
|
|
604
|
+
logger.info("AGENT", "Agent initialized successfully", {
|
|
605
|
+
toolCount: this.tools.length,
|
|
606
|
+
hasContextSchema: !!this.contextSchema,
|
|
607
|
+
});
|
|
608
|
+
|
|
609
|
+
return Promise.resolve();
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
/**
|
|
613
|
+
* Query repository with a given query and optional context
|
|
614
|
+
*
|
|
615
|
+
* @param repoPath - The repository path (deprecated, for compatibility)
|
|
616
|
+
* @param query - The query string
|
|
617
|
+
* @param context - Optional context object containing working directory and metadata
|
|
618
|
+
* @returns The agent's response as a string
|
|
619
|
+
*/
|
|
620
|
+
async queryRepository(
|
|
621
|
+
_repoPath: string,
|
|
622
|
+
query: string,
|
|
623
|
+
context?: AgentContext
|
|
624
|
+
): Promise<string> {
|
|
625
|
+
logger.info("AGENT", "Query started", {
|
|
626
|
+
queryLength: query.length,
|
|
627
|
+
hasContext: !!context,
|
|
628
|
+
});
|
|
629
|
+
|
|
630
|
+
if (this.config.aiProvider.type === "claude-code") {
|
|
631
|
+
let fullContent = "";
|
|
632
|
+
for await (const chunk of this.streamClaudeCli(query, context)) {
|
|
633
|
+
fullContent += chunk;
|
|
634
|
+
}
|
|
635
|
+
return fullContent;
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
if (this.config.aiProvider.type === "gemini-cli") {
|
|
639
|
+
let fullContent = "";
|
|
640
|
+
for await (const chunk of this.streamGeminiCli(query, context)) {
|
|
641
|
+
fullContent += chunk;
|
|
642
|
+
}
|
|
643
|
+
return fullContent;
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
const timingId = logger.timingStart("agentQuery");
|
|
647
|
+
|
|
648
|
+
if (!this.agent) {
|
|
649
|
+
logger.error(
|
|
650
|
+
"AGENT",
|
|
651
|
+
"Agent not initialized",
|
|
652
|
+
new Error("Agent not initialized. Call initialize() first.")
|
|
653
|
+
);
|
|
654
|
+
throw new Error("Agent not initialized. Call initialize() first.");
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
// Prepare the messages for the agent - system prompt already set during initialization
|
|
658
|
+
const messages = [new HumanMessage(query)];
|
|
659
|
+
|
|
660
|
+
logger.debug("AGENT", "Invoking agent with messages", {
|
|
661
|
+
messageCount: messages.length,
|
|
662
|
+
hasContext: !!context,
|
|
663
|
+
});
|
|
664
|
+
|
|
665
|
+
// Execute the agent with optional context
|
|
666
|
+
const result = await this.agent.invoke(
|
|
667
|
+
{
|
|
668
|
+
messages,
|
|
669
|
+
},
|
|
670
|
+
context ? { context, recursionLimit: 100 } : { recursionLimit: 100 }
|
|
671
|
+
);
|
|
672
|
+
|
|
673
|
+
// Extract the last message content from the state
|
|
674
|
+
const lastMessage = result.messages.at(-1);
|
|
675
|
+
const content =
|
|
676
|
+
typeof lastMessage.content === "string"
|
|
677
|
+
? lastMessage.content
|
|
678
|
+
: JSON.stringify(lastMessage.content);
|
|
679
|
+
|
|
680
|
+
logger.timingEnd(timingId, "AGENT", "Query completed");
|
|
681
|
+
logger.info("AGENT", "Query result received", {
|
|
682
|
+
responseLength: content.length,
|
|
683
|
+
});
|
|
684
|
+
|
|
685
|
+
return content;
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
/**
|
|
689
|
+
* Stream repository query with optional context
|
|
690
|
+
*
|
|
691
|
+
* @param repoPath - The repository path (deprecated, for compatibility)
|
|
692
|
+
* @param query - The query string
|
|
693
|
+
* @param context - Optional context object containing working directory and metadata
|
|
694
|
+
* @returns Async generator yielding string chunks
|
|
695
|
+
*/
|
|
696
|
+
async *streamRepository(
|
|
697
|
+
_repoPath: string,
|
|
698
|
+
query: string,
|
|
699
|
+
context?: AgentContext
|
|
700
|
+
): AsyncGenerator<string, void, unknown> {
|
|
701
|
+
logger.info("AGENT", "Stream started", {
|
|
702
|
+
queryLength: query.length,
|
|
703
|
+
hasContext: !!context,
|
|
704
|
+
});
|
|
705
|
+
|
|
706
|
+
if (this.config.aiProvider.type === "claude-code") {
|
|
707
|
+
yield* this.streamClaudeCli(query, context);
|
|
708
|
+
return;
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
if (this.config.aiProvider.type === "gemini-cli") {
|
|
712
|
+
yield* this.streamGeminiCli(query, context);
|
|
713
|
+
return;
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
const timingId = logger.timingStart("agentStream");
|
|
717
|
+
|
|
718
|
+
if (!this.agent) {
|
|
719
|
+
logger.error(
|
|
720
|
+
"AGENT",
|
|
721
|
+
"Agent not initialized",
|
|
722
|
+
new Error("Agent not initialized. Call initialize() first.")
|
|
723
|
+
);
|
|
724
|
+
throw new Error("Agent not initialized. Call initialize() first.");
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
const messages = [new HumanMessage(query)];
|
|
728
|
+
|
|
729
|
+
logger.debug("AGENT", "Invoking agent stream with messages", {
|
|
730
|
+
messageCount: messages.length,
|
|
731
|
+
hasContext: !!context,
|
|
732
|
+
});
|
|
733
|
+
|
|
734
|
+
const cleanup = () => {
|
|
735
|
+
// Signal interruption for potential future use
|
|
736
|
+
};
|
|
737
|
+
|
|
738
|
+
logger.debug("AGENT", "Setting up interruption handlers for streaming");
|
|
739
|
+
process.on("SIGINT", cleanup);
|
|
740
|
+
process.on("SIGTERM", cleanup);
|
|
741
|
+
|
|
742
|
+
try {
|
|
743
|
+
const result = await this.agent.invoke(
|
|
744
|
+
{ messages },
|
|
745
|
+
context ? { context, recursionLimit: 100 } : { recursionLimit: 100 }
|
|
746
|
+
);
|
|
747
|
+
|
|
748
|
+
const content = extractMessageContent(result);
|
|
749
|
+
if (content) {
|
|
750
|
+
yield content;
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
yield "\n";
|
|
754
|
+
} catch (error) {
|
|
755
|
+
const errorMessage = getStreamingErrorMessage(error);
|
|
756
|
+
logger.error(
|
|
757
|
+
"AGENT",
|
|
758
|
+
"Streaming error",
|
|
759
|
+
error instanceof Error ? error : new Error(errorMessage)
|
|
760
|
+
);
|
|
761
|
+
yield `\n\n[Error: ${errorMessage}]`;
|
|
762
|
+
throw error;
|
|
763
|
+
} finally {
|
|
764
|
+
process.removeListener("SIGINT", cleanup);
|
|
765
|
+
process.removeListener("SIGTERM", cleanup);
|
|
766
|
+
logger.timingEnd(timingId, "AGENT", "Streaming completed");
|
|
767
|
+
}
|
|
768
|
+
}
|
|
867
769
|
}
|
|
868
770
|
|
|
869
771
|
/**
|
|
870
772
|
* Extract content from the last message in the result
|
|
871
773
|
*/
|
|
872
|
-
function extractMessageContent(result: {
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
774
|
+
function extractMessageContent(result: {
|
|
775
|
+
messages?: Array<{ content: unknown }>;
|
|
776
|
+
}): string | null {
|
|
777
|
+
if (!result.messages || result.messages.length === 0) {
|
|
778
|
+
return null;
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
const lastMessage = result.messages.at(-1);
|
|
782
|
+
if (!lastMessage?.content) {
|
|
783
|
+
return null;
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
const content = lastMessage.content;
|
|
787
|
+
if (typeof content === "string") {
|
|
788
|
+
return content;
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
if (Array.isArray(content)) {
|
|
792
|
+
const parts: string[] = [];
|
|
793
|
+
for (const block of content) {
|
|
794
|
+
if (block && typeof block === "object") {
|
|
795
|
+
const blockObj = block as { type?: string; text?: unknown };
|
|
796
|
+
if (blockObj.type === "text" && typeof blockObj.text === "string") {
|
|
797
|
+
parts.push(blockObj.text);
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
return parts.length > 0 ? parts.join("") : null;
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
return null;
|
|
901
805
|
}
|
|
902
806
|
|
|
903
807
|
/**
|
|
904
808
|
* Get user-friendly error message for streaming errors
|
|
905
809
|
*/
|
|
906
810
|
function getStreamingErrorMessage(error: unknown): string {
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
811
|
+
if (!(error instanceof Error)) {
|
|
812
|
+
return "Unknown streaming error";
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
if (error.message.includes("timeout")) {
|
|
816
|
+
return "Streaming timeout - request took too long to complete";
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
if (
|
|
820
|
+
error.message.includes("network") ||
|
|
821
|
+
error.message.includes("ENOTFOUND")
|
|
822
|
+
) {
|
|
823
|
+
return "Network error - unable to connect to AI provider";
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
if (error.message.includes("rate limit")) {
|
|
827
|
+
return "Rate limit exceeded - please try again later";
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
if (
|
|
831
|
+
error.message.includes("authentication") ||
|
|
832
|
+
error.message.includes("unauthorized")
|
|
833
|
+
) {
|
|
834
|
+
return "Authentication error - check your API credentials";
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
return `Streaming error: ${error.message}`;
|
|
928
838
|
}
|