lynkr 9.9.1 → 9.11.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.
- package/README.md +50 -9
- package/bin/cli.js +2 -0
- package/bin/lynkr-init.js +11 -12
- package/bin/lynkr-reset.js +71 -0
- package/config/model-tiers.json +199 -52
- package/package.json +3 -3
- package/public/dashboard.html +29 -0
- package/scripts/validate-difficulty-classifier.js +27 -6
- package/src/api/providers-handler.js +0 -1
- package/src/api/router.js +309 -160
- package/src/clients/databricks.js +215 -71
- package/src/clients/gpt-utils.js +8 -1
- package/src/clients/ollama-utils.js +19 -10
- package/src/clients/prompt-cache-injection.js +93 -30
- package/src/config/index.js +82 -43
- package/src/context/compression.js +27 -8
- package/src/context/distill.js +2 -1
- package/src/dashboard/api.js +5 -1
- package/src/memory/distiller.js +350 -0
- package/src/memory/extractor.js +0 -8
- package/src/memory/format.js +17 -2
- package/src/memory/index.js +10 -0
- package/src/memory/retriever.js +7 -2
- package/src/memory/skills-cache.js +180 -0
- package/src/memory/tencentdb-launcher.js +459 -0
- package/src/memory/wiki.js +171 -0
- package/src/orchestrator/index.js +202 -1237
- package/src/orchestrator/passthrough-stream.js +382 -0
- package/src/orchestrator/sse-transformer.js +408 -0
- package/src/prompts/system.js +20 -6
- package/src/routing/affinity-store.js +85 -4
- package/src/routing/agentic-detector.js +9 -1
- package/src/routing/cache-economics.js +123 -0
- package/src/routing/cache-switch-cost.js +194 -0
- package/src/routing/calibration.js +8 -1
- package/src/routing/complexity-analyzer.js +7 -1
- package/src/routing/context-compressor.js +170 -0
- package/src/routing/difficulty-classifier.js +161 -17
- package/src/routing/index.js +196 -56
- package/src/routing/intent-score.js +20 -2
- package/src/routing/model-registry.js +35 -10
- package/src/routing/session-affinity.js +68 -1
- package/src/routing/side-channel-detector.js +103 -0
- package/src/routing/telemetry.js +139 -2
- package/src/routing/verifier.js +41 -2
- package/src/server.js +36 -49
- package/src/tools/code-graph.js +6 -4
- package/src/agents/context-manager.js +0 -236
- package/src/agents/decomposition/dispatcher.js +0 -185
- package/src/agents/decomposition/gate.js +0 -136
- package/src/agents/decomposition/index.js +0 -183
- package/src/agents/decomposition/model-call.js +0 -75
- package/src/agents/decomposition/planner.js +0 -223
- package/src/agents/decomposition/synthesizer.js +0 -89
- package/src/agents/decomposition/telemetry.js +0 -55
- package/src/agents/definitions/loader.js +0 -653
- package/src/agents/executor.js +0 -457
- package/src/agents/index.js +0 -165
- package/src/agents/parallel-coordinator.js +0 -68
- package/src/agents/reflector.js +0 -331
- package/src/agents/skillbook.js +0 -331
- package/src/agents/store.js +0 -259
- package/src/edits/index.js +0 -171
- package/src/indexer/babel-parser.js +0 -213
- package/src/indexer/index.js +0 -1629
- package/src/indexer/navigation/index.js +0 -32
- package/src/indexer/navigation/providers/treeSitter.js +0 -36
- package/src/indexer/parser.js +0 -443
- package/src/tasks/store.js +0 -349
- package/src/tests/coverage.js +0 -173
- package/src/tests/index.js +0 -171
- package/src/tests/store.js +0 -213
- package/src/tools/agent-task.js +0 -145
- package/src/tools/code-mode.js +0 -304
- package/src/tools/decompose.js +0 -91
- package/src/tools/edits.js +0 -94
- package/src/tools/execution.js +0 -171
- package/src/tools/git.js +0 -1346
- package/src/tools/index.js +0 -306
- package/src/tools/indexer.js +0 -360
- package/src/tools/lazy-loader.js +0 -366
- package/src/tools/mcp-remote.js +0 -88
- package/src/tools/mcp.js +0 -116
- package/src/tools/process.js +0 -167
- package/src/tools/smart-selection.js +0 -180
- package/src/tools/stubs.js +0 -55
- package/src/tools/tasks.js +0 -260
- package/src/tools/tests.js +0 -132
- package/src/tools/tinyfish.js +0 -358
- package/src/tools/truncate.js +0 -106
- package/src/tools/web-client.js +0 -71
- package/src/tools/web.js +0 -415
- package/src/tools/workspace.js +0 -204
|
@@ -1,236 +0,0 @@
|
|
|
1
|
-
const fs = require("fs");
|
|
2
|
-
const path = require("path");
|
|
3
|
-
const logger = require("../logger");
|
|
4
|
-
|
|
5
|
-
class ContextManager {
|
|
6
|
-
constructor() {
|
|
7
|
-
this.transcriptsDir = path.join(process.cwd(), "data", "agent-transcripts");
|
|
8
|
-
this.ensureTranscriptsDir();
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
ensureTranscriptsDir() {
|
|
12
|
-
if (!fs.existsSync(this.transcriptsDir)) {
|
|
13
|
-
fs.mkdirSync(this.transcriptsDir, { recursive: true });
|
|
14
|
-
}
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* Create fresh context for subagent
|
|
19
|
-
* @param {Object} agentDef - Agent definition
|
|
20
|
-
* @param {string} taskPrompt - Task from main agent
|
|
21
|
-
* @param {Object} mainContext - Minimal context from main agent
|
|
22
|
-
* @returns {Object} - Fresh context for subagent
|
|
23
|
-
*/
|
|
24
|
-
createSubagentContext(agentDef, taskPrompt, mainContext = {}) {
|
|
25
|
-
const agentId = this.generateAgentId();
|
|
26
|
-
const transcriptPath = path.join(this.transcriptsDir, `agent-${agentId}.jsonl`);
|
|
27
|
-
|
|
28
|
-
// Initialize transcript file
|
|
29
|
-
fs.writeFileSync(transcriptPath, "");
|
|
30
|
-
|
|
31
|
-
// Build minimal context (NOT full main agent history)
|
|
32
|
-
const messages = [];
|
|
33
|
-
|
|
34
|
-
// System prompt from agent definition
|
|
35
|
-
messages.push({
|
|
36
|
-
role: "system",
|
|
37
|
-
content: agentDef.systemPrompt
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
// Optional: Add minimal context from main agent
|
|
41
|
-
if (mainContext.relevant_context) {
|
|
42
|
-
messages.push({
|
|
43
|
-
role: "system",
|
|
44
|
-
content: `Context from main agent:\n${mainContext.relevant_context}`
|
|
45
|
-
});
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
// Task prompt
|
|
49
|
-
messages.push({
|
|
50
|
-
role: "user",
|
|
51
|
-
content: taskPrompt
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
const context = {
|
|
55
|
-
agentId,
|
|
56
|
-
agentName: agentDef.name,
|
|
57
|
-
transcriptPath,
|
|
58
|
-
messages,
|
|
59
|
-
steps: 0,
|
|
60
|
-
maxSteps: agentDef.maxSteps,
|
|
61
|
-
model: agentDef.model,
|
|
62
|
-
allowedTools: agentDef.allowedTools,
|
|
63
|
-
startTime: Date.now(),
|
|
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
|
-
|
|
73
|
-
// Token tracking
|
|
74
|
-
inputTokens: 0,
|
|
75
|
-
outputTokens: 0,
|
|
76
|
-
|
|
77
|
-
// State
|
|
78
|
-
terminated: false,
|
|
79
|
-
result: null
|
|
80
|
-
};
|
|
81
|
-
|
|
82
|
-
this.writeTranscriptEntry(transcriptPath, {
|
|
83
|
-
type: "agent_start",
|
|
84
|
-
agentId,
|
|
85
|
-
agentName: agentDef.name,
|
|
86
|
-
taskPrompt,
|
|
87
|
-
timestamp: Date.now()
|
|
88
|
-
});
|
|
89
|
-
|
|
90
|
-
logger.info({ agentId, agentName: agentDef.name }, "Created fresh subagent context");
|
|
91
|
-
|
|
92
|
-
return context;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
/**
|
|
96
|
-
* Add message to subagent context
|
|
97
|
-
*/
|
|
98
|
-
addMessage(context, message) {
|
|
99
|
-
context.messages.push(message);
|
|
100
|
-
|
|
101
|
-
this.writeTranscriptEntry(context.transcriptPath, {
|
|
102
|
-
type: "message",
|
|
103
|
-
agentId: context.agentId,
|
|
104
|
-
message,
|
|
105
|
-
timestamp: Date.now()
|
|
106
|
-
});
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
/**
|
|
110
|
-
* Record tool execution in transcript
|
|
111
|
-
*/
|
|
112
|
-
recordToolCall(context, toolName, input, output, error = null) {
|
|
113
|
-
const entry = {
|
|
114
|
-
type: "tool_call",
|
|
115
|
-
agentId: context.agentId,
|
|
116
|
-
step: context.steps,
|
|
117
|
-
toolName,
|
|
118
|
-
input,
|
|
119
|
-
output: error ? null : output,
|
|
120
|
-
error: error ? error.message : null,
|
|
121
|
-
timestamp: Date.now()
|
|
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);
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
/**
|
|
134
|
-
* Complete subagent execution
|
|
135
|
-
*/
|
|
136
|
-
completeExecution(context, result) {
|
|
137
|
-
context.terminated = true;
|
|
138
|
-
context.result = result;
|
|
139
|
-
|
|
140
|
-
this.writeTranscriptEntry(context.transcriptPath, {
|
|
141
|
-
type: "agent_complete",
|
|
142
|
-
agentId: context.agentId,
|
|
143
|
-
result,
|
|
144
|
-
stats: {
|
|
145
|
-
steps: context.steps,
|
|
146
|
-
durationMs: Date.now() - context.startTime,
|
|
147
|
-
inputTokens: context.inputTokens,
|
|
148
|
-
outputTokens: context.outputTokens
|
|
149
|
-
},
|
|
150
|
-
timestamp: Date.now()
|
|
151
|
-
});
|
|
152
|
-
|
|
153
|
-
logger.info({
|
|
154
|
-
agentId: context.agentId,
|
|
155
|
-
steps: context.steps,
|
|
156
|
-
durationMs: Date.now() - context.startTime
|
|
157
|
-
}, "Subagent execution completed");
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
/**
|
|
161
|
-
* Fail subagent execution
|
|
162
|
-
*/
|
|
163
|
-
failExecution(context, error) {
|
|
164
|
-
context.terminated = true;
|
|
165
|
-
|
|
166
|
-
this.writeTranscriptEntry(context.transcriptPath, {
|
|
167
|
-
type: "agent_failed",
|
|
168
|
-
agentId: context.agentId,
|
|
169
|
-
error: error.message,
|
|
170
|
-
stats: {
|
|
171
|
-
steps: context.steps,
|
|
172
|
-
durationMs: Date.now() - context.startTime
|
|
173
|
-
},
|
|
174
|
-
timestamp: Date.now()
|
|
175
|
-
});
|
|
176
|
-
|
|
177
|
-
logger.error({
|
|
178
|
-
agentId: context.agentId,
|
|
179
|
-
error: error.message
|
|
180
|
-
}, "Subagent execution failed");
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
/**
|
|
184
|
-
* Write entry to transcript file (JSONL format)
|
|
185
|
-
*/
|
|
186
|
-
writeTranscriptEntry(transcriptPath, entry) {
|
|
187
|
-
try {
|
|
188
|
-
fs.appendFileSync(transcriptPath, JSON.stringify(entry) + "\n");
|
|
189
|
-
} catch (error) {
|
|
190
|
-
logger.warn({ error: error.message }, "Failed to write transcript entry");
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
/**
|
|
195
|
-
* Read transcript for debugging
|
|
196
|
-
*/
|
|
197
|
-
readTranscript(agentId) {
|
|
198
|
-
const transcriptPath = path.join(this.transcriptsDir, `agent-${agentId}.jsonl`);
|
|
199
|
-
|
|
200
|
-
if (!fs.existsSync(transcriptPath)) {
|
|
201
|
-
return null;
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
const lines = fs.readFileSync(transcriptPath, "utf8").split("\n").filter(l => l.trim());
|
|
205
|
-
return lines.map(line => JSON.parse(line));
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
/**
|
|
209
|
-
* Generate unique agent ID
|
|
210
|
-
*/
|
|
211
|
-
generateAgentId() {
|
|
212
|
-
return `${Date.now()}_${Math.random().toString(36).slice(2, 10)}`;
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
/**
|
|
216
|
-
* Clean old transcripts (older than 7 days)
|
|
217
|
-
*/
|
|
218
|
-
cleanOldTranscripts() {
|
|
219
|
-
const maxAge = 7 * 24 * 60 * 60 * 1000; // 7 days
|
|
220
|
-
const now = Date.now();
|
|
221
|
-
|
|
222
|
-
const files = fs.readdirSync(this.transcriptsDir);
|
|
223
|
-
|
|
224
|
-
for (const file of files) {
|
|
225
|
-
const filePath = path.join(this.transcriptsDir, file);
|
|
226
|
-
const stats = fs.statSync(filePath);
|
|
227
|
-
|
|
228
|
-
if (now - stats.mtimeMs > maxAge) {
|
|
229
|
-
fs.unlinkSync(filePath);
|
|
230
|
-
logger.debug({ file }, "Cleaned old transcript");
|
|
231
|
-
}
|
|
232
|
-
}
|
|
233
|
-
}
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
module.exports = ContextManager;
|
|
@@ -1,185 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Subtask dispatcher (Phase 3).
|
|
3
|
-
*
|
|
4
|
-
* Executes a validated plan respecting its dependency DAG:
|
|
5
|
-
* - subtasks are grouped into topological "levels" (Kahn's algorithm)
|
|
6
|
-
* - subtasks in the same level have no dependency on each other → run in
|
|
7
|
-
* parallel via the existing ParallelCoordinator (spawnParallel)
|
|
8
|
-
* - a subtask receives ONLY its own prompt plus the compressed results of the
|
|
9
|
-
* subtasks it depends on (context isolation — the token win)
|
|
10
|
-
*
|
|
11
|
-
* The spawn functions are injectable for testing.
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
const logger = require("../../logger");
|
|
15
|
-
|
|
16
|
-
// Cap how much of a dependency's result we forward, to preserve the
|
|
17
|
-
// context-isolation savings (subagents already return summaries; this bounds
|
|
18
|
-
// pathological cases).
|
|
19
|
-
const MAX_CONTEXT_CHARS = 2000;
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* Group subtasks into dependency levels. Returns array of arrays of ids.
|
|
23
|
-
* Throws if the graph is unresolvable (should not happen — planner validated).
|
|
24
|
-
*/
|
|
25
|
-
function topologicalLevels(subtasks) {
|
|
26
|
-
const byId = new Map(subtasks.map((s) => [s.id, s]));
|
|
27
|
-
const indegree = new Map(subtasks.map((s) => [s.id, 0]));
|
|
28
|
-
const dependents = new Map(subtasks.map((s) => [s.id, []]));
|
|
29
|
-
|
|
30
|
-
for (const s of subtasks) {
|
|
31
|
-
for (const dep of s.dependsOn) {
|
|
32
|
-
if (!byId.has(dep)) continue;
|
|
33
|
-
indegree.set(s.id, indegree.get(s.id) + 1);
|
|
34
|
-
dependents.get(dep).push(s.id);
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
const levels = [];
|
|
39
|
-
let frontier = subtasks.filter((s) => indegree.get(s.id) === 0).map((s) => s.id);
|
|
40
|
-
const resolved = new Set();
|
|
41
|
-
|
|
42
|
-
while (frontier.length > 0) {
|
|
43
|
-
levels.push(frontier);
|
|
44
|
-
const next = [];
|
|
45
|
-
for (const id of frontier) {
|
|
46
|
-
resolved.add(id);
|
|
47
|
-
for (const child of dependents.get(id)) {
|
|
48
|
-
indegree.set(child, indegree.get(child) - 1);
|
|
49
|
-
if (indegree.get(child) === 0) next.push(child);
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
frontier = next;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
if (resolved.size !== subtasks.length) {
|
|
56
|
-
throw new Error("Unresolvable subtask graph (cycle or dangling dependency)");
|
|
57
|
-
}
|
|
58
|
-
return levels;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
function compressResult(text) {
|
|
62
|
-
if (typeof text !== "string") return String(text ?? "");
|
|
63
|
-
if (text.length <= MAX_CONTEXT_CHARS) return text;
|
|
64
|
-
return text.slice(0, MAX_CONTEXT_CHARS) + "\n…[truncated]";
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
function buildContextForSubtask(subtask, resultsById) {
|
|
68
|
-
if (!subtask.dependsOn || subtask.dependsOn.length === 0) return null;
|
|
69
|
-
const parts = [];
|
|
70
|
-
for (const dep of subtask.dependsOn) {
|
|
71
|
-
const r = resultsById.get(dep);
|
|
72
|
-
if (r && r.success && r.result) {
|
|
73
|
-
parts.push(`Result of subtask ${dep}:\n${compressResult(r.result)}`);
|
|
74
|
-
} else if (r) {
|
|
75
|
-
parts.push(`Subtask ${dep} did not complete successfully.`);
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
return parts.length > 0 ? parts.join("\n\n") : null;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
/**
|
|
82
|
-
* Dispatch a validated plan.
|
|
83
|
-
* @param {Object} plan - { subtasks: [...] }
|
|
84
|
-
* @param {Object} [options]
|
|
85
|
-
* @param {string} [options.sessionId]
|
|
86
|
-
* @param {string} [options.cwd]
|
|
87
|
-
* @param {Function} [options.spawnParallel] - (agentTypes[], prompts[], opts) => results[]
|
|
88
|
-
* @returns {Promise<{results: Array, levels: Array, stats: Object}>}
|
|
89
|
-
*/
|
|
90
|
-
async function dispatchPlan(plan, options = {}) {
|
|
91
|
-
const spawnParallel = options.spawnParallel || require("../index").spawnParallel;
|
|
92
|
-
const subtasks = plan.subtasks;
|
|
93
|
-
const byId = new Map(subtasks.map((s) => [s.id, s]));
|
|
94
|
-
const levels = topologicalLevels(subtasks);
|
|
95
|
-
const resultsById = new Map();
|
|
96
|
-
|
|
97
|
-
let totalInputTokens = 0;
|
|
98
|
-
let totalOutputTokens = 0;
|
|
99
|
-
let totalSubagents = 0;
|
|
100
|
-
|
|
101
|
-
for (let li = 0; li < levels.length; li++) {
|
|
102
|
-
const levelIds = levels[li];
|
|
103
|
-
const levelSubtasks = levelIds.map((id) => byId.get(id));
|
|
104
|
-
|
|
105
|
-
const agentTypes = levelSubtasks.map((s) => s.agentType);
|
|
106
|
-
const prompts = levelSubtasks.map((s) => s.prompt);
|
|
107
|
-
const perTaskContext = levelSubtasks.map((s) => buildContextForSubtask(s, resultsById));
|
|
108
|
-
|
|
109
|
-
logger.info(
|
|
110
|
-
{ level: li, count: levelIds.length, ids: levelIds },
|
|
111
|
-
"[Decomposition] Dispatching subtask level"
|
|
112
|
-
);
|
|
113
|
-
|
|
114
|
-
// spawnParallel shares one options object; pass per-task context by spawning
|
|
115
|
-
// the level as individual parallel calls when contexts differ.
|
|
116
|
-
const levelResults = await runLevel(
|
|
117
|
-
spawnParallel,
|
|
118
|
-
agentTypes,
|
|
119
|
-
prompts,
|
|
120
|
-
perTaskContext,
|
|
121
|
-
options
|
|
122
|
-
);
|
|
123
|
-
|
|
124
|
-
levelResults.forEach((res, idx) => {
|
|
125
|
-
const st = levelSubtasks[idx];
|
|
126
|
-
totalSubagents += 1;
|
|
127
|
-
totalInputTokens += res?.stats?.inputTokens || 0;
|
|
128
|
-
totalOutputTokens += res?.stats?.outputTokens || 0;
|
|
129
|
-
resultsById.set(st.id, {
|
|
130
|
-
id: st.id,
|
|
131
|
-
agentType: st.agentType,
|
|
132
|
-
success: !!res?.success,
|
|
133
|
-
result: res?.success ? res.result : null,
|
|
134
|
-
error: res?.success ? null : res?.error || "unknown error",
|
|
135
|
-
stats: res?.stats || {},
|
|
136
|
-
});
|
|
137
|
-
});
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
const results = subtasks.map((s) => resultsById.get(s.id));
|
|
141
|
-
return {
|
|
142
|
-
results,
|
|
143
|
-
levels,
|
|
144
|
-
stats: {
|
|
145
|
-
subagents: totalSubagents,
|
|
146
|
-
inputTokens: totalInputTokens,
|
|
147
|
-
outputTokens: totalOutputTokens,
|
|
148
|
-
},
|
|
149
|
-
};
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
/**
|
|
153
|
-
* Run one level. When subtasks in the level have differing injected contexts we
|
|
154
|
-
* spawn them as separate parallel calls (each with its own mainContext), then
|
|
155
|
-
* await all. When none need context, a single spawnParallel batch is used.
|
|
156
|
-
*/
|
|
157
|
-
async function runLevel(spawnParallel, agentTypes, prompts, perTaskContext, options) {
|
|
158
|
-
const anyContext = perTaskContext.some((c) => c);
|
|
159
|
-
|
|
160
|
-
if (!anyContext) {
|
|
161
|
-
return spawnParallel(agentTypes, prompts, {
|
|
162
|
-
sessionId: options.sessionId,
|
|
163
|
-
cwd: options.cwd,
|
|
164
|
-
});
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
// Mixed/with-context: one spawnParallel call per subtask so each gets its own
|
|
168
|
-
// mainContext, executed concurrently.
|
|
169
|
-
const calls = agentTypes.map((type, i) =>
|
|
170
|
-
spawnParallel([type], [prompts[i]], {
|
|
171
|
-
sessionId: options.sessionId,
|
|
172
|
-
cwd: options.cwd,
|
|
173
|
-
mainContext: perTaskContext[i] ? { relevant_context: perTaskContext[i] } : undefined,
|
|
174
|
-
}).then((arr) => arr[0])
|
|
175
|
-
);
|
|
176
|
-
return Promise.all(calls);
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
module.exports = {
|
|
180
|
-
dispatchPlan,
|
|
181
|
-
topologicalLevels,
|
|
182
|
-
buildContextForSubtask,
|
|
183
|
-
compressResult,
|
|
184
|
-
MAX_CONTEXT_CHARS,
|
|
185
|
-
};
|
|
@@ -1,136 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Decomposition gate (Phase 1).
|
|
3
|
-
*
|
|
4
|
-
* Decides whether breaking a task into isolated-context subtasks is actually
|
|
5
|
-
* worth it. This is the make-or-break of the feature: naive "decompose
|
|
6
|
-
* everything" loses money, because every subagent carries fixed overhead
|
|
7
|
-
* (planning + per-agent handoff/summarisation). Decomposition only pays off
|
|
8
|
-
* when the task is (a) genuinely complex, (b) large enough to amortise that
|
|
9
|
-
* overhead, and (c) divisible into reasonably independent units.
|
|
10
|
-
*
|
|
11
|
-
* Pure and synchronous so it can be unit-tested without a model. The caller
|
|
12
|
-
* supplies a pre-computed complexity `analysis` (from routing/complexity-analyzer)
|
|
13
|
-
* and the raw payload.
|
|
14
|
-
*/
|
|
15
|
-
|
|
16
|
-
const DEFAULTS = {
|
|
17
|
-
minComplexity: 60, // 0-100; only decompose genuinely complex work
|
|
18
|
-
minTokens: 3000, // estimated monolithic tokens; below this the overhead wins
|
|
19
|
-
minIndependentUnits: 2, // need at least 2 separable pieces to bother
|
|
20
|
-
maxSubtasks: 6,
|
|
21
|
-
};
|
|
22
|
-
|
|
23
|
-
const ENUMERATION_RE = /^\s*(?:[-*+]|\d+[.)]|step\s+\d+\b)/gim;
|
|
24
|
-
const CONJUNCTION_RE = /\b(?:and then|then|also|additionally|as well as|after that|finally|next,)\b/gi;
|
|
25
|
-
const IMPERATIVE_RE = /\b(?:add|create|build|implement|write|refactor|update|fix|remove|delete|migrate|test|document|configure|set up|wire|integrate|generate)\b/gi;
|
|
26
|
-
const FILE_PATH_RE = /\b[\w./-]+\.(?:js|ts|tsx|jsx|py|go|rs|java|rb|c|cpp|h|json|yaml|yml|md|sql|sh|css|html)\b/gi;
|
|
27
|
-
|
|
28
|
-
function _uniqueMatches(text, re) {
|
|
29
|
-
const set = new Set();
|
|
30
|
-
const matches = text.match(re) || [];
|
|
31
|
-
for (const m of matches) set.add(m.toLowerCase().trim());
|
|
32
|
-
return set;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* Heuristically estimate how many independent units a task contains.
|
|
37
|
-
* Conservative: takes the strongest of several weak signals rather than summing
|
|
38
|
-
* them, so a single rambling sentence doesn't look like five subtasks.
|
|
39
|
-
* @param {string} text
|
|
40
|
-
* @returns {number}
|
|
41
|
-
*/
|
|
42
|
-
function estimateIndependentUnits(text) {
|
|
43
|
-
if (!text || typeof text !== "string") return 1;
|
|
44
|
-
|
|
45
|
-
const enumerated = (text.match(ENUMERATION_RE) || []).length;
|
|
46
|
-
const conjunctions = (text.match(CONJUNCTION_RE) || []).length;
|
|
47
|
-
const imperatives = _uniqueMatches(text, IMPERATIVE_RE).size;
|
|
48
|
-
const files = _uniqueMatches(text, FILE_PATH_RE).size;
|
|
49
|
-
|
|
50
|
-
// Each signal is an independent lower-bound estimate of separable units.
|
|
51
|
-
const signals = [
|
|
52
|
-
enumerated, // explicit list items
|
|
53
|
-
conjunctions + 1, // "do A and then B" → 2 units
|
|
54
|
-
imperatives, // distinct action verbs
|
|
55
|
-
files, // distinct files usually map to distinct work
|
|
56
|
-
];
|
|
57
|
-
|
|
58
|
-
const estimate = Math.max(...signals, 1);
|
|
59
|
-
return estimate;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
/**
|
|
63
|
-
* Decide whether to decompose.
|
|
64
|
-
* @param {Object} analysis - result of analyzeComplexity(payload)
|
|
65
|
-
* @param {Object} payload - the request payload
|
|
66
|
-
* @param {Object} [options]
|
|
67
|
-
* @param {Object} [options.config] - threshold overrides (see DEFAULTS)
|
|
68
|
-
* @param {string} [options.riskLevel] - 'low'|'medium'|'high'; 'high' disables decomposition
|
|
69
|
-
* @param {string} [options.taskText] - explicit task text (else derived from analysis/payload)
|
|
70
|
-
* @returns {{ decompose: boolean, reason: string, signals: Object }}
|
|
71
|
-
*/
|
|
72
|
-
function shouldDecompose(analysis, payload = {}, options = {}) {
|
|
73
|
-
const cfg = { ...DEFAULTS, ...(options.config || {}) };
|
|
74
|
-
|
|
75
|
-
const score = Number(analysis?.score ?? 0);
|
|
76
|
-
const estimatedTokens = Number(
|
|
77
|
-
analysis?.breakdown?.tokens?.estimated ?? options.estimatedTokens ?? 0
|
|
78
|
-
);
|
|
79
|
-
|
|
80
|
-
const taskText =
|
|
81
|
-
options.taskText ||
|
|
82
|
-
analysis?.content ||
|
|
83
|
-
_firstUserText(payload) ||
|
|
84
|
-
"";
|
|
85
|
-
|
|
86
|
-
const independentUnits = estimateIndependentUnits(taskText);
|
|
87
|
-
|
|
88
|
-
const signals = {
|
|
89
|
-
score,
|
|
90
|
-
estimatedTokens,
|
|
91
|
-
independentUnits,
|
|
92
|
-
riskLevel: options.riskLevel || "low",
|
|
93
|
-
thresholds: cfg,
|
|
94
|
-
};
|
|
95
|
-
|
|
96
|
-
// Never decompose high-risk work — keep it in one capable context where the
|
|
97
|
-
// exempt-from-laziness concerns (validation/security) stay coherent.
|
|
98
|
-
if (options.riskLevel === "high") {
|
|
99
|
-
return { decompose: false, reason: "high_risk_skip", signals };
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
if (score < cfg.minComplexity) {
|
|
103
|
-
return { decompose: false, reason: "below_complexity_threshold", signals };
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
if (estimatedTokens < cfg.minTokens) {
|
|
107
|
-
return { decompose: false, reason: "too_small_to_amortise_overhead", signals };
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
if (independentUnits < cfg.minIndependentUnits) {
|
|
111
|
-
return { decompose: false, reason: "not_divisible", signals };
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
return { decompose: true, reason: "decompose_worthwhile", signals };
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
function _firstUserText(payload) {
|
|
118
|
-
const messages = payload?.messages;
|
|
119
|
-
if (!Array.isArray(messages)) return "";
|
|
120
|
-
const user = [...messages].reverse().find((m) => m.role === "user");
|
|
121
|
-
if (!user) return "";
|
|
122
|
-
if (typeof user.content === "string") return user.content;
|
|
123
|
-
if (Array.isArray(user.content)) {
|
|
124
|
-
return user.content
|
|
125
|
-
.filter((b) => b?.type === "text" || typeof b?.text === "string")
|
|
126
|
-
.map((b) => b.text || "")
|
|
127
|
-
.join("\n");
|
|
128
|
-
}
|
|
129
|
-
return "";
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
module.exports = {
|
|
133
|
-
shouldDecompose,
|
|
134
|
-
estimateIndependentUnits,
|
|
135
|
-
DEFAULTS,
|
|
136
|
-
};
|