ctxpkg 0.0.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/LICENSE +661 -0
- package/README.md +282 -0
- package/bin/cli.js +8 -0
- package/bin/daemon.js +7 -0
- package/package.json +70 -0
- package/src/agent/AGENTS.md +249 -0
- package/src/agent/agent.prompts.ts +66 -0
- package/src/agent/agent.test-runner.schemas.ts +158 -0
- package/src/agent/agent.test-runner.ts +436 -0
- package/src/agent/agent.ts +371 -0
- package/src/agent/agent.types.ts +94 -0
- package/src/backend/AGENTS.md +112 -0
- package/src/backend/backend.protocol.ts +95 -0
- package/src/backend/backend.schemas.ts +123 -0
- package/src/backend/backend.services.ts +151 -0
- package/src/backend/backend.ts +111 -0
- package/src/backend/backend.types.ts +34 -0
- package/src/cli/AGENTS.md +213 -0
- package/src/cli/cli.agent.ts +197 -0
- package/src/cli/cli.chat.ts +369 -0
- package/src/cli/cli.client.ts +55 -0
- package/src/cli/cli.collections.ts +491 -0
- package/src/cli/cli.config.ts +252 -0
- package/src/cli/cli.daemon.ts +160 -0
- package/src/cli/cli.documents.ts +413 -0
- package/src/cli/cli.mcp.ts +177 -0
- package/src/cli/cli.ts +28 -0
- package/src/cli/cli.utils.ts +122 -0
- package/src/client/AGENTS.md +135 -0
- package/src/client/client.adapters.ts +279 -0
- package/src/client/client.ts +86 -0
- package/src/client/client.types.ts +17 -0
- package/src/collections/AGENTS.md +185 -0
- package/src/collections/collections.schemas.ts +195 -0
- package/src/collections/collections.ts +1160 -0
- package/src/config/config.ts +118 -0
- package/src/daemon/AGENTS.md +168 -0
- package/src/daemon/daemon.config.ts +23 -0
- package/src/daemon/daemon.manager.ts +215 -0
- package/src/daemon/daemon.schemas.ts +22 -0
- package/src/daemon/daemon.ts +205 -0
- package/src/database/AGENTS.md +211 -0
- package/src/database/database.ts +64 -0
- package/src/database/migrations/migrations.001-init.ts +56 -0
- package/src/database/migrations/migrations.002-fts5.ts +32 -0
- package/src/database/migrations/migrations.ts +20 -0
- package/src/database/migrations/migrations.types.ts +9 -0
- package/src/documents/AGENTS.md +301 -0
- package/src/documents/documents.schemas.ts +190 -0
- package/src/documents/documents.ts +734 -0
- package/src/embedder/embedder.ts +53 -0
- package/src/exports.ts +0 -0
- package/src/mcp/AGENTS.md +264 -0
- package/src/mcp/mcp.ts +105 -0
- package/src/tools/AGENTS.md +228 -0
- package/src/tools/agent/agent.ts +45 -0
- package/src/tools/documents/documents.ts +401 -0
- package/src/tools/tools.langchain.ts +37 -0
- package/src/tools/tools.mcp.ts +46 -0
- package/src/tools/tools.types.ts +35 -0
- package/src/utils/utils.services.ts +46 -0
|
@@ -0,0 +1,371 @@
|
|
|
1
|
+
import { AIMessage, BaseMessage, HumanMessage, SystemMessage, ToolMessage } from '@langchain/core/messages';
|
|
2
|
+
import { createReactAgent } from '@langchain/langgraph/prebuilt';
|
|
3
|
+
import { ChatOpenAI } from '@langchain/openai';
|
|
4
|
+
|
|
5
|
+
import type {
|
|
6
|
+
AgentResponse,
|
|
7
|
+
AgentStep,
|
|
8
|
+
AgentStepCallback,
|
|
9
|
+
AskOptions,
|
|
10
|
+
DocumentAgentOptions,
|
|
11
|
+
LLMConfig,
|
|
12
|
+
RetryConfig,
|
|
13
|
+
} from './agent.types.ts';
|
|
14
|
+
import { AGENT_SYSTEM_PROMPT, formatCollectionRestriction, formatUserPrompt } from './agent.prompts.ts';
|
|
15
|
+
|
|
16
|
+
import type { BackendClient } from '#root/client/client.ts';
|
|
17
|
+
import { createDocumentToolDefinitions } from '#root/tools/documents/documents.ts';
|
|
18
|
+
import { toLangchainTools } from '#root/tools/tools.langchain.ts';
|
|
19
|
+
|
|
20
|
+
/** Default retry configuration */
|
|
21
|
+
const DEFAULT_RETRY_CONFIG: RetryConfig = {
|
|
22
|
+
maxRetries: 3,
|
|
23
|
+
initialDelayMs: 1000,
|
|
24
|
+
maxDelayMs: 30000,
|
|
25
|
+
backoffMultiplier: 2,
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Sleep for a given number of milliseconds
|
|
30
|
+
*/
|
|
31
|
+
const sleep = (ms: number): Promise<void> => new Promise((resolve) => setTimeout(resolve, ms));
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Check if an error is retryable (rate limit, temporary failure, etc.)
|
|
35
|
+
*/
|
|
36
|
+
const isRetryableError = (error: unknown): boolean => {
|
|
37
|
+
if (error instanceof Error) {
|
|
38
|
+
const message = error.message.toLowerCase();
|
|
39
|
+
// Rate limit errors
|
|
40
|
+
if (message.includes('rate limit') || message.includes('429') || message.includes('too many requests')) {
|
|
41
|
+
return true;
|
|
42
|
+
}
|
|
43
|
+
// Temporary server errors
|
|
44
|
+
if (message.includes('500') || message.includes('502') || message.includes('503') || message.includes('504')) {
|
|
45
|
+
return true;
|
|
46
|
+
}
|
|
47
|
+
// Network errors
|
|
48
|
+
if (message.includes('econnreset') || message.includes('etimedout') || message.includes('network')) {
|
|
49
|
+
return true;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return false;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Execute a function with retry logic
|
|
57
|
+
*/
|
|
58
|
+
const withRetry = async <T>(
|
|
59
|
+
fn: () => Promise<T>,
|
|
60
|
+
config: RetryConfig = DEFAULT_RETRY_CONFIG,
|
|
61
|
+
onRetry?: (attempt: number, error: Error, delayMs: number) => void,
|
|
62
|
+
): Promise<T> => {
|
|
63
|
+
let lastError: Error | undefined;
|
|
64
|
+
let delayMs = config.initialDelayMs;
|
|
65
|
+
|
|
66
|
+
for (let attempt = 1; attempt <= config.maxRetries + 1; attempt++) {
|
|
67
|
+
try {
|
|
68
|
+
return await fn();
|
|
69
|
+
} catch (error) {
|
|
70
|
+
lastError = error instanceof Error ? error : new Error(String(error));
|
|
71
|
+
|
|
72
|
+
// Don't retry non-retryable errors or on last attempt
|
|
73
|
+
if (!isRetryableError(error) || attempt > config.maxRetries) {
|
|
74
|
+
throw lastError;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Notify about retry
|
|
78
|
+
if (onRetry) {
|
|
79
|
+
onRetry(attempt, lastError, delayMs);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Wait before retrying
|
|
83
|
+
await sleep(delayMs);
|
|
84
|
+
|
|
85
|
+
// Exponential backoff
|
|
86
|
+
delayMs = Math.min(delayMs * config.backoffMultiplier, config.maxDelayMs);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
throw lastError;
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Document search agent that uses LangChain tools to find and synthesize information.
|
|
95
|
+
*/
|
|
96
|
+
class DocumentAgent {
|
|
97
|
+
#agent: ReturnType<typeof createReactAgent>;
|
|
98
|
+
#maxIterations: number;
|
|
99
|
+
#onStep?: AgentStepCallback;
|
|
100
|
+
#collections?: string[];
|
|
101
|
+
#conversationHistory: BaseMessage[];
|
|
102
|
+
#systemPrompt: string;
|
|
103
|
+
|
|
104
|
+
constructor(options: DocumentAgentOptions) {
|
|
105
|
+
const { llmConfig, tools, maxIterations = 15, onStep, collections } = options;
|
|
106
|
+
|
|
107
|
+
const llm = new ChatOpenAI({
|
|
108
|
+
configuration: {
|
|
109
|
+
baseURL: llmConfig.provider,
|
|
110
|
+
},
|
|
111
|
+
modelName: llmConfig.model,
|
|
112
|
+
apiKey: llmConfig.apiKey,
|
|
113
|
+
temperature: llmConfig.temperature,
|
|
114
|
+
maxTokens: llmConfig.maxTokens,
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
this.#agent = createReactAgent({
|
|
118
|
+
llm,
|
|
119
|
+
tools,
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
this.#maxIterations = maxIterations;
|
|
123
|
+
this.#onStep = onStep;
|
|
124
|
+
this.#collections = collections;
|
|
125
|
+
this.#conversationHistory = [];
|
|
126
|
+
|
|
127
|
+
// Build system prompt with collection restriction if needed
|
|
128
|
+
this.#systemPrompt = AGENT_SYSTEM_PROMPT;
|
|
129
|
+
if (collections && collections.length > 0) {
|
|
130
|
+
this.#systemPrompt += formatCollectionRestriction(collections);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Set the step callback for verbose mode
|
|
136
|
+
*/
|
|
137
|
+
setOnStep(callback: AgentStepCallback | undefined): void {
|
|
138
|
+
this.#onStep = callback;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Clear conversation history for a fresh start
|
|
143
|
+
*/
|
|
144
|
+
clearHistory(): void {
|
|
145
|
+
this.#conversationHistory = [];
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Get current conversation history length
|
|
150
|
+
*/
|
|
151
|
+
getHistoryLength(): number {
|
|
152
|
+
return this.#conversationHistory.length;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Ask a question and get a synthesized answer (stateless - doesn't use conversation history).
|
|
157
|
+
*/
|
|
158
|
+
async ask(query: string, useCase: string, options?: AskOptions): Promise<AgentResponse> {
|
|
159
|
+
const onStep = options?.onStep ?? this.#onStep;
|
|
160
|
+
const userPrompt = formatUserPrompt(query, useCase, this.#collections);
|
|
161
|
+
|
|
162
|
+
const messages: BaseMessage[] = [new SystemMessage(this.#systemPrompt), new HumanMessage(userPrompt)];
|
|
163
|
+
|
|
164
|
+
return this.#runAgent(messages, onStep);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Chat with conversation history (stateful - maintains context across calls).
|
|
169
|
+
*/
|
|
170
|
+
async chat(message: string, useCase: string, options?: AskOptions): Promise<AgentResponse> {
|
|
171
|
+
const onStep = options?.onStep ?? this.#onStep;
|
|
172
|
+
|
|
173
|
+
// Add user message to history
|
|
174
|
+
const userMessage = new HumanMessage(formatUserPrompt(message, useCase, this.#collections));
|
|
175
|
+
this.#conversationHistory.push(userMessage);
|
|
176
|
+
|
|
177
|
+
// Build full message list with system prompt
|
|
178
|
+
const messages: BaseMessage[] = [new SystemMessage(this.#systemPrompt), ...this.#conversationHistory];
|
|
179
|
+
|
|
180
|
+
const response = await this.#runAgent(messages, onStep);
|
|
181
|
+
|
|
182
|
+
// Add assistant response to history
|
|
183
|
+
this.#conversationHistory.push(new AIMessage(JSON.stringify(response)));
|
|
184
|
+
|
|
185
|
+
return response;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Run the agent with retry logic and step callbacks
|
|
190
|
+
*/
|
|
191
|
+
async #runAgent(messages: BaseMessage[], onStep?: AgentStepCallback): Promise<AgentResponse> {
|
|
192
|
+
// Notify about starting
|
|
193
|
+
if (onStep) {
|
|
194
|
+
onStep({ type: 'thinking', content: 'Starting search...' });
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const result = await withRetry(
|
|
198
|
+
async () => {
|
|
199
|
+
return this.#agent.invoke(
|
|
200
|
+
{ messages },
|
|
201
|
+
{
|
|
202
|
+
recursionLimit: this.#maxIterations,
|
|
203
|
+
},
|
|
204
|
+
);
|
|
205
|
+
},
|
|
206
|
+
DEFAULT_RETRY_CONFIG,
|
|
207
|
+
(attempt, error, delayMs) => {
|
|
208
|
+
if (onStep) {
|
|
209
|
+
onStep({
|
|
210
|
+
type: 'error',
|
|
211
|
+
content: `Retry attempt ${attempt} after error: ${error.message}. Waiting ${delayMs}ms...`,
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
},
|
|
215
|
+
);
|
|
216
|
+
|
|
217
|
+
// Process messages for verbose output
|
|
218
|
+
if (onStep) {
|
|
219
|
+
this.#processMessagesForVerbose(result.messages, onStep);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Extract the final message content
|
|
223
|
+
const resultMessages = result.messages;
|
|
224
|
+
const lastMessage = resultMessages[resultMessages.length - 1];
|
|
225
|
+
const content = typeof lastMessage.content === 'string' ? lastMessage.content : JSON.stringify(lastMessage.content);
|
|
226
|
+
|
|
227
|
+
// Try to parse as JSON response
|
|
228
|
+
return this.#parseResponse(content);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Process agent messages and emit verbose step callbacks
|
|
233
|
+
*/
|
|
234
|
+
#processMessagesForVerbose(messages: BaseMessage[], onStep: AgentStepCallback): void {
|
|
235
|
+
for (const message of messages) {
|
|
236
|
+
if (message instanceof AIMessage) {
|
|
237
|
+
// Check for tool calls
|
|
238
|
+
const toolCalls = message.tool_calls;
|
|
239
|
+
if (toolCalls && toolCalls.length > 0) {
|
|
240
|
+
for (const toolCall of toolCalls) {
|
|
241
|
+
const step: AgentStep = {
|
|
242
|
+
type: 'tool_call',
|
|
243
|
+
content: `Calling ${toolCall.name}`,
|
|
244
|
+
toolName: toolCall.name,
|
|
245
|
+
toolInput: toolCall.args as Record<string, unknown>,
|
|
246
|
+
};
|
|
247
|
+
onStep(step);
|
|
248
|
+
}
|
|
249
|
+
} else if (message.content) {
|
|
250
|
+
// Regular AI message (thinking or final answer)
|
|
251
|
+
const content = typeof message.content === 'string' ? message.content : JSON.stringify(message.content);
|
|
252
|
+
if (content.trim()) {
|
|
253
|
+
onStep({ type: 'thinking', content: content.slice(0, 200) + (content.length > 200 ? '...' : '') });
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
} else if (message instanceof ToolMessage) {
|
|
257
|
+
// Tool result
|
|
258
|
+
const content = typeof message.content === 'string' ? message.content : JSON.stringify(message.content);
|
|
259
|
+
const preview = content.slice(0, 150) + (content.length > 150 ? '...' : '');
|
|
260
|
+
onStep({
|
|
261
|
+
type: 'tool_result',
|
|
262
|
+
content: preview,
|
|
263
|
+
toolName: message.name,
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Parse the agent's response, extracting JSON if present.
|
|
271
|
+
*/
|
|
272
|
+
#parseResponse(content: string): AgentResponse {
|
|
273
|
+
// Try to find JSON in the response
|
|
274
|
+
const jsonMatch = content.match(/```json\s*([\s\S]*?)\s*```/);
|
|
275
|
+
if (jsonMatch) {
|
|
276
|
+
try {
|
|
277
|
+
const parsed = JSON.parse(jsonMatch[1]);
|
|
278
|
+
return {
|
|
279
|
+
answer: parsed.answer ?? content,
|
|
280
|
+
sources: parsed.sources ?? [],
|
|
281
|
+
confidence: parsed.confidence ?? 'medium',
|
|
282
|
+
note: parsed.note,
|
|
283
|
+
};
|
|
284
|
+
} catch {
|
|
285
|
+
// Fall through to default
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// Try to parse the whole content as JSON
|
|
290
|
+
try {
|
|
291
|
+
const parsed = JSON.parse(content);
|
|
292
|
+
if (parsed.answer) {
|
|
293
|
+
return {
|
|
294
|
+
answer: parsed.answer,
|
|
295
|
+
sources: parsed.sources ?? [],
|
|
296
|
+
confidence: parsed.confidence ?? 'medium',
|
|
297
|
+
note: parsed.note,
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
} catch {
|
|
301
|
+
// Fall through to default
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// Default: treat the whole content as the answer
|
|
305
|
+
return {
|
|
306
|
+
answer: content,
|
|
307
|
+
sources: [],
|
|
308
|
+
confidence: 'medium',
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Options for creating a document agent
|
|
315
|
+
*/
|
|
316
|
+
type CreateDocumentAgentOptions = {
|
|
317
|
+
/** Backend client for API calls */
|
|
318
|
+
client: BackendClient;
|
|
319
|
+
/** LLM configuration */
|
|
320
|
+
llmConfig: LLMConfig;
|
|
321
|
+
/** Optional map of alias names to collection IDs */
|
|
322
|
+
aliasMap?: Map<string, string>;
|
|
323
|
+
/** Maximum agent iterations */
|
|
324
|
+
maxIterations?: number;
|
|
325
|
+
/** Callback for verbose mode */
|
|
326
|
+
onStep?: AgentStepCallback;
|
|
327
|
+
/** Collections to restrict searches to */
|
|
328
|
+
collections?: string[];
|
|
329
|
+
};
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* Create a document search agent.
|
|
333
|
+
*/
|
|
334
|
+
const createDocumentAgent = (options: CreateDocumentAgentOptions): DocumentAgent => {
|
|
335
|
+
const { client, llmConfig, aliasMap, maxIterations, onStep, collections } = options;
|
|
336
|
+
|
|
337
|
+
// Create document tool definitions and convert to LangChain tools
|
|
338
|
+
const toolDefinitions = createDocumentToolDefinitions({ client, aliasMap });
|
|
339
|
+
const langchainTools = toLangchainTools(toolDefinitions);
|
|
340
|
+
const tools = Object.values(langchainTools);
|
|
341
|
+
|
|
342
|
+
return new DocumentAgent({
|
|
343
|
+
llmConfig,
|
|
344
|
+
tools,
|
|
345
|
+
maxIterations,
|
|
346
|
+
onStep,
|
|
347
|
+
collections,
|
|
348
|
+
});
|
|
349
|
+
};
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* Get LLM config from the application config.
|
|
353
|
+
*/
|
|
354
|
+
const getLLMConfigFromAppConfig = async (): Promise<LLMConfig> => {
|
|
355
|
+
const { config } = await import('#root/config/config.ts');
|
|
356
|
+
|
|
357
|
+
// Use type assertion for dynamic config access
|
|
358
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
359
|
+
const c = config as any;
|
|
360
|
+
|
|
361
|
+
return {
|
|
362
|
+
provider: c.get('llm.provider'),
|
|
363
|
+
model: c.get('llm.model'),
|
|
364
|
+
apiKey: c.get('llm.apiKey'),
|
|
365
|
+
temperature: c.get('llm.temperature'),
|
|
366
|
+
maxTokens: c.get('llm.maxTokens'),
|
|
367
|
+
};
|
|
368
|
+
};
|
|
369
|
+
|
|
370
|
+
export { DocumentAgent, createDocumentAgent, getLLMConfigFromAppConfig, withRetry, isRetryableError };
|
|
371
|
+
export type { CreateDocumentAgentOptions };
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import * as z from 'zod';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* LLM configuration for the agent
|
|
5
|
+
*/
|
|
6
|
+
export type LLMConfig = {
|
|
7
|
+
/** OpenAI-compatible API base URL */
|
|
8
|
+
provider: string;
|
|
9
|
+
/** Model identifier */
|
|
10
|
+
model: string;
|
|
11
|
+
/** API key */
|
|
12
|
+
apiKey: string;
|
|
13
|
+
/** Temperature (0-2) */
|
|
14
|
+
temperature: number;
|
|
15
|
+
/** Maximum tokens */
|
|
16
|
+
maxTokens: number;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Source reference for an answer
|
|
21
|
+
*/
|
|
22
|
+
export const sourceSchema = z.object({
|
|
23
|
+
collection: z.string(),
|
|
24
|
+
document: z.string(),
|
|
25
|
+
section: z.string().optional(),
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
export type Source = z.infer<typeof sourceSchema>;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Agent response
|
|
32
|
+
*/
|
|
33
|
+
export const agentResponseSchema = z.object({
|
|
34
|
+
answer: z.string(),
|
|
35
|
+
sources: z.array(sourceSchema),
|
|
36
|
+
confidence: z.enum(['high', 'medium', 'low']),
|
|
37
|
+
note: z.string().optional(),
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
export type AgentResponse = z.infer<typeof agentResponseSchema>;
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Callback for verbose mode - called when agent takes a step
|
|
44
|
+
*/
|
|
45
|
+
export type AgentStepCallback = (step: AgentStep) => void;
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Represents a step in the agent's reasoning
|
|
49
|
+
*/
|
|
50
|
+
export type AgentStep = {
|
|
51
|
+
type: 'thinking' | 'tool_call' | 'tool_result' | 'error';
|
|
52
|
+
content: string;
|
|
53
|
+
toolName?: string;
|
|
54
|
+
toolInput?: Record<string, unknown>;
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Options for creating a document agent
|
|
59
|
+
*/
|
|
60
|
+
export type DocumentAgentOptions = {
|
|
61
|
+
/** LLM configuration */
|
|
62
|
+
llmConfig: LLMConfig;
|
|
63
|
+
/** LangChain tools to use */
|
|
64
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
65
|
+
tools: any[];
|
|
66
|
+
/** Maximum iterations before stopping */
|
|
67
|
+
maxIterations?: number;
|
|
68
|
+
/** Callback for verbose mode */
|
|
69
|
+
onStep?: AgentStepCallback;
|
|
70
|
+
/** Collections to restrict searches to (instruction in system prompt) */
|
|
71
|
+
collections?: string[];
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Options for asking a question
|
|
76
|
+
*/
|
|
77
|
+
export type AskOptions = {
|
|
78
|
+
/** Callback for verbose mode */
|
|
79
|
+
onStep?: AgentStepCallback;
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Retry configuration for LLM calls
|
|
84
|
+
*/
|
|
85
|
+
export type RetryConfig = {
|
|
86
|
+
/** Maximum number of retry attempts */
|
|
87
|
+
maxRetries: number;
|
|
88
|
+
/** Initial delay in ms */
|
|
89
|
+
initialDelayMs: number;
|
|
90
|
+
/** Maximum delay in ms */
|
|
91
|
+
maxDelayMs: number;
|
|
92
|
+
/** Multiplier for exponential backoff */
|
|
93
|
+
backoffMultiplier: number;
|
|
94
|
+
};
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
# Backend Service — Agent Guidelines
|
|
2
|
+
|
|
3
|
+
This document describes the backend service architecture for AI agents working on this module.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
The backend service is a JSON-RPC 2.0 inspired request handler that provides the core API for ctxpkg. It exposes procedures for managing reference documents, collections, and system operations. The daemon runs this backend and exposes it via Unix socket.
|
|
8
|
+
|
|
9
|
+
## File Structure
|
|
10
|
+
|
|
11
|
+
| File | Purpose |
|
|
12
|
+
|------|---------|
|
|
13
|
+
| `backend.ts` | Main `Backend` class — request routing and lifecycle |
|
|
14
|
+
| `backend.protocol.ts` | JSON-RPC protocol types, error codes, procedure helpers |
|
|
15
|
+
| `backend.schemas.ts` | Zod schemas for all API parameters and responses |
|
|
16
|
+
| `backend.services.ts` | Service procedure implementations (business logic) |
|
|
17
|
+
| `backend.types.ts` | Type utilities for type-safe client usage |
|
|
18
|
+
|
|
19
|
+
## Architecture
|
|
20
|
+
|
|
21
|
+
```
|
|
22
|
+
┌─────────────────────────────────────────────────────────────┐
|
|
23
|
+
│ Backend │
|
|
24
|
+
│ ┌─────────────────┐ │
|
|
25
|
+
│ │ handleRequest() │ ← raw JSON │
|
|
26
|
+
│ └────────┬────────┘ │
|
|
27
|
+
│ │ parse & validate │
|
|
28
|
+
│ ┌────────▼────────┐ │
|
|
29
|
+
│ │ #routeRequest() │ → "service.method" dispatch │
|
|
30
|
+
│ └────────┬────────┘ │
|
|
31
|
+
│ │ │
|
|
32
|
+
│ ┌────────▼────────────────────────────────────────────┐ │
|
|
33
|
+
│ │ BackendServices │ │
|
|
34
|
+
│ │ ┌────────────┐ ┌─────────────┐ ┌────────────────┐ │ │
|
|
35
|
+
│ │ │ documents │ │ collections │ │ system │ │ │
|
|
36
|
+
│ │ └────────────┘ └─────────────┘ └────────────────┘ │ │
|
|
37
|
+
│ └─────────────────────────────────────────────────────┘ │
|
|
38
|
+
└─────────────────────────────────────────────────────────────┘
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
### Request Flow
|
|
42
|
+
|
|
43
|
+
1. Raw JSON arrives at `handleRequest()`
|
|
44
|
+
2. Request is parsed and validated against `requestSchema`
|
|
45
|
+
3. Method string (`"service.method"`) is split and routed
|
|
46
|
+
4. Procedure input is validated against its Zod schema
|
|
47
|
+
5. Handler executes and returns result or error
|
|
48
|
+
|
|
49
|
+
### Protocol Format
|
|
50
|
+
|
|
51
|
+
```typescript
|
|
52
|
+
// Request: { id, method: "service.method", params? }
|
|
53
|
+
// Success: { id, result }
|
|
54
|
+
// Error: { id, error: { code, message, data? } }
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Error codes are defined in `ErrorCodes` (e.g., `ParseError`, `MethodNotFound`, `InvalidParams`, `ServiceError`).
|
|
58
|
+
|
|
59
|
+
## Adding New Procedures
|
|
60
|
+
|
|
61
|
+
1. **Add schema** in `backend.schemas.ts`:
|
|
62
|
+
|
|
63
|
+
```typescript
|
|
64
|
+
const myNewParamsSchema = z.object({
|
|
65
|
+
foo: z.string(),
|
|
66
|
+
bar: z.number().optional(),
|
|
67
|
+
});
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
2. **Add procedure** in `backend.services.ts` under the appropriate service namespace:
|
|
71
|
+
|
|
72
|
+
```typescript
|
|
73
|
+
const myService = {
|
|
74
|
+
myMethod: procedure(myNewParamsSchema, async (params): Promise<MyResult> => {
|
|
75
|
+
const service = services.get(MyService);
|
|
76
|
+
return service.doSomething(params);
|
|
77
|
+
}),
|
|
78
|
+
};
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
3. **Export types** if needed from `backend.schemas.ts`.
|
|
82
|
+
|
|
83
|
+
## Key Patterns
|
|
84
|
+
|
|
85
|
+
### Procedure Definition
|
|
86
|
+
|
|
87
|
+
Use the `procedure()` helper for type-safe handlers:
|
|
88
|
+
|
|
89
|
+
```typescript
|
|
90
|
+
procedure(
|
|
91
|
+
inputSchema, // Zod schema for params
|
|
92
|
+
async (params) => { // Handler receives validated params
|
|
93
|
+
return result; // Return type is inferred
|
|
94
|
+
}
|
|
95
|
+
);
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
### Service Access
|
|
99
|
+
|
|
100
|
+
Services are accessed via the dependency injection container:
|
|
101
|
+
|
|
102
|
+
```typescript
|
|
103
|
+
const docService = services.get(DocumentsService);
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
### Method Routing
|
|
107
|
+
|
|
108
|
+
Methods use `service.method` format — the Backend class splits this and looks up the procedure in `BackendServices`.
|
|
109
|
+
|
|
110
|
+
### Type-Safe Clients
|
|
111
|
+
|
|
112
|
+
The `BackendAPI` type in `backend.types.ts` converts procedures to function signatures. Use `GetBackendAPIParams` and `GetBackendAPIResponse` helpers to extract types for specific methods.
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
|
|
3
|
+
// Request/Response protocol (JSON-RPC 2.0 inspired)
|
|
4
|
+
const requestSchema = z.object({
|
|
5
|
+
id: z.string(),
|
|
6
|
+
method: z.string(),
|
|
7
|
+
params: z.unknown().optional(),
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
type Request = z.infer<typeof requestSchema>;
|
|
11
|
+
|
|
12
|
+
const responseSchema = z.object({
|
|
13
|
+
id: z.string(),
|
|
14
|
+
result: z.unknown().optional(),
|
|
15
|
+
error: z
|
|
16
|
+
.object({
|
|
17
|
+
code: z.number(),
|
|
18
|
+
message: z.string(),
|
|
19
|
+
data: z.unknown().optional(),
|
|
20
|
+
})
|
|
21
|
+
.optional(),
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
type Response = z.infer<typeof responseSchema>;
|
|
25
|
+
|
|
26
|
+
// Standard error codes
|
|
27
|
+
const ErrorCodes = {
|
|
28
|
+
ParseError: -32700,
|
|
29
|
+
InvalidRequest: -32600,
|
|
30
|
+
MethodNotFound: -32601,
|
|
31
|
+
InvalidParams: -32602,
|
|
32
|
+
InternalError: -32603,
|
|
33
|
+
// Custom codes
|
|
34
|
+
ServiceError: -32000,
|
|
35
|
+
NotConnected: -32001,
|
|
36
|
+
Timeout: -32002,
|
|
37
|
+
} as const;
|
|
38
|
+
|
|
39
|
+
type ErrorCode = (typeof ErrorCodes)[keyof typeof ErrorCodes];
|
|
40
|
+
|
|
41
|
+
// Procedure definition for type-safe handlers
|
|
42
|
+
type Procedure<TInput extends z.ZodTypeAny, TOutput> = {
|
|
43
|
+
input: TInput;
|
|
44
|
+
handler: (params: z.infer<TInput>) => Promise<TOutput>;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
// Helper to create procedures with type inference
|
|
48
|
+
const procedure = <TInput extends z.ZodTypeAny, TOutput>(
|
|
49
|
+
input: TInput,
|
|
50
|
+
handler: (params: z.infer<TInput>) => Promise<TOutput>,
|
|
51
|
+
): Procedure<TInput, TOutput> => ({
|
|
52
|
+
input,
|
|
53
|
+
handler,
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
// Service definition type - maps method names to procedures
|
|
57
|
+
type ServiceDefinition = Record<string, Procedure<z.ZodTypeAny, unknown>>;
|
|
58
|
+
|
|
59
|
+
// Extract input type from a procedure
|
|
60
|
+
type ProcedureInput<T> = T extends Procedure<infer TInput, unknown> ? z.infer<TInput> : never;
|
|
61
|
+
|
|
62
|
+
// Extract output type from a procedure
|
|
63
|
+
type ProcedureOutput<T> = T extends Procedure<z.ZodTypeAny, infer TOutput> ? TOutput : never;
|
|
64
|
+
|
|
65
|
+
// Convert a procedure to its function signature
|
|
66
|
+
// When input is an empty object, the parameter becomes optional
|
|
67
|
+
type ProcedureToFunction<T> =
|
|
68
|
+
T extends Procedure<infer TInput, infer TOutput>
|
|
69
|
+
? keyof z.infer<TInput> extends never
|
|
70
|
+
? (params?: z.infer<TInput>) => Promise<TOutput>
|
|
71
|
+
: (params: z.infer<TInput>) => Promise<TOutput>
|
|
72
|
+
: never;
|
|
73
|
+
|
|
74
|
+
// Create response helpers
|
|
75
|
+
const createSuccessResponse = (id: string, result: unknown): Response => ({
|
|
76
|
+
id,
|
|
77
|
+
result,
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
const createErrorResponse = (id: string, code: ErrorCode, message: string, data?: unknown): Response => ({
|
|
81
|
+
id,
|
|
82
|
+
error: { code, message, data },
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
export type {
|
|
86
|
+
Request,
|
|
87
|
+
Response,
|
|
88
|
+
Procedure,
|
|
89
|
+
ServiceDefinition,
|
|
90
|
+
ProcedureInput,
|
|
91
|
+
ProcedureOutput,
|
|
92
|
+
ProcedureToFunction,
|
|
93
|
+
ErrorCode,
|
|
94
|
+
};
|
|
95
|
+
export { requestSchema, responseSchema, ErrorCodes, procedure, createSuccessResponse, createErrorResponse };
|