lynkr 1.0.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/CITATIONS.bib +6 -0
  2. package/DEPLOYMENT.md +1001 -0
  3. package/README.md +215 -71
  4. package/docs/index.md +55 -2
  5. package/monitor-agents.sh +31 -0
  6. package/package.json +7 -3
  7. package/src/agents/context-manager.js +220 -0
  8. package/src/agents/definitions/loader.js +563 -0
  9. package/src/agents/executor.js +412 -0
  10. package/src/agents/index.js +157 -0
  11. package/src/agents/parallel-coordinator.js +68 -0
  12. package/src/agents/reflector.js +321 -0
  13. package/src/agents/skillbook.js +331 -0
  14. package/src/agents/store.js +244 -0
  15. package/src/api/router.js +55 -0
  16. package/src/clients/databricks.js +214 -17
  17. package/src/clients/routing.js +15 -7
  18. package/src/clients/standard-tools.js +341 -0
  19. package/src/config/index.js +41 -5
  20. package/src/orchestrator/index.js +254 -37
  21. package/src/server.js +2 -0
  22. package/src/tools/agent-task.js +96 -0
  23. package/test/azure-openai-config.test.js +203 -0
  24. package/test/azure-openai-error-resilience.test.js +238 -0
  25. package/test/azure-openai-format-conversion.test.js +354 -0
  26. package/test/azure-openai-integration.test.js +281 -0
  27. package/test/azure-openai-routing.test.js +148 -0
  28. package/test/azure-openai-streaming.test.js +171 -0
  29. package/test/format-conversion.test.js +578 -0
  30. package/test/hybrid-routing-integration.test.js +18 -11
  31. package/test/openrouter-error-resilience.test.js +418 -0
  32. package/test/passthrough-mode.test.js +385 -0
  33. package/test/routing.test.js +9 -3
  34. package/test/web-tools.test.js +3 -0
  35. package/test-agents-simple.js +43 -0
  36. package/test-cli-connection.sh +33 -0
  37. package/test-learning-unit.js +126 -0
  38. package/test-learning.js +112 -0
  39. package/test-parallel-agents.sh +124 -0
  40. package/test-parallel-direct.js +155 -0
  41. package/test-subagents.sh +117 -0
@@ -0,0 +1,341 @@
1
+ /**
2
+ * Standard tool definitions for Claude Code
3
+ * These tools are injected when the client doesn't send tools in passthrough mode
4
+ */
5
+
6
+ const STANDARD_TOOLS = [
7
+ {
8
+ name: "Write",
9
+ description: "Writes a file to the local filesystem. Overwrites existing files. ALWAYS prefer editing existing files in the codebase. NEVER write new files unless explicitly required.",
10
+ input_schema: {
11
+ type: "object",
12
+ properties: {
13
+ file_path: {
14
+ type: "string",
15
+ description: "The absolute path to the file to write (must be absolute, not relative)"
16
+ },
17
+ content: {
18
+ type: "string",
19
+ description: "The content to write to the file"
20
+ }
21
+ },
22
+ required: ["file_path", "content"]
23
+ }
24
+ },
25
+ {
26
+ name: "Read",
27
+ description: "Reads a file from the local filesystem. You can access any file directly by using this tool.",
28
+ input_schema: {
29
+ type: "object",
30
+ properties: {
31
+ file_path: {
32
+ type: "string",
33
+ description: "The absolute path to the file to read"
34
+ },
35
+ limit: {
36
+ type: "number",
37
+ description: "The number of lines to read. Only provide if the file is too large to read at once."
38
+ },
39
+ offset: {
40
+ type: "number",
41
+ description: "The line number to start reading from. Only provide if the file is too large to read at once"
42
+ }
43
+ },
44
+ required: ["file_path"]
45
+ }
46
+ },
47
+ {
48
+ name: "Edit",
49
+ description: "Performs exact string replacements in files. You must use your Read tool at least once before editing. The edit will FAIL if old_string is not unique in the file.",
50
+ input_schema: {
51
+ type: "object",
52
+ properties: {
53
+ file_path: {
54
+ type: "string",
55
+ description: "The absolute path to the file to modify"
56
+ },
57
+ old_string: {
58
+ type: "string",
59
+ description: "The text to replace"
60
+ },
61
+ new_string: {
62
+ type: "string",
63
+ description: "The text to replace it with (must be different from old_string)"
64
+ },
65
+ replace_all: {
66
+ type: "boolean",
67
+ description: "Replace all occurences of old_string (default false)"
68
+ }
69
+ },
70
+ required: ["file_path", "old_string", "new_string"]
71
+ }
72
+ },
73
+ {
74
+ name: "Bash",
75
+ description: "Executes a bash command in a persistent shell session. Use for terminal operations like git, npm, docker, etc. DO NOT use for file operations - use specialized tools instead.",
76
+ input_schema: {
77
+ type: "object",
78
+ properties: {
79
+ command: {
80
+ type: "string",
81
+ description: "The command to execute"
82
+ },
83
+ description: {
84
+ type: "string",
85
+ description: "Clear, concise description of what this command does in 5-10 words"
86
+ },
87
+ timeout: {
88
+ type: "number",
89
+ description: "Optional timeout in milliseconds (max 600000)"
90
+ }
91
+ },
92
+ required: ["command"]
93
+ }
94
+ },
95
+ {
96
+ name: "Glob",
97
+ description: "Fast file pattern matching tool. Supports glob patterns like '**/*.js' or 'src/**/*.ts'. Returns matching file paths sorted by modification time.",
98
+ input_schema: {
99
+ type: "object",
100
+ properties: {
101
+ pattern: {
102
+ type: "string",
103
+ description: "The glob pattern to match files against"
104
+ },
105
+ path: {
106
+ type: "string",
107
+ description: "The directory to search in. If not specified, the current working directory will be used."
108
+ }
109
+ },
110
+ required: ["pattern"]
111
+ }
112
+ },
113
+ {
114
+ name: "Grep",
115
+ description: "A powerful search tool built on ripgrep. Supports full regex syntax. Filter files with glob parameter or type parameter.",
116
+ input_schema: {
117
+ type: "object",
118
+ properties: {
119
+ pattern: {
120
+ type: "string",
121
+ description: "The regular expression pattern to search for in file contents"
122
+ },
123
+ path: {
124
+ type: "string",
125
+ description: "File or directory to search in. Defaults to current working directory."
126
+ },
127
+ glob: {
128
+ type: "string",
129
+ description: "Glob pattern to filter files (e.g. '*.js', '*.{ts,tsx}')"
130
+ },
131
+ output_mode: {
132
+ type: "string",
133
+ enum: ["content", "files_with_matches", "count"],
134
+ description: "Output mode: 'content' shows matching lines, 'files_with_matches' shows file paths, 'count' shows match counts"
135
+ },
136
+ "-i": {
137
+ type: "boolean",
138
+ description: "Case insensitive search"
139
+ }
140
+ },
141
+ required: ["pattern"]
142
+ }
143
+ },
144
+ {
145
+ name: "TodoWrite",
146
+ description: "Create and manage a structured task list for tracking progress and organizing complex tasks. Use proactively for multi-step tasks or when user provides multiple tasks.",
147
+ input_schema: {
148
+ type: "object",
149
+ properties: {
150
+ todos: {
151
+ type: "array",
152
+ description: "Array of todo items with status tracking",
153
+ items: {
154
+ type: "object",
155
+ properties: {
156
+ content: {
157
+ type: "string",
158
+ description: "Task description in imperative form (e.g., 'Run tests', 'Build project')"
159
+ },
160
+ status: {
161
+ type: "string",
162
+ enum: ["pending", "in_progress", "completed"],
163
+ description: "Task status: pending (not started), in_progress (currently working), completed (finished)"
164
+ },
165
+ activeForm: {
166
+ type: "string",
167
+ description: "Present continuous form shown during execution (e.g., 'Running tests', 'Building project')"
168
+ }
169
+ },
170
+ required: ["content", "status", "activeForm"]
171
+ }
172
+ }
173
+ },
174
+ required: ["todos"]
175
+ }
176
+ },
177
+ {
178
+ name: "Task",
179
+ description: "Launch specialized agents for complex multi-step tasks. Available agents: general-purpose (complex tasks), Explore (codebase exploration), Plan (implementation planning), claude-code-guide (Claude Code documentation).",
180
+ input_schema: {
181
+ type: "object",
182
+ properties: {
183
+ description: {
184
+ type: "string",
185
+ description: "A short (3-5 word) description of the task"
186
+ },
187
+ prompt: {
188
+ type: "string",
189
+ description: "The detailed task for the agent to perform"
190
+ },
191
+ subagent_type: {
192
+ type: "string",
193
+ enum: ["general-purpose", "Explore", "Plan", "claude-code-guide"],
194
+ description: "The type of specialized agent to use"
195
+ },
196
+ model: {
197
+ type: "string",
198
+ enum: ["sonnet", "opus", "haiku"],
199
+ description: "Optional model to use (haiku for quick tasks, sonnet for balanced, opus for complex)"
200
+ }
201
+ },
202
+ required: ["description", "prompt", "subagent_type"]
203
+ }
204
+ },
205
+ {
206
+ name: "AskUserQuestion",
207
+ description: "Ask the user questions to gather preferences, clarify requirements, or get decisions on implementation choices. Supports multiple choice questions.",
208
+ input_schema: {
209
+ type: "object",
210
+ properties: {
211
+ questions: {
212
+ type: "array",
213
+ description: "Questions to ask the user (1-4 questions)",
214
+ minItems: 1,
215
+ maxItems: 4,
216
+ items: {
217
+ type: "object",
218
+ properties: {
219
+ question: {
220
+ type: "string",
221
+ description: "The complete question to ask. Should be clear, specific, and end with a question mark."
222
+ },
223
+ header: {
224
+ type: "string",
225
+ description: "Very short label (max 12 chars). Examples: 'Auth method', 'Library', 'Approach'"
226
+ },
227
+ options: {
228
+ type: "array",
229
+ description: "Available choices (2-4 options). Each should be distinct and mutually exclusive.",
230
+ minItems: 2,
231
+ maxItems: 4,
232
+ items: {
233
+ type: "object",
234
+ properties: {
235
+ label: {
236
+ type: "string",
237
+ description: "Display text for this option (1-5 words)"
238
+ },
239
+ description: {
240
+ type: "string",
241
+ description: "Explanation of what this option means or implications"
242
+ }
243
+ },
244
+ required: ["label", "description"]
245
+ }
246
+ },
247
+ multiSelect: {
248
+ type: "boolean",
249
+ description: "Set to true to allow multiple selections instead of just one"
250
+ }
251
+ },
252
+ required: ["question", "header", "options", "multiSelect"]
253
+ }
254
+ }
255
+ },
256
+ required: ["questions"]
257
+ }
258
+ },
259
+ {
260
+ name: "WebSearch",
261
+ description: "Search the web for current information beyond the model's knowledge cutoff. Returns search results that can inform responses. Always include sources in your response.",
262
+ input_schema: {
263
+ type: "object",
264
+ properties: {
265
+ query: {
266
+ type: "string",
267
+ description: "The search query to use",
268
+ minLength: 2
269
+ },
270
+ allowed_domains: {
271
+ type: "array",
272
+ description: "Only include search results from these domains",
273
+ items: {
274
+ type: "string"
275
+ }
276
+ },
277
+ blocked_domains: {
278
+ type: "array",
279
+ description: "Never include search results from these domains",
280
+ items: {
281
+ type: "string"
282
+ }
283
+ }
284
+ },
285
+ required: ["query"]
286
+ }
287
+ },
288
+ {
289
+ name: "WebFetch",
290
+ description: "Fetches content from a specified URL and processes it using AI. Takes a URL and a prompt describing what information to extract from the page.",
291
+ input_schema: {
292
+ type: "object",
293
+ properties: {
294
+ url: {
295
+ type: "string",
296
+ description: "The URL to fetch content from (must be fully-formed valid URL)",
297
+ format: "uri"
298
+ },
299
+ prompt: {
300
+ type: "string",
301
+ description: "The prompt describing what information you want to extract from the page"
302
+ }
303
+ },
304
+ required: ["url", "prompt"]
305
+ }
306
+ },
307
+ {
308
+ name: "NotebookEdit",
309
+ description: "Completely replaces the contents of a specific cell in a Jupyter notebook (.ipynb file). Use for editing interactive documents that combine code, text, and visualizations.",
310
+ input_schema: {
311
+ type: "object",
312
+ properties: {
313
+ notebook_path: {
314
+ type: "string",
315
+ description: "The absolute path to the Jupyter notebook file to edit"
316
+ },
317
+ new_source: {
318
+ type: "string",
319
+ description: "The new source for the cell"
320
+ },
321
+ cell_id: {
322
+ type: "string",
323
+ description: "The ID of the cell to edit. When inserting, new cell will be inserted after this cell."
324
+ },
325
+ cell_type: {
326
+ type: "string",
327
+ enum: ["code", "markdown"],
328
+ description: "The type of the cell. Required when using edit_mode=insert."
329
+ },
330
+ edit_mode: {
331
+ type: "string",
332
+ enum: ["replace", "insert", "delete"],
333
+ description: "The type of edit to make. Defaults to replace."
334
+ }
335
+ },
336
+ required: ["notebook_path", "new_source"]
337
+ }
338
+ }
339
+ ];
340
+
341
+ module.exports = { STANDARD_TOOLS };
@@ -62,7 +62,7 @@ function resolveConfigPath(targetPath) {
62
62
  return path.resolve(normalised);
63
63
  }
64
64
 
65
- const SUPPORTED_MODEL_PROVIDERS = new Set(["databricks", "azure-anthropic", "ollama", "openrouter"]);
65
+ const SUPPORTED_MODEL_PROVIDERS = new Set(["databricks", "azure-anthropic", "ollama", "openrouter", "azure-openai"]);
66
66
  const rawModelProvider = (process.env.MODEL_PROVIDER ?? "databricks").toLowerCase();
67
67
  const modelProvider = SUPPORTED_MODEL_PROVIDERS.has(rawModelProvider)
68
68
  ? rawModelProvider
@@ -84,6 +84,13 @@ const openRouterApiKey = process.env.OPENROUTER_API_KEY ?? null;
84
84
  const openRouterModel = process.env.OPENROUTER_MODEL ?? "openai/gpt-4o-mini";
85
85
  const openRouterEndpoint = process.env.OPENROUTER_ENDPOINT ?? "https://openrouter.ai/api/v1/chat/completions";
86
86
 
87
+ // Azure OpenAI configuration
88
+ const azureOpenAIEndpoint = process.env.AZURE_OPENAI_ENDPOINT?.trim() || null;
89
+ const azureOpenAIApiKey = process.env.AZURE_OPENAI_API_KEY?.trim() || null;
90
+ const azureOpenAIDeployment = process.env.AZURE_OPENAI_DEPLOYMENT?.trim() || "gpt-4o";
91
+ const azureOpenAIApiVersion = process.env.AZURE_OPENAI_API_VERSION?.trim() || "2024-08-01-preview";
92
+
93
+
87
94
  // Hybrid routing configuration
88
95
  const preferOllama = process.env.PREFER_OLLAMA === "true";
89
96
  const fallbackEnabled = process.env.FALLBACK_ENABLED !== "false"; // default true
@@ -108,8 +115,8 @@ if (!["server", "client", "passthrough"].includes(toolExecutionMode)) {
108
115
  // Only require Databricks credentials if it's the primary provider or used as fallback
109
116
  if (modelProvider === "databricks" && (!rawBaseUrl || !apiKey)) {
110
117
  throw new Error("Set DATABRICKS_API_BASE and DATABRICKS_API_KEY before starting the proxy.");
111
- } else if (modelProvider === "ollama" && fallbackEnabled && fallbackProvider === "databricks" && (!rawBaseUrl || !apiKey)) {
112
- // Relaxed: Allow mock credentials for Ollama-only testing
118
+ } else if (modelProvider === "ollama" && !fallbackEnabled && (!rawBaseUrl || !apiKey)) {
119
+ // Relaxed: Allow mock credentials for true Ollama-only mode (fallback disabled)
113
120
  if (!rawBaseUrl) process.env.DATABRICKS_API_BASE = "http://localhost:8080";
114
121
  if (!apiKey) process.env.DATABRICKS_API_KEY = "mock-key-for-ollama-only";
115
122
  console.log("[CONFIG] Using mock Databricks credentials (Ollama-only mode with fallback disabled)");
@@ -121,6 +128,12 @@ if (modelProvider === "azure-anthropic" && (!azureAnthropicEndpoint || !azureAnt
121
128
  );
122
129
  }
123
130
 
131
+ if (modelProvider === "azure-openai" && (!azureOpenAIEndpoint || !azureOpenAIApiKey)) {
132
+ throw new Error(
133
+ "Set AZURE_OPENAI_ENDPOINT and AZURE_OPENAI_API_KEY before starting the proxy.",
134
+ );
135
+ }
136
+
124
137
  if (modelProvider === "ollama") {
125
138
  try {
126
139
  new URL(ollamaEndpoint);
@@ -146,10 +159,13 @@ if (preferOllama) {
146
159
  // Ensure fallback provider is properly configured (only if fallback is enabled)
147
160
  if (fallbackEnabled) {
148
161
  if (fallbackProvider === "databricks" && (!rawBaseUrl || !apiKey)) {
149
- console.warn("[CONFIG WARNING] Databricks fallback configured but credentials missing. Fallback will fail if needed.");
162
+ throw new Error("FALLBACK_PROVIDER is set to 'databricks' but DATABRICKS_API_BASE and DATABRICKS_API_KEY are not configured. Please set these environment variables or choose a different fallback provider.");
150
163
  }
151
164
  if (fallbackProvider === "azure-anthropic" && (!azureAnthropicEndpoint || !azureAnthropicApiKey)) {
152
- console.warn("[CONFIG WARNING] Azure Anthropic fallback configured but credentials missing. Fallback will fail if needed.");
165
+ throw new Error("FALLBACK_PROVIDER is set to 'azure-anthropic' but AZURE_ANTHROPIC_ENDPOINT and AZURE_ANTHROPIC_API_KEY are not configured. Please set these environment variables or choose a different fallback provider.");
166
+ }
167
+ if (fallbackProvider === "azure-openai" && (!azureOpenAIEndpoint || !azureOpenAIApiKey)) {
168
+ throw new Error("FALLBACK_PROVIDER is set to 'azure-openai' but AZURE_OPENAI_ENDPOINT and AZURE_OPENAI_API_KEY are not configured. Please set these environment variables or choose a different fallback provider.");
153
169
  }
154
170
  }
155
171
  }
@@ -300,6 +316,13 @@ if (testCoverageFiles.length === 0) {
300
316
  }
301
317
  const testProfiles = parseJson(process.env.WORKSPACE_TEST_PROFILES ?? "", null);
302
318
 
319
+ // Agents configuration
320
+ const agentsEnabled = process.env.AGENTS_ENABLED === "true";
321
+ const agentsMaxConcurrent = Number.parseInt(process.env.AGENTS_MAX_CONCURRENT ?? "10", 10);
322
+ const agentsDefaultModel = process.env.AGENTS_DEFAULT_MODEL ?? "haiku";
323
+ const agentsMaxSteps = Number.parseInt(process.env.AGENTS_MAX_STEPS ?? "15", 10);
324
+ const agentsTimeout = Number.parseInt(process.env.AGENTS_TIMEOUT ?? "120000", 10);
325
+
303
326
  const config = {
304
327
  env: process.env.NODE_ENV ?? "development",
305
328
  port: Number.isNaN(port) ? 8080 : port,
@@ -324,6 +347,12 @@ const config = {
324
347
  model: openRouterModel,
325
348
  endpoint: openRouterEndpoint,
326
349
  },
350
+ azureOpenAI: {
351
+ endpoint: azureOpenAIEndpoint,
352
+ apiKey: azureOpenAIApiKey,
353
+ deployment: azureOpenAIDeployment,
354
+ apiVersion: azureOpenAIApiVersion
355
+ },
327
356
  modelProvider: {
328
357
  type: modelProvider,
329
358
  defaultModel,
@@ -426,6 +455,13 @@ const config = {
426
455
  maxEntries: Number.isNaN(promptCacheMaxEntriesRaw) ? 64 : promptCacheMaxEntriesRaw,
427
456
  ttlMs: Number.isNaN(promptCacheTtlRaw) ? 300000 : promptCacheTtlRaw,
428
457
  },
458
+ agents: {
459
+ enabled: agentsEnabled,
460
+ maxConcurrent: Number.isNaN(agentsMaxConcurrent) ? 10 : agentsMaxConcurrent,
461
+ defaultModel: agentsDefaultModel,
462
+ maxSteps: Number.isNaN(agentsMaxSteps) ? 15 : agentsMaxSteps,
463
+ timeout: Number.isNaN(agentsTimeout) ? 120000 : agentsTimeout,
464
+ },
429
465
  tests: {
430
466
  defaultCommand: testDefaultCommand ? testDefaultCommand.trim() : null,
431
467
  defaultArgs: testDefaultArgs,