lynkr 9.7.2 → 9.7.3
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/package.json +2 -2
- package/src/agents/context-manager.js +18 -2
- package/src/agents/definitions/loader.js +90 -0
- package/src/agents/executor.js +24 -2
- package/src/agents/index.js +9 -1
- package/src/agents/parallel-coordinator.js +2 -2
- package/src/agents/reflector.js +11 -1
- package/src/clients/databricks.js +4 -4
- package/src/orchestrator/index.js +24 -3
- package/src/routing/telemetry.js +20 -5
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lynkr",
|
|
3
|
-
"version": "9.7.
|
|
3
|
+
"version": "9.7.3",
|
|
4
4
|
"description": "Self-hosted LLM gateway and tier-routing proxy for Claude Code, Cursor, and Codex. Routes across Ollama, AWS Bedrock, OpenRouter, Databricks, Azure OpenAI, llama.cpp, and LM Studio with prompt caching, MCP tools, and 60-80% cost savings.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"bin": {
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
"dev": "nodemon index.js",
|
|
17
17
|
"lint": "eslint src index.js",
|
|
18
18
|
"test": "npm run test:unit && npm run test:performance",
|
|
19
|
-
"test:unit": "DATABRICKS_API_KEY=test-key DATABRICKS_API_BASE=http://test.com node --test test/routing.test.js test/hybrid-routing-integration.test.js test/web-tools.test.js test/passthrough-mode.test.js test/openrouter-error-resilience.test.js test/format-conversion.test.js test/azure-openai-config.test.js test/azure-openai-format-conversion.test.js test/azure-openai-routing.test.js test/azure-openai-streaming.test.js test/azure-openai-error-resilience.test.js test/azure-openai-integration.test.js test/openai-integration.test.js test/toon-compression.test.js test/llamacpp-integration.test.js test/resilience.test.js test/telemetry-routing.test.js test/memory/store.test.js test/memory/surprise.test.js test/memory/extractor.test.js test/memory/search.test.js test/memory/retriever.test.js test/distill.test.js test/large-payload.test.js test/code-mode.test.js test/prompt-cache-injection.test.js test/risk-analyzer.test.js test/interaction-block.test.js test/preflight.test.js test/token-reduction.test.js test/session-affinity.test.js test/model-registry-cost.test.js test/task-decomposition.test.js test/output-format-guard.test.js test/tier-fallback.test.js test/wrap.test.js test/init.test.js",
|
|
19
|
+
"test:unit": "DATABRICKS_API_KEY=test-key DATABRICKS_API_BASE=http://test.com node --test test/routing.test.js test/hybrid-routing-integration.test.js test/web-tools.test.js test/passthrough-mode.test.js test/openrouter-error-resilience.test.js test/format-conversion.test.js test/azure-openai-config.test.js test/azure-openai-format-conversion.test.js test/azure-openai-routing.test.js test/azure-openai-streaming.test.js test/azure-openai-error-resilience.test.js test/azure-openai-integration.test.js test/openai-integration.test.js test/toon-compression.test.js test/llamacpp-integration.test.js test/resilience.test.js test/telemetry-routing.test.js test/memory/store.test.js test/memory/surprise.test.js test/memory/extractor.test.js test/memory/search.test.js test/memory/retriever.test.js test/distill.test.js test/large-payload.test.js test/code-mode.test.js test/prompt-cache-injection.test.js test/risk-analyzer.test.js test/interaction-block.test.js test/preflight.test.js test/token-reduction.test.js test/session-affinity.test.js test/model-registry-cost.test.js test/task-decomposition.test.js test/output-format-guard.test.js test/tier-fallback.test.js test/wrap.test.js test/init.test.js test/agent-learning.test.js test/tool-call-response-metadata.test.js",
|
|
20
20
|
"test:memory": "DATABRICKS_API_KEY=test-key DATABRICKS_API_BASE=http://test.com node --test test/memory/store.test.js test/memory/surprise.test.js test/memory/extractor.test.js test/memory/search.test.js test/memory/retriever.test.js",
|
|
21
21
|
"test:new-features": "DATABRICKS_API_KEY=test-key DATABRICKS_API_BASE=http://test.com node --test test/passthrough-mode.test.js test/openrouter-error-resilience.test.js test/format-conversion.test.js",
|
|
22
22
|
"test:performance": "DATABRICKS_API_KEY=test-key DATABRICKS_API_BASE=http://test.com node test/hybrid-routing-performance.test.js && DATABRICKS_API_KEY=test-key DATABRICKS_API_BASE=http://test.com node test/performance-tests.js",
|
|
@@ -62,6 +62,14 @@ class ContextManager {
|
|
|
62
62
|
allowedTools: agentDef.allowedTools,
|
|
63
63
|
startTime: Date.now(),
|
|
64
64
|
|
|
65
|
+
// Original task prompt — consumed by the Reflector to infer task type.
|
|
66
|
+
taskPrompt,
|
|
67
|
+
|
|
68
|
+
// In-memory transcript mirror of the JSONL file — consumed by the
|
|
69
|
+
// Reflector for tool-usage / error-recovery analysis. The JSONL file
|
|
70
|
+
// remains the durable record; this array is the hot-path copy.
|
|
71
|
+
transcript: [],
|
|
72
|
+
|
|
65
73
|
// Token tracking
|
|
66
74
|
inputTokens: 0,
|
|
67
75
|
outputTokens: 0,
|
|
@@ -102,7 +110,7 @@ class ContextManager {
|
|
|
102
110
|
* Record tool execution in transcript
|
|
103
111
|
*/
|
|
104
112
|
recordToolCall(context, toolName, input, output, error = null) {
|
|
105
|
-
|
|
113
|
+
const entry = {
|
|
106
114
|
type: "tool_call",
|
|
107
115
|
agentId: context.agentId,
|
|
108
116
|
step: context.steps,
|
|
@@ -111,7 +119,15 @@ class ContextManager {
|
|
|
111
119
|
output: error ? null : output,
|
|
112
120
|
error: error ? error.message : null,
|
|
113
121
|
timestamp: Date.now()
|
|
114
|
-
}
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
// Keep the in-memory transcript in sync so the Reflector (which reads
|
|
125
|
+
// context.transcript) sees tool calls without re-parsing the JSONL file.
|
|
126
|
+
if (Array.isArray(context.transcript)) {
|
|
127
|
+
context.transcript.push(entry);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
this.writeTranscriptEntry(context.transcriptPath, entry);
|
|
115
131
|
}
|
|
116
132
|
|
|
117
133
|
/**
|
|
@@ -4,11 +4,17 @@ const yaml = require("js-yaml");
|
|
|
4
4
|
const logger = require("../../logger");
|
|
5
5
|
const Skillbook = require("../skillbook");
|
|
6
6
|
|
|
7
|
+
// How often to prune low-quality learned skills. 6 hours is frequent enough to
|
|
8
|
+
// keep skillbooks tidy on a long-running process but rare enough to be
|
|
9
|
+
// negligible overhead. Hardcoded on purpose — not worth a config knob.
|
|
10
|
+
const SKILL_PRUNE_INTERVAL_MS = 6 * 60 * 60 * 1000;
|
|
11
|
+
|
|
7
12
|
class AgentDefinitionLoader {
|
|
8
13
|
constructor() {
|
|
9
14
|
this.agents = new Map();
|
|
10
15
|
this.skillbooks = new Map(); // agentType → Skillbook
|
|
11
16
|
this.initialized = false;
|
|
17
|
+
this.pruneTimer = null;
|
|
12
18
|
|
|
13
19
|
// Initialize synchronously for compatibility
|
|
14
20
|
this.loadBuiltInAgentsSync();
|
|
@@ -82,6 +88,27 @@ class AgentDefinitionLoader {
|
|
|
82
88
|
return this.skillbooks.get(agentType);
|
|
83
89
|
}
|
|
84
90
|
|
|
91
|
+
/**
|
|
92
|
+
* Replace an agent's skillbook with a freshly-learned instance and re-inject
|
|
93
|
+
* its skills into the (in-memory) system prompt. Called by the executor after
|
|
94
|
+
* a run persists new skills, so learning takes effect within the running
|
|
95
|
+
* process instead of only after a restart. Injection rebuilds from
|
|
96
|
+
* originalSystemPrompt, so repeated calls do not duplicate the skills block.
|
|
97
|
+
*/
|
|
98
|
+
setSkillbook(agentType, skillbook) {
|
|
99
|
+
if (!agentType || !skillbook) return;
|
|
100
|
+
// Match the case-insensitive key an agent is actually stored under.
|
|
101
|
+
const key = this.agents.has(agentType)
|
|
102
|
+
? agentType
|
|
103
|
+
: Array.from(this.agents.keys()).find(
|
|
104
|
+
k => k.toLowerCase() === String(agentType).toLowerCase()
|
|
105
|
+
);
|
|
106
|
+
if (!key) return;
|
|
107
|
+
|
|
108
|
+
this.skillbooks.set(key, skillbook);
|
|
109
|
+
this._injectSkillsIntoPrompt(key);
|
|
110
|
+
}
|
|
111
|
+
|
|
85
112
|
/**
|
|
86
113
|
* Reload skillbooks and update prompts (call after learning)
|
|
87
114
|
*/
|
|
@@ -89,6 +116,69 @@ class AgentDefinitionLoader {
|
|
|
89
116
|
await this._loadSkillbooksAsync();
|
|
90
117
|
}
|
|
91
118
|
|
|
119
|
+
/**
|
|
120
|
+
* Prune low-quality skills from every loaded skillbook. Skillbook.prune()
|
|
121
|
+
* only drops skills that have been tried enough times to prove they don't
|
|
122
|
+
* help (default: useCount >= 3 && confidence < 0.2), so fresh skills are
|
|
123
|
+
* never removed. Persists and re-injects only the skillbooks that changed.
|
|
124
|
+
* @returns {Promise<number>} total skills pruned across all agents
|
|
125
|
+
*/
|
|
126
|
+
async pruneSkillbooks() {
|
|
127
|
+
let totalPruned = 0;
|
|
128
|
+
|
|
129
|
+
for (const [agentType, skillbook] of this.skillbooks.entries()) {
|
|
130
|
+
try {
|
|
131
|
+
const pruned = skillbook.prune();
|
|
132
|
+
if (pruned > 0) {
|
|
133
|
+
await skillbook.save();
|
|
134
|
+
this._injectSkillsIntoPrompt(agentType);
|
|
135
|
+
totalPruned += pruned;
|
|
136
|
+
}
|
|
137
|
+
} catch (error) {
|
|
138
|
+
logger.warn(
|
|
139
|
+
{ agentType, error: error.message },
|
|
140
|
+
"Failed to prune skillbook"
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (totalPruned > 0) {
|
|
146
|
+
logger.info({ totalPruned }, "Pruned low-quality agent skills");
|
|
147
|
+
}
|
|
148
|
+
return totalPruned;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Start periodic skillbook pruning. Idempotent; a non-positive interval
|
|
153
|
+
* disables pruning. The timer is unref'd so it never keeps the process alive.
|
|
154
|
+
*/
|
|
155
|
+
startSkillPruning(intervalMs = SKILL_PRUNE_INTERVAL_MS) {
|
|
156
|
+
if (this.pruneTimer) return; // already running
|
|
157
|
+
if (!Number.isInteger(intervalMs) || intervalMs <= 0) {
|
|
158
|
+
logger.debug({ intervalMs }, "Skillbook pruning disabled");
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
this.pruneTimer = setInterval(() => {
|
|
163
|
+
this.pruneSkillbooks().catch((error) => {
|
|
164
|
+
logger.warn({ error: error.message }, "Skillbook pruning failed");
|
|
165
|
+
});
|
|
166
|
+
}, intervalMs);
|
|
167
|
+
this.pruneTimer.unref();
|
|
168
|
+
|
|
169
|
+
logger.info({ intervalMs }, "Skillbook pruning started");
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Stop periodic skillbook pruning (for shutdown / tests).
|
|
174
|
+
*/
|
|
175
|
+
stopSkillPruning() {
|
|
176
|
+
if (this.pruneTimer) {
|
|
177
|
+
clearInterval(this.pruneTimer);
|
|
178
|
+
this.pruneTimer = null;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
92
182
|
/**
|
|
93
183
|
* Load built-in agents (Explore, Plan, General)
|
|
94
184
|
*/
|
package/src/agents/executor.js
CHANGED
|
@@ -9,6 +9,18 @@ const Reflector = require("./reflector");
|
|
|
9
9
|
const contextManager = new ContextManager();
|
|
10
10
|
|
|
11
11
|
class SubagentExecutor {
|
|
12
|
+
/**
|
|
13
|
+
* @param {Object} [options]
|
|
14
|
+
* @param {Object} [options.definitionLoader] - Agent definition loader. When
|
|
15
|
+
* provided, newly-learned skills are re-injected into agent prompts live
|
|
16
|
+
* (within the running process). Optional so the executor still works
|
|
17
|
+
* standalone (e.g. in tests); without it, learning still persists to disk
|
|
18
|
+
* and is picked up on the next process start.
|
|
19
|
+
*/
|
|
20
|
+
constructor({ definitionLoader = null } = {}) {
|
|
21
|
+
this.definitionLoader = definitionLoader;
|
|
22
|
+
}
|
|
23
|
+
|
|
12
24
|
/**
|
|
13
25
|
* Execute a single subagent
|
|
14
26
|
* @param {Object} agentDef - Agent definition
|
|
@@ -403,8 +415,12 @@ class SubagentExecutor {
|
|
|
403
415
|
return; // Nothing to learn
|
|
404
416
|
}
|
|
405
417
|
|
|
406
|
-
//
|
|
407
|
-
|
|
418
|
+
// Prefer the loader's live skillbook instance (so use-counts/confidence
|
|
419
|
+
// accumulate in one place); fall back to loading from disk when running
|
|
420
|
+
// standalone.
|
|
421
|
+
const skillbook =
|
|
422
|
+
this.definitionLoader?.getSkillbook(context.agentName) ||
|
|
423
|
+
(await Skillbook.load(context.agentName));
|
|
408
424
|
|
|
409
425
|
// Add each learned pattern
|
|
410
426
|
for (const pattern of patterns) {
|
|
@@ -414,6 +430,12 @@ class SubagentExecutor {
|
|
|
414
430
|
// Save skillbook (persists learning)
|
|
415
431
|
await skillbook.save();
|
|
416
432
|
|
|
433
|
+
// Re-inject into the agent's prompt so the next run benefits without a
|
|
434
|
+
// restart. No-op when no loader was provided.
|
|
435
|
+
if (this.definitionLoader) {
|
|
436
|
+
this.definitionLoader.setSkillbook(context.agentName, skillbook);
|
|
437
|
+
}
|
|
438
|
+
|
|
417
439
|
logger.info({
|
|
418
440
|
agentType: context.agentName,
|
|
419
441
|
patternsLearned: patterns.length,
|
package/src/agents/index.js
CHANGED
|
@@ -5,7 +5,15 @@ const ParallelCoordinator = require("./parallel-coordinator");
|
|
|
5
5
|
const agentStore = require("./store");
|
|
6
6
|
|
|
7
7
|
const definitionLoader = new AgentDefinitionLoader();
|
|
8
|
-
const coordinator = new ParallelCoordinator(config.agents?.maxConcurrent || 10
|
|
8
|
+
const coordinator = new ParallelCoordinator(config.agents?.maxConcurrent || 10, {
|
|
9
|
+
definitionLoader,
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
// Periodically drop learned skills that have proven unhelpful. Only when the
|
|
13
|
+
// agents subsystem is active; the timer is unref'd so it never blocks exit.
|
|
14
|
+
if (config.agents?.enabled) {
|
|
15
|
+
definitionLoader.startSkillPruning();
|
|
16
|
+
}
|
|
9
17
|
|
|
10
18
|
/**
|
|
11
19
|
* Spawn and execute subagent(s)
|
|
@@ -2,9 +2,9 @@ const logger = require("../logger");
|
|
|
2
2
|
const SubagentExecutor = require("./executor");
|
|
3
3
|
|
|
4
4
|
class ParallelCoordinator {
|
|
5
|
-
constructor(maxConcurrent = 10) {
|
|
5
|
+
constructor(maxConcurrent = 10, { definitionLoader = null } = {}) {
|
|
6
6
|
this.maxConcurrent = maxConcurrent;
|
|
7
|
-
this.executor = new SubagentExecutor();
|
|
7
|
+
this.executor = new SubagentExecutor({ definitionLoader });
|
|
8
8
|
}
|
|
9
9
|
|
|
10
10
|
/**
|
package/src/agents/reflector.js
CHANGED
|
@@ -184,8 +184,13 @@ class Reflector {
|
|
|
184
184
|
// Pattern 1: Recovered from errors
|
|
185
185
|
if (successful) {
|
|
186
186
|
const failedTools = errorEntries.map(e => e.toolName);
|
|
187
|
+
// Tools invoked *after* the last error entry are the recovery path.
|
|
188
|
+
// Use the entry's position in the transcript, not its timestamp
|
|
189
|
+
// (slicing by a ~1.7e12 timestamp always yielded []).
|
|
190
|
+
const lastErrorEntry = errorEntries[errorEntries.length - 1];
|
|
191
|
+
const lastErrorIndex = transcript.indexOf(lastErrorEntry);
|
|
187
192
|
const recoveryTools = transcript
|
|
188
|
-
.slice(
|
|
193
|
+
.slice(lastErrorIndex + 1)
|
|
189
194
|
.filter(e => e.type === "tool_call" && !e.error)
|
|
190
195
|
.map(e => e.toolName);
|
|
191
196
|
|
|
@@ -248,6 +253,11 @@ class Reflector {
|
|
|
248
253
|
* Infer task type from prompt
|
|
249
254
|
*/
|
|
250
255
|
static _inferTaskType(prompt) {
|
|
256
|
+
// Guard against a missing/non-string prompt so reflection never throws
|
|
257
|
+
// (a throw here previously aborted all learning silently).
|
|
258
|
+
if (typeof prompt !== "string" || prompt.length === 0) {
|
|
259
|
+
return null;
|
|
260
|
+
}
|
|
251
261
|
const lower = prompt.toLowerCase();
|
|
252
262
|
|
|
253
263
|
const taskTypes = [
|
|
@@ -2708,7 +2708,7 @@ async function invokeModel(body, options = {}) {
|
|
|
2708
2708
|
message_count: Array.isArray(body?.messages) ? body.messages.length : 0,
|
|
2709
2709
|
request_type: routingResult.analysis?.requestType || null,
|
|
2710
2710
|
provider: initialProvider,
|
|
2711
|
-
model: routingDecision.model,
|
|
2711
|
+
model: routingDecision.model ?? body._tierModel ?? null,
|
|
2712
2712
|
routing_method: routingDecision.method,
|
|
2713
2713
|
was_fallback: false,
|
|
2714
2714
|
output_tokens: outputTokens || null,
|
|
@@ -2826,7 +2826,7 @@ async function invokeModel(body, options = {}) {
|
|
|
2826
2826
|
message_count: Array.isArray(body?.messages) ? body.messages.length : 0,
|
|
2827
2827
|
request_type: routingResult.analysis?.requestType || null,
|
|
2828
2828
|
provider: initialProvider,
|
|
2829
|
-
model: routingDecision.model,
|
|
2829
|
+
model: routingDecision.model ?? body._tierModel ?? null,
|
|
2830
2830
|
routing_method: routingDecision.method,
|
|
2831
2831
|
was_fallback: false,
|
|
2832
2832
|
latency_ms: failLatency,
|
|
@@ -2934,7 +2934,7 @@ async function invokeModel(body, options = {}) {
|
|
|
2934
2934
|
message_count: Array.isArray(body?.messages) ? body.messages.length : 0,
|
|
2935
2935
|
request_type: routingResult.analysis?.requestType || null,
|
|
2936
2936
|
provider: fallbackProvider,
|
|
2937
|
-
model: routingDecision.model,
|
|
2937
|
+
model: routingDecision.model ?? body._tierModel ?? null,
|
|
2938
2938
|
routing_method: "fallback",
|
|
2939
2939
|
was_fallback: true,
|
|
2940
2940
|
output_tokens: fbOutputTokens || null,
|
|
@@ -2980,7 +2980,7 @@ async function invokeModel(body, options = {}) {
|
|
|
2980
2980
|
complexity_score: routingResult.score ?? null,
|
|
2981
2981
|
tier: routingDecision.tier,
|
|
2982
2982
|
provider: fallbackProvider,
|
|
2983
|
-
model: routingDecision.model,
|
|
2983
|
+
model: routingDecision.model ?? body._tierModel ?? null,
|
|
2984
2984
|
routing_method: "fallback",
|
|
2985
2985
|
was_fallback: true,
|
|
2986
2986
|
latency_ms: Date.now() - startTime,
|
|
@@ -1111,7 +1111,13 @@ function toAnthropicResponse(openai, requestedModel, wantsThinking) {
|
|
|
1111
1111
|
id: openai.id ?? `msg_${Date.now()}`,
|
|
1112
1112
|
type: "message",
|
|
1113
1113
|
role: "assistant",
|
|
1114
|
-
model
|
|
1114
|
+
// Prefer the model the provider actually served with; fall back to the
|
|
1115
|
+
// requested model only when the provider omits it. Mirrors the direct
|
|
1116
|
+
// (non-tool) path at `databricksResponse.json.model || requestedModel`, so
|
|
1117
|
+
// tool-call responses no longer report a stale/aliased client-request model.
|
|
1118
|
+
model: (typeof openai?.model === "string" && openai.model.trim())
|
|
1119
|
+
? openai.model
|
|
1120
|
+
: requestedModel,
|
|
1115
1121
|
content: contentItems,
|
|
1116
1122
|
stop_reason:
|
|
1117
1123
|
choice?.finish_reason === "stop"
|
|
@@ -1123,8 +1129,11 @@ function toAnthropicResponse(openai, requestedModel, wantsThinking) {
|
|
|
1123
1129
|
: choice?.finish_reason ?? "end_turn",
|
|
1124
1130
|
stop_sequence: null,
|
|
1125
1131
|
usage: {
|
|
1126
|
-
|
|
1127
|
-
output_tokens
|
|
1132
|
+
// Accept both OpenAI (prompt_tokens/completion_tokens) and
|
|
1133
|
+
// already-Anthropic (input_tokens/output_tokens) usage shapes so token
|
|
1134
|
+
// counts survive regardless of which provider/converter produced them.
|
|
1135
|
+
input_tokens: usage.prompt_tokens ?? usage.input_tokens ?? 0,
|
|
1136
|
+
output_tokens: usage.completion_tokens ?? usage.output_tokens ?? 0,
|
|
1128
1137
|
cache_creation_input_tokens: 0,
|
|
1129
1138
|
cache_read_input_tokens: 0,
|
|
1130
1139
|
},
|
|
@@ -4053,6 +4062,16 @@ async function processMessage({ payload, headers, session, cwd, options = {} })
|
|
|
4053
4062
|
anthropicPayload.usage.cache_read_input_tokens = promptTokens;
|
|
4054
4063
|
anthropicPayload.usage.cache_creation_input_tokens = 0;
|
|
4055
4064
|
|
|
4065
|
+
// Carry routing metadata on the cache-hit path too, so downstream model
|
|
4066
|
+
// name resolution (OpenClaw) behaves the same as the live loop path.
|
|
4067
|
+
if (cachedResponse.routingDecision) {
|
|
4068
|
+
anthropicPayload._routingMeta = {
|
|
4069
|
+
provider: cachedResponse.routingDecision.provider,
|
|
4070
|
+
model: cachedResponse.routingDecision.model,
|
|
4071
|
+
tier: cachedResponse.routingDecision.tier,
|
|
4072
|
+
};
|
|
4073
|
+
}
|
|
4074
|
+
|
|
4056
4075
|
appendTurnToSession(session, {
|
|
4057
4076
|
role: "assistant",
|
|
4058
4077
|
type: "message",
|
|
@@ -4147,4 +4166,6 @@ async function processMessage({ payload, headers, session, cwd, options = {} })
|
|
|
4147
4166
|
|
|
4148
4167
|
module.exports = {
|
|
4149
4168
|
processMessage,
|
|
4169
|
+
// Exported for unit testing of response-metadata conversion.
|
|
4170
|
+
toAnthropicResponse,
|
|
4150
4171
|
};
|
package/src/routing/telemetry.js
CHANGED
|
@@ -49,12 +49,27 @@ function init() {
|
|
|
49
49
|
}
|
|
50
50
|
|
|
51
51
|
try {
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
52
|
+
// Allow tests to redirect telemetry to an isolated DB so unit/integration
|
|
53
|
+
// runs never pollute the production .lynkr/telemetry.db that build-knn-index.js
|
|
54
|
+
// reads from. Empty string disables telemetry entirely.
|
|
55
|
+
const override = process.env.LYNKR_TELEMETRY_DB_PATH;
|
|
56
|
+
let dbPath;
|
|
57
|
+
if (override === "") {
|
|
58
|
+
logger.debug("Telemetry: LYNKR_TELEMETRY_DB_PATH is empty, telemetry disabled");
|
|
59
|
+
return false;
|
|
60
|
+
} else if (override) {
|
|
61
|
+
dbPath = path.resolve(override);
|
|
62
|
+
const overrideDir = path.dirname(dbPath);
|
|
63
|
+
if (!fs.existsSync(overrideDir)) {
|
|
64
|
+
fs.mkdirSync(overrideDir, { recursive: true });
|
|
65
|
+
}
|
|
66
|
+
} else {
|
|
67
|
+
const dbDir = path.resolve(process.cwd(), ".lynkr");
|
|
68
|
+
if (!fs.existsSync(dbDir)) {
|
|
69
|
+
fs.mkdirSync(dbDir, { recursive: true });
|
|
70
|
+
}
|
|
71
|
+
dbPath = path.join(dbDir, "telemetry.db");
|
|
55
72
|
}
|
|
56
|
-
|
|
57
|
-
const dbPath = path.join(dbDir, "telemetry.db");
|
|
58
73
|
db = new Database(dbPath, {
|
|
59
74
|
verbose: process.env.DEBUG_SQL ? console.log : null,
|
|
60
75
|
fileMustExist: false,
|