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,148 @@
1
+ const assert = require("assert");
2
+ const { describe, it, beforeEach, afterEach } = require("node:test");
3
+
4
+ describe("Azure OpenAI Routing Tests", () => {
5
+ let routing;
6
+ let originalConfig;
7
+
8
+ beforeEach(() => {
9
+ // Clear module cache
10
+ delete require.cache[require.resolve("../src/config")];
11
+ delete require.cache[require.resolve("../src/clients/routing")];
12
+
13
+ // Store original config
14
+ originalConfig = { ...process.env };
15
+
16
+ // Clean OpenRouter config from previous tests
17
+ delete process.env.OPENROUTER_API_KEY;
18
+
19
+ // Base config for routing tests
20
+ process.env.MODEL_PROVIDER = "databricks"; // Set default to avoid validation errors
21
+ process.env.DATABRICKS_API_KEY = "test-key";
22
+ process.env.DATABRICKS_API_BASE = "http://test.com";
23
+ });
24
+
25
+ afterEach(() => {
26
+ // Restore original environment
27
+ process.env = originalConfig;
28
+ });
29
+
30
+ describe("Primary Provider Routing", () => {
31
+ it("should route to azure-openai when set as MODEL_PROVIDER", () => {
32
+ process.env.MODEL_PROVIDER = "azure-openai";
33
+ process.env.AZURE_OPENAI_ENDPOINT = "https://test.openai.azure.com";
34
+ process.env.AZURE_OPENAI_API_KEY = "test-key";
35
+ process.env.PREFER_OLLAMA = "false";
36
+
37
+ routing = require("../src/clients/routing");
38
+
39
+ const provider = routing.determineProvider({ tools: [] });
40
+
41
+ assert.strictEqual(provider, "azure-openai");
42
+ });
43
+ });
44
+
45
+ describe("Hybrid Routing with Azure OpenAI", () => {
46
+ it("should route moderate tool requests to azure-openai when available", () => {
47
+ // Explicitly unset OpenRouter to ensure it's not available
48
+ // Set to empty string instead of delete to prevent dotenv from reloading it
49
+ process.env.OPENROUTER_API_KEY = "";
50
+
51
+ process.env.PREFER_OLLAMA = "true";
52
+ process.env.OLLAMA_ENDPOINT = "http://localhost:11434";
53
+ process.env.OLLAMA_MODEL = "qwen2.5-coder:latest";
54
+ process.env.FALLBACK_ENABLED = "true";
55
+ process.env.OLLAMA_MAX_TOOLS_FOR_ROUTING = "3";
56
+ process.env.OPENROUTER_MAX_TOOLS_FOR_ROUTING = "15";
57
+ process.env.AZURE_OPENAI_ENDPOINT = "https://test.openai.azure.com";
58
+ process.env.AZURE_OPENAI_API_KEY = "test-key";
59
+
60
+ // Clear cache after env setup
61
+ delete require.cache[require.resolve("../src/config")];
62
+ delete require.cache[require.resolve("../src/clients/routing")];
63
+
64
+ routing = require("../src/clients/routing");
65
+
66
+ // 5 tools: more than Ollama threshold (3), less than OpenRouter threshold (15)
67
+ const provider = routing.determineProvider({
68
+ tools: [{}, {}, {}, {}, {}]
69
+ });
70
+
71
+ assert.strictEqual(provider, "azure-openai");
72
+ });
73
+
74
+ it("should prefer OpenRouter over Azure OpenAI when both configured", () => {
75
+ process.env.PREFER_OLLAMA = "true";
76
+ process.env.OLLAMA_ENDPOINT = "http://localhost:11434";
77
+ process.env.OLLAMA_MODEL = "qwen2.5-coder:latest";
78
+ process.env.FALLBACK_ENABLED = "true";
79
+ process.env.OLLAMA_MAX_TOOLS_FOR_ROUTING = "3";
80
+ process.env.OPENROUTER_MAX_TOOLS_FOR_ROUTING = "15";
81
+ process.env.OPENROUTER_API_KEY = "openrouter-key";
82
+ process.env.AZURE_OPENAI_ENDPOINT = "https://test.openai.azure.com";
83
+ process.env.AZURE_OPENAI_API_KEY = "azure-key";
84
+
85
+ routing = require("../src/clients/routing");
86
+
87
+ // 5 tools: should prefer OpenRouter
88
+ const provider = routing.determineProvider({
89
+ tools: [{}, {}, {}, {}, {}]
90
+ });
91
+
92
+ assert.strictEqual(provider, "openrouter");
93
+ });
94
+
95
+ it("should route simple requests to Ollama even when Azure OpenAI configured", () => {
96
+ process.env.PREFER_OLLAMA = "true";
97
+ process.env.OLLAMA_ENDPOINT = "http://localhost:11434";
98
+ process.env.OLLAMA_MODEL = "qwen2.5-coder:latest";
99
+ process.env.FALLBACK_ENABLED = "true";
100
+ process.env.OLLAMA_MAX_TOOLS_FOR_ROUTING = "3";
101
+ process.env.AZURE_OPENAI_ENDPOINT = "https://test.openai.azure.com";
102
+ process.env.AZURE_OPENAI_API_KEY = "test-key";
103
+
104
+ routing = require("../src/clients/routing");
105
+
106
+ // 2 tools: under Ollama threshold
107
+ const provider = routing.determineProvider({
108
+ tools: [{}, {}]
109
+ });
110
+
111
+ assert.strictEqual(provider, "ollama");
112
+ });
113
+ });
114
+
115
+ describe("Fallback Configuration", () => {
116
+ it("should support azure-openai as fallback provider", () => {
117
+ process.env.PREFER_OLLAMA = "true";
118
+ process.env.OLLAMA_ENDPOINT = "http://localhost:11434";
119
+ process.env.OLLAMA_MODEL = "qwen2.5-coder:latest";
120
+ process.env.FALLBACK_ENABLED = "true";
121
+ process.env.FALLBACK_PROVIDER = "azure-openai";
122
+ process.env.AZURE_OPENAI_ENDPOINT = "https://test.openai.azure.com";
123
+ process.env.AZURE_OPENAI_API_KEY = "test-key";
124
+
125
+ routing = require("../src/clients/routing");
126
+
127
+ const fallbackProvider = routing.getFallbackProvider();
128
+
129
+ assert.strictEqual(fallbackProvider, "azure-openai");
130
+ });
131
+
132
+ it("should return true for fallback enabled", () => {
133
+ process.env.FALLBACK_ENABLED = "true";
134
+
135
+ routing = require("../src/clients/routing");
136
+
137
+ assert.strictEqual(routing.isFallbackEnabled(), true);
138
+ });
139
+
140
+ it("should return false when fallback disabled", () => {
141
+ process.env.FALLBACK_ENABLED = "false";
142
+
143
+ routing = require("../src/clients/routing");
144
+
145
+ assert.strictEqual(routing.isFallbackEnabled(), false);
146
+ });
147
+ });
148
+ });
@@ -0,0 +1,171 @@
1
+ const assert = require("assert");
2
+ const { describe, it } = require("node:test");
3
+
4
+ describe("Azure OpenAI Streaming Tests", () => {
5
+ describe("Streaming Response Structure", () => {
6
+ it("should recognize Azure OpenAI SSE streaming format", () => {
7
+ const streamChunk = {
8
+ id: "chatcmpl-123",
9
+ object: "chat.completion.chunk",
10
+ created: 1677652288,
11
+ model: "gpt-4o",
12
+ choices: [
13
+ {
14
+ index: 0,
15
+ delta: {
16
+ content: "Hello"
17
+ },
18
+ finish_reason: null
19
+ }
20
+ ]
21
+ };
22
+
23
+ assert.strictEqual(streamChunk.object, "chat.completion.chunk");
24
+ assert.ok(streamChunk.choices[0].delta);
25
+ });
26
+
27
+ it("should handle streaming delta with tool_calls", () => {
28
+ const streamChunk = {
29
+ id: "chatcmpl-123",
30
+ object: "chat.completion.chunk",
31
+ model: "gpt-4o",
32
+ choices: [
33
+ {
34
+ index: 0,
35
+ delta: {
36
+ tool_calls: [
37
+ {
38
+ index: 0,
39
+ id: "call_abc123",
40
+ type: "function",
41
+ function: {
42
+ name: "Read",
43
+ arguments: "{\"file"
44
+ }
45
+ }
46
+ ]
47
+ },
48
+ finish_reason: null
49
+ }
50
+ ]
51
+ };
52
+
53
+ assert.ok(streamChunk.choices[0].delta.tool_calls);
54
+ assert.strictEqual(streamChunk.choices[0].delta.tool_calls[0].function.name, "Read");
55
+ });
56
+
57
+ it("should handle final streaming chunk with finish_reason", () => {
58
+ const finalChunk = {
59
+ id: "chatcmpl-123",
60
+ object: "chat.completion.chunk",
61
+ model: "gpt-4o",
62
+ choices: [
63
+ {
64
+ index: 0,
65
+ delta: {},
66
+ finish_reason: "stop"
67
+ }
68
+ ]
69
+ };
70
+
71
+ assert.strictEqual(finalChunk.choices[0].finish_reason, "stop");
72
+ });
73
+
74
+ it("should handle tool_calls finish_reason in streaming", () => {
75
+ const toolCallFinish = {
76
+ id: "chatcmpl-123",
77
+ object: "chat.completion.chunk",
78
+ model: "gpt-4o",
79
+ choices: [
80
+ {
81
+ index: 0,
82
+ delta: {},
83
+ finish_reason: "tool_calls"
84
+ }
85
+ ]
86
+ };
87
+
88
+ assert.strictEqual(toolCallFinish.choices[0].finish_reason, "tool_calls");
89
+ });
90
+ });
91
+
92
+ describe("Stream Flag in Requests", () => {
93
+ it("should set stream:false by default for buffered requests", () => {
94
+ const requestBody = {
95
+ messages: [{ role: "user", content: "Hello" }],
96
+ temperature: 0.7
97
+ };
98
+
99
+ // Default stream value
100
+ const stream = requestBody.stream ?? false;
101
+
102
+ assert.strictEqual(stream, false);
103
+ });
104
+
105
+ it("should set stream:true for streaming requests", () => {
106
+ const requestBody = {
107
+ messages: [{ role: "user", content: "Hello" }],
108
+ stream: true
109
+ };
110
+
111
+ assert.strictEqual(requestBody.stream, true);
112
+ });
113
+ });
114
+
115
+ describe("SSE Format Validation", () => {
116
+ it("should recognize SSE event format with data prefix", () => {
117
+ const sseChunk = 'data: {"id":"chatcmpl-123","object":"chat.completion.chunk","choices":[{"delta":{"content":"Hi"}}]}';
118
+
119
+ assert.ok(sseChunk.startsWith("data:"));
120
+
121
+ // Extract JSON after "data: "
122
+ const jsonStr = sseChunk.substring(6);
123
+ const parsed = JSON.parse(jsonStr);
124
+
125
+ assert.strictEqual(parsed.object, "chat.completion.chunk");
126
+ assert.strictEqual(parsed.choices[0].delta.content, "Hi");
127
+ });
128
+
129
+ it("should handle SSE done signal", () => {
130
+ const doneSig = "data: [DONE]";
131
+
132
+ assert.strictEqual(doneSig, "data: [DONE]");
133
+ });
134
+ });
135
+
136
+ describe("Chunk Accumulation", () => {
137
+ it("should accumulate tool arguments across multiple chunks", () => {
138
+ const chunks = [
139
+ { delta: { tool_calls: [{ index: 0, function: { arguments: "{\"file_" } }] } },
140
+ { delta: { tool_calls: [{ index: 0, function: { arguments: "path\":" } }] } },
141
+ { delta: { tool_calls: [{ index: 0, function: { arguments: "\"/test.js" } }] } },
142
+ { delta: { tool_calls: [{ index: 0, function: { arguments: "\"}" } }] } }
143
+ ];
144
+
145
+ let accumulated = "";
146
+ for (const chunk of chunks) {
147
+ accumulated += chunk.delta.tool_calls[0].function.arguments;
148
+ }
149
+
150
+ assert.strictEqual(accumulated, '{"file_path":"/test.js"}');
151
+ const parsed = JSON.parse(accumulated);
152
+ assert.strictEqual(parsed.file_path, "/test.js");
153
+ });
154
+
155
+ it("should accumulate content across multiple chunks", () => {
156
+ const chunks = [
157
+ { delta: { content: "Hello" } },
158
+ { delta: { content: ", " } },
159
+ { delta: { content: "world" } },
160
+ { delta: { content: "!" } }
161
+ ];
162
+
163
+ let accumulated = "";
164
+ for (const chunk of chunks) {
165
+ accumulated += chunk.delta.content;
166
+ }
167
+
168
+ assert.strictEqual(accumulated, "Hello, world!");
169
+ });
170
+ });
171
+ });