dsh-omni-router 2.2.0 → 2.3.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 +6 -1
- package/README.zh-CN.md +6 -1
- package/package.json +2 -2
- package/src/agent-runtime.mjs +13 -2
- package/src/capability-performance.mjs +24 -0
- package/src/context-capsule.mjs +113 -0
- package/src/mission-dag.mjs +7 -4
- package/src/omni-router.mjs +131 -0
- package/src/progressive-intelligence.mjs +85 -0
- package/src/strategy-shift.mjs +34 -0
- package/test/agent-runtime.test.mjs +12 -0
- package/test/capability-performance.test.mjs +20 -0
- package/test/context-capsule.test.mjs +53 -0
- package/test/progressive-intelligence.test.mjs +39 -0
- package/test/strategy-shift.test.mjs +28 -0
package/README.md
CHANGED
|
@@ -3,6 +3,10 @@
|
|
|
3
3
|
> GitHub: https://github.com/qwe225380/dsh-omni-router
|
|
4
4
|
> [中文说明](./README.zh-CN.md)
|
|
5
5
|
|
|
6
|
+
> **Fast by default. Smart when needed. Proven when done.**
|
|
7
|
+
> **Decide · Compose · Verify**
|
|
8
|
+
> Install it. Keep coding.
|
|
9
|
+
|
|
6
10
|
**Omni** 是一个 DeepSeek Harness 的 **reliability and orchestration control plane**,定位为 Intelligence Runtime:把复杂度路由、任务编译、项目上下文、能力编排、Mission DAG、Runtime 预算、证据验证、Memory/Learning、Benchmark 组合成一个统一的、自动运行的工程闭环。
|
|
7
11
|
|
|
8
12
|
## Why Omni Router?
|
|
@@ -74,6 +78,7 @@
|
|
|
74
78
|
- **P1/P3 能力**:Real capability sandbox(已接入 `omni_mission_run`,按 role + capability 生成 toolFilter)、Capability Resolver v2(risk/reliability 综合打分)、OmniBench v2 runner(manifest schema + prepare + prompt generation + `--exec` 本地执行/结果采集 + DAG-to-plan compile)。
|
|
75
79
|
- **2.1 收敛增强**:Mission Resume(`omni_mission_resume` 跨 session 继续 DAG,`runDagLoop` 支持 `onProgress` 周期保存)、Harness evidence extraction(`extractHarnessEvidence` 从 commands/tests/toolCalls/EVIDENCE_JSON 提取机器可验证证据)、context token budget(`maxContextTokens` 真正限制检索输出)、semantic failure retrieval + evidence-backed skill distillation、OmniBench v2 real runner。
|
|
76
80
|
- **2.2 Capability Auto-Provisioning(优化7)**:Capability Auditor(baseline/task-time audit)、Gap Detection、Plugin/Skill Quality Scoring、Minimal Capability Set Solver、Discovery Adapters(marketplace/hub/static registry)、Trusted Auto Provisioning(recommend/auto-trusted/manual)、Post-install probe、Install rollback transaction、Capability performance learning / redundancy detection。
|
|
81
|
+
- **2.3 Progressive Intelligence(优化8)**:L0 Direct / L1 Assisted / L2 Orchestrated / L3 Guarded 四级渐进智能;Context Capsule(最小相关上下文 + 动态 needs);Strategy Shift(相同失败策略自动换策略,不再盲目 retry);`omni_explain` / `omni_doctor` 用户入口。
|
|
77
82
|
- **TaskDecision**:`createTaskDecision` 生成唯一决策对象,Policy/Runtime 统一消费,避免重复 classify 分叉。
|
|
78
83
|
- **Evidence Protocol**:`src/evidence.mjs` 用结构化 command/file/test/finding 证据判定 PASS/FAIL;`isQaPass` 已支持从 QA 输出解析 JSON evidence,不再只信文本。
|
|
79
84
|
- **Task Compiler**:`compileTask` 生成 objective / constraints / non-goals / acceptance / hidden assumptions / ambiguities / invariants / risk / artifacts;`compileTaskWithLLM` 可用 LLM 增强。
|
|
@@ -155,7 +160,7 @@ Then restart DSH and select **Omni Router** in a new session.
|
|
|
155
160
|
- `/omni direct` — enter direct mode.
|
|
156
161
|
- `/omni mode spec|react|balanced` — set thinking mode.
|
|
157
162
|
- `/omni reroute plan|direct` — adaptively reroute current task.
|
|
158
|
-
- Model tools: `omni_status`, `omni_plan`, `omni_direct`, `omni_mode`, `omni_reroute`, `omni_delegate`, `omni_memory`, `omni_benchmark`, `omni_mission_run`, `omni_mission_resume`, `omni_capability_audit`, `omni_capability_provision`, `omni_capability_probe`, `omni_visual_check`.
|
|
163
|
+
- Model tools: `omni_status`, `omni_explain`, `omni_doctor`, `omni_plan`, `omni_direct`, `omni_mode`, `omni_reroute`, `omni_delegate`, `omni_memory`, `omni_benchmark`, `omni_mission_run`, `omni_mission_resume`, `omni_capability_audit`, `omni_capability_provision`, `omni_capability_probe`, `omni_capability_performance`, `omni_visual_check`.
|
|
159
164
|
|
|
160
165
|
## Configuration
|
|
161
166
|
|
package/README.zh-CN.md
CHANGED
|
@@ -3,6 +3,10 @@
|
|
|
3
3
|
> GitHub:https://github.com/qwe225380/dsh-omni-router
|
|
4
4
|
> [English](./README.md)
|
|
5
5
|
|
|
6
|
+
> **默认快,必要时聪明,完成必须有证据。**
|
|
7
|
+
> **Decide · Compose · Verify**
|
|
8
|
+
> 安装即可使用,继续写代码。
|
|
9
|
+
|
|
6
10
|
**Omni** 是一个 DeepSeek Harness 的 **reliability and orchestration control plane**,定位为 Intelligence Runtime:把复杂度路由、任务编译、项目上下文、能力编排、Mission DAG、Runtime 预算、证据验证、Memory/Learning、Benchmark 组合成一个统一的、自动运行的工程闭环。
|
|
7
11
|
|
|
8
12
|
## 为什么需要 Omni Router?
|
|
@@ -74,6 +78,7 @@
|
|
|
74
78
|
- **P1/P3 能力**:Real capability sandbox(已接入 `omni_mission_run`,按 role + capability 生成 toolFilter)、Capability Resolver v2(risk/reliability 综合打分)、OmniBench v2 runner(manifest schema + prepare + prompt generation + `--exec` 本地执行/结果采集 + DAG-to-plan compile)。
|
|
75
79
|
- **2.1 收敛增强**:Mission Resume(`omni_mission_resume` 跨 session 继续 DAG,`runDagLoop` 支持 `onProgress` 周期保存)、Harness evidence extraction(`extractHarnessEvidence` 从 commands/tests/toolCalls/EVIDENCE_JSON 提取机器可验证证据)、context token budget(`maxContextTokens` 真正限制检索输出)、semantic failure retrieval + evidence-backed skill distillation、OmniBench v2 real runner。
|
|
76
80
|
- **2.2 Capability Auto-Provisioning(优化7)**:Capability Auditor(baseline/task-time audit)、Gap Detection、Plugin/Skill Quality Scoring、Minimal Capability Set Solver、Discovery Adapters(marketplace/hub/static registry)、Trusted Auto Provisioning(recommend/auto-trusted/manual)、Post-install probe、Install rollback transaction、Capability performance learning / redundancy detection。
|
|
81
|
+
- **2.3 Progressive Intelligence(优化8)**:L0 Direct / L1 Assisted / L2 Orchestrated / L3 Guarded 四级渐进智能;Context Capsule(最小相关上下文 + 动态 needs);Strategy Shift(相同失败策略自动换策略,不再盲目 retry);`omni_explain` / `omni_doctor` 用户入口。
|
|
77
82
|
- **TaskDecision**:`createTaskDecision` 生成唯一决策对象,Policy/Runtime 统一消费,避免重复 classify 分叉。
|
|
78
83
|
- **Evidence Protocol**:`src/evidence.mjs` 用结构化 command/file/test/finding 证据判定 PASS/FAIL;`isQaPass` 已支持从 QA 输出解析 JSON evidence,不再只信文本。
|
|
79
84
|
- **Task Compiler**:`compileTask` 生成 objective / constraints / non-goals / acceptance / hidden assumptions / ambiguities / invariants / risk / artifacts;`compileTaskWithLLM` 可用 LLM 增强。
|
|
@@ -155,7 +160,7 @@ node scripts/install-preset.mjs
|
|
|
155
160
|
- `/omni direct` — 进入直接执行模式。
|
|
156
161
|
- `/omni mode spec|react|balanced` — 设置思维模式。
|
|
157
162
|
- `/omni reroute plan|direct` — 动态切换当前任务路由。
|
|
158
|
-
- 模型工具:`omni_status` / `omni_plan` / `omni_direct` / `omni_mode` / `omni_reroute` / `omni_delegate` / `omni_memory` / `omni_benchmark` / `omni_mission_run` / `omni_mission_resume` / `omni_capability_audit` / `omni_capability_provision` / `omni_capability_probe` / `omni_visual_check`。
|
|
163
|
+
- 模型工具:`omni_status` / `omni_explain` / `omni_doctor` / `omni_plan` / `omni_direct` / `omni_mode` / `omni_reroute` / `omni_delegate` / `omni_memory` / `omni_benchmark` / `omni_mission_run` / `omni_mission_resume` / `omni_capability_audit` / `omni_capability_provision` / `omni_capability_probe` / `omni_capability_performance` / `omni_visual_check`。
|
|
159
164
|
|
|
160
165
|
## 配置
|
|
161
166
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-omni-router",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.3.0",
|
|
4
4
|
"description": "Omni - a reliability and orchestration control plane for DeepSeek Harness, designed to turn fast models into dependable long-horizon software-engineering agents.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./lib/index.js",
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
}
|
|
42
42
|
},
|
|
43
43
|
"scripts": {
|
|
44
|
-
"test": "node test/omni-router.test.mjs && node test/agent-chain.test.mjs && node test/skill-suggest.test.mjs && node test/methodology.test.mjs && node test/compat.test.mjs && node test/project-brain.test.mjs && node test/mission-planner.test.mjs && node test/agent-runtime.test.mjs && node test/judge.test.mjs && node test/memory.test.mjs && node test/memory-v3.test.mjs && node test/memory-engine.test.mjs && node test/mission-resume.test.mjs && node test/engineering-benchmark.test.mjs && node test/benchmark-runner.test.mjs && node test/benchmark-results.test.mjs && node test/continuous-eval.test.mjs && node --no-warnings test/project-brain-v2.test.mjs && node --no-warnings test/project-brain-v3.test.mjs && node test/visual-qa.test.mjs && node test/evidence.test.mjs && node test/evidence-store.test.mjs && node test/evidence-engine.test.mjs && node test/task-decision.test.mjs && node test/task-compiler.test.mjs && node test/capability-brain.test.mjs && node test/capability-manifest.test.mjs && node test/capability-sandbox.test.mjs && node test/capability-auditor.test.mjs && node test/capability-quality.test.mjs && node test/capability-solver.test.mjs && node test/capability-provisioner.test.mjs && node test/capability-performance.test.mjs && node test/planner-dag.test.mjs && node test/dynamic-context.test.mjs && node test/mission-dag.test.mjs && node test/failure-taxonomy.test.mjs && node test/context-expansion.test.mjs && node test/hybrid-retrieval.test.mjs && node test/dependency-graph.test.mjs && node test/ast-provider.test.mjs && node test/experience-router.test.mjs && node test/benchmark-tasks.test.mjs && node test/bundle-installer.test.mjs && node test/omnibench-runner.test.mjs",
|
|
44
|
+
"test": "node test/omni-router.test.mjs && node test/agent-chain.test.mjs && node test/skill-suggest.test.mjs && node test/methodology.test.mjs && node test/compat.test.mjs && node test/project-brain.test.mjs && node test/mission-planner.test.mjs && node test/agent-runtime.test.mjs && node test/judge.test.mjs && node test/memory.test.mjs && node test/memory-v3.test.mjs && node test/memory-engine.test.mjs && node test/mission-resume.test.mjs && node test/engineering-benchmark.test.mjs && node test/benchmark-runner.test.mjs && node test/benchmark-results.test.mjs && node test/continuous-eval.test.mjs && node --no-warnings test/project-brain-v2.test.mjs && node --no-warnings test/project-brain-v3.test.mjs && node test/visual-qa.test.mjs && node test/evidence.test.mjs && node test/evidence-store.test.mjs && node test/evidence-engine.test.mjs && node test/task-decision.test.mjs && node test/task-compiler.test.mjs && node test/capability-brain.test.mjs && node test/capability-manifest.test.mjs && node test/capability-sandbox.test.mjs && node test/capability-auditor.test.mjs && node test/capability-quality.test.mjs && node test/capability-solver.test.mjs && node test/capability-provisioner.test.mjs && node test/capability-performance.test.mjs && node test/progressive-intelligence.test.mjs && node test/context-capsule.test.mjs && node test/strategy-shift.test.mjs && node test/planner-dag.test.mjs && node test/dynamic-context.test.mjs && node test/mission-dag.test.mjs && node test/failure-taxonomy.test.mjs && node test/context-expansion.test.mjs && node test/hybrid-retrieval.test.mjs && node test/dependency-graph.test.mjs && node test/ast-provider.test.mjs && node test/experience-router.test.mjs && node test/benchmark-tasks.test.mjs && node test/bundle-installer.test.mjs && node test/omnibench-runner.test.mjs",
|
|
45
45
|
"benchmark": "node benchmark/run.mjs",
|
|
46
46
|
"benchmark:engineering": "node benchmark/engineering-run.mjs",
|
|
47
47
|
"benchmark:compare": "node benchmark/compare.mjs",
|
package/src/agent-runtime.mjs
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
|
|
12
12
|
import { buildPhaseTasks, decideReplan } from './mission-planner.mjs'
|
|
13
13
|
import { applyObservationToDag, getReadyTasks, isMissionDagComplete, markTaskDone, selectReadyBatch } from './mission-dag.mjs'
|
|
14
|
+
import { nextStrategy, shouldShiftStrategy } from './strategy-shift.mjs'
|
|
14
15
|
|
|
15
16
|
export function createRuntimeState(mission, options = {}) {
|
|
16
17
|
const phases = mission?.phases || []
|
|
@@ -187,6 +188,7 @@ export async function runDagLoop(dag, {
|
|
|
187
188
|
let cost = 0
|
|
188
189
|
let toolCalls = 0
|
|
189
190
|
const actions = []
|
|
191
|
+
const failureHistory = new Map()
|
|
190
192
|
const startTime = Date.now()
|
|
191
193
|
|
|
192
194
|
const statusFromBudget = () => {
|
|
@@ -219,12 +221,21 @@ export async function runDagLoop(dag, {
|
|
|
219
221
|
tokenUsage += Number(result?.tokenUsage || 0)
|
|
220
222
|
cost += Number(result?.cost || 0)
|
|
221
223
|
toolCalls += Number(result?.toolCalls || 0)
|
|
222
|
-
const isFailure = observation?.type === 'test_failure' || observation?.type === 'build_failure'
|
|
224
|
+
const isFailure = observation?.type === 'test_failure' || observation?.type === 'build_failure' || observation?.type === 'strategy_shift'
|
|
223
225
|
if (isFailure) {
|
|
224
226
|
replanCount += 1
|
|
225
227
|
repairCount += 1
|
|
226
228
|
sameActionCount = sameActionCount + 1
|
|
227
|
-
|
|
229
|
+
const logicalKey = task.logicalId || task.id
|
|
230
|
+
const history = failureHistory.get(logicalKey) || []
|
|
231
|
+
history.push({ category: observation.type, reason: observation.reason, hypothesis: observation.hypothesis })
|
|
232
|
+
failureHistory.set(logicalKey, history)
|
|
233
|
+
let appliedObservation = { ...observation, taskId: task.id }
|
|
234
|
+
if (shouldShiftStrategy(history)) {
|
|
235
|
+
appliedObservation = { type: 'strategy_shift', reason: nextStrategy(history), taskId: task.id }
|
|
236
|
+
failureHistory.delete(logicalKey)
|
|
237
|
+
}
|
|
238
|
+
current = applyObservationToDag(current, appliedObservation, task.id)
|
|
228
239
|
} else {
|
|
229
240
|
sameActionCount = 0
|
|
230
241
|
current = markTaskDone(current, task.id, observation)
|
|
@@ -6,12 +6,36 @@
|
|
|
6
6
|
* demotion/removal decisions and benchmark plugin-contribution analysis.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
+
import fs from 'node:fs'
|
|
10
|
+
import path from 'node:path'
|
|
11
|
+
|
|
9
12
|
export function createPerformanceRegistry(initial = {}) {
|
|
10
13
|
return {
|
|
11
14
|
providers: { ...(initial.providers || {}) },
|
|
12
15
|
}
|
|
13
16
|
}
|
|
14
17
|
|
|
18
|
+
export function performanceRegistryPath(cwd) {
|
|
19
|
+
return path.join(cwd, '.omni', 'capability-performance.json')
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function loadPerformanceRegistry(cwd) {
|
|
23
|
+
const file = performanceRegistryPath(cwd)
|
|
24
|
+
if (!fs.existsSync(file)) return createPerformanceRegistry()
|
|
25
|
+
try {
|
|
26
|
+
return createPerformanceRegistry(JSON.parse(fs.readFileSync(file, 'utf8')))
|
|
27
|
+
} catch {
|
|
28
|
+
return createPerformanceRegistry()
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function savePerformanceRegistry(cwd, registry) {
|
|
33
|
+
const file = performanceRegistryPath(cwd)
|
|
34
|
+
fs.mkdirSync(path.dirname(file), { recursive: true })
|
|
35
|
+
fs.writeFileSync(file, JSON.stringify(registry, null, 2), 'utf8')
|
|
36
|
+
return file
|
|
37
|
+
}
|
|
38
|
+
|
|
15
39
|
export function recordProvisionOutcome(registry, providerId, outcome = {}) {
|
|
16
40
|
const previous = registry.providers[providerId] || {
|
|
17
41
|
runs: 0,
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Context Capsule.
|
|
3
|
+
*
|
|
4
|
+
* Gives a weak model the smallest useful slice of context for the current
|
|
5
|
+
* task: a few relevant files, their callers/callees, related tests, invariants,
|
|
6
|
+
* and recent failures. This is the opposite of dumping the whole repository
|
|
7
|
+
* map into the prompt.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const STOP_WORDS = new Set([
|
|
11
|
+
'the', 'and', 'for', 'with', 'that', 'this', 'from', 'are', 'was', 'were',
|
|
12
|
+
'not', 'but', 'you', 'your', 'fix', 'add', 'make', 'use', 'should', 'when',
|
|
13
|
+
])
|
|
14
|
+
|
|
15
|
+
export function buildContextCapsule(taskText, options = {}) {
|
|
16
|
+
const entries = options.entries || []
|
|
17
|
+
const files = options.files || {}
|
|
18
|
+
const graph = options.graph || {}
|
|
19
|
+
const needs = options.needs || []
|
|
20
|
+
const invariants = options.invariants || []
|
|
21
|
+
const failures = options.failures || []
|
|
22
|
+
const maxFiles = Number(options.maxFiles) || 6
|
|
23
|
+
const maxTotalChars = Number(options.maxTotalChars) || 8000
|
|
24
|
+
const maxFileChars = Number(options.maxFileChars) || 1200
|
|
25
|
+
|
|
26
|
+
const keywords = extractKeywords([taskText, ...needs])
|
|
27
|
+
const fileNames = entries.filter((e) => e.type === 'file').map((e) => e.name)
|
|
28
|
+
const scored = fileNames.map((name) => {
|
|
29
|
+
const content = files[name] || ''
|
|
30
|
+
const nameScore = scoreText(name, keywords)
|
|
31
|
+
const contentScore = content ? scoreText(content.slice(0, 2000), keywords) * 0.5 : 0
|
|
32
|
+
return { name, score: nameScore + contentScore }
|
|
33
|
+
}).sort((a, b) => b.score - a.score)
|
|
34
|
+
const selected = scored.slice(0, maxFiles).map((s) => s.name)
|
|
35
|
+
|
|
36
|
+
const lines = []
|
|
37
|
+
lines.push(`Task: ${taskText}`)
|
|
38
|
+
if (needs.length) lines.push(`Needs: ${needs.join('; ')}`)
|
|
39
|
+
if (selected.length) {
|
|
40
|
+
lines.push('', 'Relevant files:')
|
|
41
|
+
for (const name of selected) {
|
|
42
|
+
const content = files[name] || ''
|
|
43
|
+
lines.push(`--- ${name} ---`)
|
|
44
|
+
lines.push(content ? content.slice(0, maxFileChars) : '(no content indexed)')
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const edgeLines = []
|
|
49
|
+
for (const name of selected) {
|
|
50
|
+
for (const edge of graph[name] || []) {
|
|
51
|
+
edgeLines.push(`${name} ${edge.kind || '->'} ${edge.to || edge}`)
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
if (edgeLines.length) {
|
|
55
|
+
lines.push('', 'Callers / callees:')
|
|
56
|
+
lines.push(...edgeLines.slice(0, 20))
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const tests = fileNames.filter((n) =>
|
|
60
|
+
/\.(test|spec)\./i.test(n) &&
|
|
61
|
+
(selected.includes(n) || keywords.some((k) => n.toLowerCase().includes(k))),
|
|
62
|
+
)
|
|
63
|
+
if (tests.length) {
|
|
64
|
+
lines.push('', 'Tests:')
|
|
65
|
+
lines.push(...tests.map((t) => `- ${t}`))
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (invariants.length) {
|
|
69
|
+
lines.push('', 'Invariants:')
|
|
70
|
+
lines.push(...invariants.map((i) => `- ${i}`))
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (failures.length) {
|
|
74
|
+
lines.push('', 'Recent failures:')
|
|
75
|
+
lines.push(...failures.map((f) => `- ${f}`))
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
let text = lines.join('\n')
|
|
79
|
+
if (text.length > maxTotalChars) {
|
|
80
|
+
text = `${text.slice(0, maxTotalChars)}\n… (context capsule truncated)`
|
|
81
|
+
}
|
|
82
|
+
return text
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function expandContextCapsule(current, request, options = {}) {
|
|
86
|
+
const requestText = Array.isArray(request) ? request.join('; ') : String(request || '')
|
|
87
|
+
if (!requestText) return current
|
|
88
|
+
const extra = buildContextCapsule(requestText, {
|
|
89
|
+
...options,
|
|
90
|
+
maxFiles: Math.max(Number(options.maxFiles) || 6, 3),
|
|
91
|
+
maxTotalChars: Math.max(Number(options.maxTotalChars) || 8000, 4000),
|
|
92
|
+
})
|
|
93
|
+
return `${current}\n\n--- additional context requested ---\n${extra}`
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function extractKeywords(texts = []) {
|
|
97
|
+
const words = new Set()
|
|
98
|
+
for (const text of texts) {
|
|
99
|
+
for (const match of String(text || '').toLowerCase().match(/[a-z0-9_]+/g) || []) {
|
|
100
|
+
if (match.length > 2 && !STOP_WORDS.has(match)) words.add(match)
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return [...words]
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function scoreText(text, keywords) {
|
|
107
|
+
const lower = String(text || '').toLowerCase()
|
|
108
|
+
let score = 0
|
|
109
|
+
for (const keyword of keywords) {
|
|
110
|
+
if (lower.includes(keyword)) score += 1
|
|
111
|
+
}
|
|
112
|
+
return score
|
|
113
|
+
}
|
package/src/mission-dag.mjs
CHANGED
|
@@ -103,7 +103,7 @@ export function insertAfter(dag, afterId, task) {
|
|
|
103
103
|
|
|
104
104
|
export function applyObservationToDag(dag, observation = {}, failedTaskId = null) {
|
|
105
105
|
const type = observation.type || ''
|
|
106
|
-
if (type === 'test_failure' || type === 'build_failure') {
|
|
106
|
+
if (type === 'test_failure' || type === 'build_failure' || type === 'strategy_shift') {
|
|
107
107
|
const taskId = failedTaskId || observation.taskId || null
|
|
108
108
|
const failedTask = taskId ? dag.tasks.find((t) => t.id === taskId) : null
|
|
109
109
|
const baseId = taskId || 'T'
|
|
@@ -114,13 +114,16 @@ export function applyObservationToDag(dag, observation = {}, failedTaskId = null
|
|
|
114
114
|
const tasks = (dag.tasks || []).map((t) => t.id === taskId ? { ...t, status: 'failed', attempt, failure: observation } : t)
|
|
115
115
|
const doneIds = tasks.filter((t) => t.status === 'done').map((t) => t.id)
|
|
116
116
|
const failure = classifyFailure(observation)
|
|
117
|
+
const isShift = type === 'strategy_shift'
|
|
117
118
|
const repair = createTask({
|
|
118
119
|
id: repairId,
|
|
119
|
-
goal:
|
|
120
|
+
goal: isShift
|
|
121
|
+
? `Strategy shift: ${observation.reason || 're-investigate with broader context and fresh capabilities'}`
|
|
122
|
+
: `Diagnose and repair: ${failure.category} — ${failure.recovery}`,
|
|
120
123
|
dependencies: doneIds,
|
|
121
|
-
acceptance: ['verification passes
|
|
124
|
+
acceptance: ['root cause is re-diagnosed and verification passes'],
|
|
122
125
|
verification: ['run failing checks again'],
|
|
123
|
-
requiredCapabilities: ['debugging'],
|
|
126
|
+
requiredCapabilities: isShift ? ['debugging', 'repository.search'] : ['debugging'],
|
|
124
127
|
forbiddenCapabilities: ['source.write'],
|
|
125
128
|
})
|
|
126
129
|
// Repair tasks should be allowed to write; fix forbidden.
|
package/src/omni-router.mjs
CHANGED
|
@@ -37,6 +37,8 @@ import { loadCapabilityManifests } from './capability-manifest.mjs'
|
|
|
37
37
|
import { capabilityToolFilter } from './capability-sandbox.mjs'
|
|
38
38
|
import { baselineAudit, formatCapabilityAudit, taskTimeAudit } from './capability-auditor.mjs'
|
|
39
39
|
import { createStaticRegistryAdapter, discoverCandidates, evaluateProvisionPlan, formatProvisionResult, probeCapability, provisionCapabilities } from './capability-provisioner.mjs'
|
|
40
|
+
import { evaluateProviderValue, formatPerformanceRegistry, loadPerformanceRegistry, recommendDemotion, recordProvisionOutcome, savePerformanceRegistry } from './capability-performance.mjs'
|
|
41
|
+
import { decideIntelligenceLevel, formatIntelligenceLevel } from './progressive-intelligence.mjs'
|
|
40
42
|
import { buildProgressiveContext } from './context-expansion.mjs'
|
|
41
43
|
import { buildDynamicContext } from './dynamic-context.mjs'
|
|
42
44
|
import { classifyFailure } from './failure-taxonomy.mjs'
|
|
@@ -1010,11 +1012,20 @@ export function apply(ctx, config = {}) {
|
|
|
1010
1012
|
const agent = agentFor(session)
|
|
1011
1013
|
const state = states.get(session.id) || { kind: null, taskType: null, thinkingMode: null, riskLevel: null, firstText: null, planRequested: false, directOverride: false }
|
|
1012
1014
|
const routerStandard = agent ? isRouterStandardAvailable(ctx.get('tools') || ctx.tools, agent) : false
|
|
1015
|
+
const taskDecision = createTaskDecision({
|
|
1016
|
+
taskText: state.firstText || '',
|
|
1017
|
+
taskType: state.taskType || 'other',
|
|
1018
|
+
complexity: state.kind === 'plan' ? 'plan' : state.kind === 'direct' ? 'direct' : 'balanced',
|
|
1019
|
+
risk: state.riskLevel || 'low',
|
|
1020
|
+
thinkingMode: state.thinkingMode || 'balanced',
|
|
1021
|
+
})
|
|
1022
|
+
const intelligence = decideIntelligenceLevel(taskDecision)
|
|
1013
1023
|
return [
|
|
1014
1024
|
`omni-router: ${state.kind || 'unclassified'}`,
|
|
1015
1025
|
`taskType=${state.taskType || 'unknown'}`,
|
|
1016
1026
|
`thinkingMode=${state.thinkingMode || 'balanced'}`,
|
|
1017
1027
|
`riskLevel=${state.riskLevel || 'unknown'}`,
|
|
1028
|
+
`intelligenceLevel=${formatIntelligenceLevel(intelligence)}`,
|
|
1018
1029
|
`planRequested=${state.planRequested}`,
|
|
1019
1030
|
`directOverride=${state.directOverride}`,
|
|
1020
1031
|
`routerStandard=${routerStandard ? 'delegated' : 'not-detected'}`,
|
|
@@ -1022,6 +1033,69 @@ export function apply(ctx, config = {}) {
|
|
|
1022
1033
|
},
|
|
1023
1034
|
})
|
|
1024
1035
|
|
|
1036
|
+
registerTool({
|
|
1037
|
+
name: 'omni_explain',
|
|
1038
|
+
description: 'Explain why Omni chose the current mode, capabilities, verification level, and what is needed for completion.',
|
|
1039
|
+
parameters: {
|
|
1040
|
+
type: 'object',
|
|
1041
|
+
properties: {
|
|
1042
|
+
topic: { type: 'string', description: 'Optional topic to focus on: mode, capabilities, verification, completion' },
|
|
1043
|
+
},
|
|
1044
|
+
},
|
|
1045
|
+
execute(args) {
|
|
1046
|
+
const session = currentSession()
|
|
1047
|
+
if (!session) return 'no agent session'
|
|
1048
|
+
const state = states.get(session.id) || {}
|
|
1049
|
+
const decision = createTaskDecision({
|
|
1050
|
+
taskText: state.firstText || '',
|
|
1051
|
+
taskType: state.taskType || 'other',
|
|
1052
|
+
complexity: state.kind === 'plan' ? 'plan' : state.kind === 'direct' ? 'direct' : 'balanced',
|
|
1053
|
+
risk: state.riskLevel || 'low',
|
|
1054
|
+
thinkingMode: state.thinkingMode || 'balanced',
|
|
1055
|
+
})
|
|
1056
|
+
const intelligence = decideIntelligenceLevel(decision)
|
|
1057
|
+
const topic = String(args?.topic || '').toLowerCase()
|
|
1058
|
+
const lines = [
|
|
1059
|
+
`Mode: ${intelligence.level} ${intelligence.label}`,
|
|
1060
|
+
`Why: complexity=${decision.complexity}, risk=${decision.risk}, taskType=${decision.type}`,
|
|
1061
|
+
`Reasoning effort: ${intelligence.reasoningEffort}`,
|
|
1062
|
+
`Verification: ${intelligence.verification}`,
|
|
1063
|
+
`Approval required: ${intelligence.approvalRequired ? 'yes' : 'no'}`,
|
|
1064
|
+
`Capabilities needed: ${(decision.evidenceRequirements || []).join(', ') || 'native tools'}`,
|
|
1065
|
+
`Completion requires: ${decision.evidenceRequirements?.length ? 'harness evidence for ' + decision.evidenceRequirements.join(', ') : 'light verification'}`,
|
|
1066
|
+
]
|
|
1067
|
+
if (topic === 'mode' || topic === 'capabilities' || topic === 'verification' || topic === 'completion') {
|
|
1068
|
+
const idx = lines.findIndex((l) => l.toLowerCase().startsWith(topic))
|
|
1069
|
+
return idx >= 0 ? lines[idx] : lines.join('\n')
|
|
1070
|
+
}
|
|
1071
|
+
return lines.join('\n')
|
|
1072
|
+
},
|
|
1073
|
+
})
|
|
1074
|
+
|
|
1075
|
+
registerTool({
|
|
1076
|
+
name: 'omni_doctor',
|
|
1077
|
+
description: 'Run a quick environment/capability/project-index health check.',
|
|
1078
|
+
parameters: {},
|
|
1079
|
+
async execute() {
|
|
1080
|
+
const session = currentSession()
|
|
1081
|
+
const toolsService = ctx.get('tools') || ctx.tools
|
|
1082
|
+
const toolNames = await collectToolNames(toolsService)
|
|
1083
|
+
let brain = autoPopulateCapabilityBrain(createCapabilityBrain(), toolNames)
|
|
1084
|
+
brain = loadCapabilityManifests(brain, config.capabilityManifests || [])
|
|
1085
|
+
const baseline = baselineAudit(brain)
|
|
1086
|
+
const fs = ctx.get('fs') || ctx.fs
|
|
1087
|
+
const lines = [
|
|
1088
|
+
`DSH session: ${session ? 'ok' : 'missing'}`,
|
|
1089
|
+
`Tools registered: ${toolNames.length}`,
|
|
1090
|
+
`Baseline capability coverage: ${baseline.available.length}/${baseline.required.length}`,
|
|
1091
|
+
`Missing baseline: ${baseline.missing.join(', ') || '(none)'}`,
|
|
1092
|
+
`ProjectIndex: ${fs ? 'available' : 'unavailable'}`,
|
|
1093
|
+
`Evidence hooks: ${ctx.get('evidence') || ctx.evidence ? 'available' : 'not-detected'}`,
|
|
1094
|
+
]
|
|
1095
|
+
return lines.join('\n')
|
|
1096
|
+
},
|
|
1097
|
+
})
|
|
1098
|
+
|
|
1025
1099
|
registerTool({
|
|
1026
1100
|
name: 'omni_memory',
|
|
1027
1101
|
description: 'View or update Omni session memory. Actions: status, add (with type=project|decision|failure|trajectory and text), clear.',
|
|
@@ -1600,6 +1674,63 @@ export function apply(ctx, config = {}) {
|
|
|
1600
1674
|
},
|
|
1601
1675
|
})
|
|
1602
1676
|
|
|
1677
|
+
registerTool({
|
|
1678
|
+
name: 'omni_capability_performance',
|
|
1679
|
+
description: 'Track and inspect whether provisioned plugins/skills measurably improve task success, false completion, tokens, or tool errors.',
|
|
1680
|
+
parameters: {
|
|
1681
|
+
type: 'object',
|
|
1682
|
+
properties: {
|
|
1683
|
+
action: { type: 'string', enum: ['list', 'record', 'recommend'], description: 'Action (default list)' },
|
|
1684
|
+
provider: { type: 'string', description: 'Provider id' },
|
|
1685
|
+
successBefore: { type: 'number', description: 'Task success before provisioning' },
|
|
1686
|
+
successAfter: { type: 'number', description: 'Task success after provisioning' },
|
|
1687
|
+
falseCompletionBefore: { type: 'number', description: 'False completion rate before' },
|
|
1688
|
+
falseCompletionAfter: { type: 'number', description: 'False completion rate after' },
|
|
1689
|
+
tokensBefore: { type: 'number', description: 'Token usage before' },
|
|
1690
|
+
tokensAfter: { type: 'number', description: 'Token usage after' },
|
|
1691
|
+
toolErrorsBefore: { type: 'number', description: 'Tool errors before' },
|
|
1692
|
+
toolErrorsAfter: { type: 'number', description: 'Tool errors after' },
|
|
1693
|
+
usageDays: { type: 'number', description: 'Days since last use (for recommend)' },
|
|
1694
|
+
uniqueCapabilities: { type: 'number', description: 'Number of unique capabilities (for recommend)' },
|
|
1695
|
+
coveredBy: { type: 'array', items: { type: 'string' }, description: 'Other providers covering the same capabilities (for recommend)' },
|
|
1696
|
+
},
|
|
1697
|
+
required: [],
|
|
1698
|
+
},
|
|
1699
|
+
async execute(args) {
|
|
1700
|
+
const session = currentSession()
|
|
1701
|
+
const cwd = session?.meta?.cwd || session?.header?.cwd
|
|
1702
|
+
if (!cwd) return 'No workspace cwd found.'
|
|
1703
|
+
const action = args?.action || 'list'
|
|
1704
|
+
let registry = loadPerformanceRegistry(cwd)
|
|
1705
|
+
if (action === 'record') {
|
|
1706
|
+
if (!args?.provider) return 'provider is required for record.'
|
|
1707
|
+
registry = recordProvisionOutcome(registry, args.provider, {
|
|
1708
|
+
successBefore: args.successBefore,
|
|
1709
|
+
successAfter: args.successAfter,
|
|
1710
|
+
falseCompletionBefore: args.falseCompletionBefore,
|
|
1711
|
+
falseCompletionAfter: args.falseCompletionAfter,
|
|
1712
|
+
tokensBefore: args.tokensBefore,
|
|
1713
|
+
tokensAfter: args.tokensAfter,
|
|
1714
|
+
toolErrorsBefore: args.toolErrorsBefore,
|
|
1715
|
+
toolErrorsAfter: args.toolErrorsAfter,
|
|
1716
|
+
})
|
|
1717
|
+
const file = savePerformanceRegistry(cwd, registry)
|
|
1718
|
+
const value = evaluateProviderValue(registry.providers[args.provider])
|
|
1719
|
+
return `Recorded ${args.provider}: ${value.label} (${value.value})\nSaved: ${file}`
|
|
1720
|
+
}
|
|
1721
|
+
if (action === 'recommend') {
|
|
1722
|
+
if (!args?.provider) return 'provider is required for recommend.'
|
|
1723
|
+
const rec = recommendDemotion(registry, args.provider, {
|
|
1724
|
+
usageDays: Number(args.usageDays) || 0,
|
|
1725
|
+
uniqueCapabilities: Number(args.uniqueCapabilities) || 0,
|
|
1726
|
+
coveredBy: Array.isArray(args.coveredBy) ? args.coveredBy : [],
|
|
1727
|
+
})
|
|
1728
|
+
return `${rec.providerId}: ${rec.recommendation} — ${rec.reason}`
|
|
1729
|
+
}
|
|
1730
|
+
return formatPerformanceRegistry(registry)
|
|
1731
|
+
},
|
|
1732
|
+
})
|
|
1733
|
+
|
|
1603
1734
|
registerTool({
|
|
1604
1735
|
name: 'omni_mission_run',
|
|
1605
1736
|
description: 'Run a Mission Planner loop with real subagents: Observe → Think → Act → Replan until completed or maxSteps.',
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Progressive Intelligence.
|
|
3
|
+
*
|
|
4
|
+
* Omni should be invisible for simple tasks and gradually unfold for hard
|
|
5
|
+
* ones. This module maps a TaskDecision to one of four run levels:
|
|
6
|
+
*
|
|
7
|
+
* L0 Direct — raw DSH + light verification
|
|
8
|
+
* L1 Assisted — context + skill + test
|
|
9
|
+
* L2 Orchestrated — plan + DAG + subagents + evidence
|
|
10
|
+
* L3 Guarded — deep plan + approval + independent verify + rollback
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
export const INTELLIGENCE_LEVELS = ['L0', 'L1', 'L2', 'L3']
|
|
14
|
+
|
|
15
|
+
export function decideIntelligenceLevel(decision = {}) {
|
|
16
|
+
const complexity = decision.complexity || 'direct'
|
|
17
|
+
const risk = decision.risk || 'low'
|
|
18
|
+
const taskType = decision.type || decision.taskType || 'other'
|
|
19
|
+
|
|
20
|
+
if (risk === 'high' || risk === 'critical') {
|
|
21
|
+
return {
|
|
22
|
+
level: 'L3',
|
|
23
|
+
label: 'Guarded',
|
|
24
|
+
useDag: true,
|
|
25
|
+
useSubagents: true,
|
|
26
|
+
approvalRequired: true,
|
|
27
|
+
verification: 'full',
|
|
28
|
+
independentVerify: true,
|
|
29
|
+
reasoningEffort: 'max',
|
|
30
|
+
contextMaxFiles: 20,
|
|
31
|
+
maxTaskAdditions: 1,
|
|
32
|
+
description: 'High-risk task: deep plan, approval, independent verify, rollback.',
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (complexity === 'plan' || (['feature', 'refactor'].includes(taskType) && complexity !== 'direct')) {
|
|
37
|
+
return {
|
|
38
|
+
level: 'L2',
|
|
39
|
+
label: 'Orchestrated',
|
|
40
|
+
useDag: true,
|
|
41
|
+
useSubagents: true,
|
|
42
|
+
approvalRequired: false,
|
|
43
|
+
verification: 'full',
|
|
44
|
+
independentVerify: false,
|
|
45
|
+
reasoningEffort: 'high',
|
|
46
|
+
contextMaxFiles: 12,
|
|
47
|
+
maxTaskAdditions: 1,
|
|
48
|
+
description: 'Multi-file or complex task: DAG + subagents + evidence.',
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (complexity === 'balanced' || ['bugfix', 'test'].includes(taskType)) {
|
|
53
|
+
return {
|
|
54
|
+
level: 'L1',
|
|
55
|
+
label: 'Assisted',
|
|
56
|
+
useDag: false,
|
|
57
|
+
useSubagents: false,
|
|
58
|
+
approvalRequired: false,
|
|
59
|
+
verification: 'standard',
|
|
60
|
+
independentVerify: false,
|
|
61
|
+
reasoningEffort: 'balanced',
|
|
62
|
+
contextMaxFiles: 6,
|
|
63
|
+
maxTaskAdditions: 0,
|
|
64
|
+
description: 'Normal task: focused context + skills + tests.',
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return {
|
|
69
|
+
level: 'L0',
|
|
70
|
+
label: 'Direct',
|
|
71
|
+
useDag: false,
|
|
72
|
+
useSubagents: false,
|
|
73
|
+
approvalRequired: false,
|
|
74
|
+
verification: 'light',
|
|
75
|
+
independentVerify: false,
|
|
76
|
+
reasoningEffort: 'balanced',
|
|
77
|
+
contextMaxFiles: 3,
|
|
78
|
+
maxTaskAdditions: 0,
|
|
79
|
+
description: 'Simple task: raw DSH + light verification.',
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function formatIntelligenceLevel(level = {}) {
|
|
84
|
+
return `${level.level} ${level.label} — ${level.description}`
|
|
85
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Strategy Shift.
|
|
3
|
+
*
|
|
4
|
+
* Prevents blind retry loops: when the same failure category/reason repeats,
|
|
5
|
+
* Omni stops issuing another identical repair and instead re-investigates with
|
|
6
|
+
* broader context, a different capability, or a different role.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export function shouldShiftStrategy(failureHistory = []) {
|
|
10
|
+
if (failureHistory.length < 2) return false
|
|
11
|
+
const last = failureHistory[failureHistory.length - 1] || {}
|
|
12
|
+
const prev = failureHistory[failureHistory.length - 2] || {}
|
|
13
|
+
const sameCategory = last.category && last.category === prev.category
|
|
14
|
+
const sameReason = last.reason && last.reason === prev.reason
|
|
15
|
+
const sameHypothesis = last.hypothesis && last.hypothesis === prev.hypothesis
|
|
16
|
+
return Boolean(sameCategory && (sameReason || sameHypothesis))
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function nextStrategy(failureHistory = []) {
|
|
20
|
+
const last = failureHistory[failureHistory.length - 1] || {}
|
|
21
|
+
if (last.category === 'test_failure' || last.category === 'build_failure') {
|
|
22
|
+
return 'Re-investigate root cause with broader context and fresh capability set.'
|
|
23
|
+
}
|
|
24
|
+
if (last.category === 'permission_auth') {
|
|
25
|
+
return 'Check credentials/scopes and switch to an approved auth path.'
|
|
26
|
+
}
|
|
27
|
+
if (last.category === 'dependency_issue') {
|
|
28
|
+
return 'Re-resolve dependency graph and pin compatible versions.'
|
|
29
|
+
}
|
|
30
|
+
if (last.category === 'scope_change') {
|
|
31
|
+
return 'Re-plan with the user to narrow or clarify scope.'
|
|
32
|
+
}
|
|
33
|
+
return 'Expand context, re-diagnose from evidence, and try a different approach.'
|
|
34
|
+
}
|
|
@@ -163,3 +163,15 @@ test('runDagLoop calls onProgress after each batch', async () => {
|
|
|
163
163
|
assert.ok(snapshots.length >= dag.tasks.length)
|
|
164
164
|
assert.ok(snapshots.every((s) => s.dag && Array.isArray(s.actions)))
|
|
165
165
|
})
|
|
166
|
+
|
|
167
|
+
test('runDagLoop triggers strategy shift after repeated same failure', async () => {
|
|
168
|
+
const dag = createMissionDag(buildMission('修复 bug', { taskType: 'bugfix' }))
|
|
169
|
+
const result = await runDagLoop(dag, {
|
|
170
|
+
act: async () => ({ ok: true }),
|
|
171
|
+
observe: async () => ({ type: 'test_failure', reason: 'same root cause' }),
|
|
172
|
+
maxSteps: 30,
|
|
173
|
+
maxParallel: 1,
|
|
174
|
+
})
|
|
175
|
+
const shiftTasks = result.dag.tasks.filter((t) => /Strategy shift/i.test(t.goal))
|
|
176
|
+
assert.ok(shiftTasks.length > 0)
|
|
177
|
+
})
|
|
@@ -1,12 +1,17 @@
|
|
|
1
1
|
import test from 'node:test'
|
|
2
2
|
import assert from 'node:assert/strict'
|
|
3
|
+
import fs from 'node:fs'
|
|
4
|
+
import os from 'node:os'
|
|
5
|
+
import path from 'node:path'
|
|
3
6
|
|
|
4
7
|
import {
|
|
5
8
|
createPerformanceRegistry,
|
|
6
9
|
evaluateProviderValue,
|
|
7
10
|
formatPerformanceRegistry,
|
|
11
|
+
loadPerformanceRegistry,
|
|
8
12
|
recommendDemotion,
|
|
9
13
|
recordProvisionOutcome,
|
|
14
|
+
savePerformanceRegistry,
|
|
10
15
|
} from '../src/capability-performance.mjs'
|
|
11
16
|
|
|
12
17
|
test('recordProvisionOutcome stores before/after metrics', () => {
|
|
@@ -62,3 +67,18 @@ test('formatPerformanceRegistry renders entries', () => {
|
|
|
62
67
|
registry = recordProvisionOutcome(registry, 'p', { successBefore: 0.5, successAfter: 0.9 })
|
|
63
68
|
assert.match(formatPerformanceRegistry(registry), /p/)
|
|
64
69
|
})
|
|
70
|
+
|
|
71
|
+
test('loadPerformanceRegistry and savePerformanceRegistry persist outcomes', () => {
|
|
72
|
+
const cwd = fs.mkdtempSync(path.join(os.tmpdir(), 'cap-perf-'))
|
|
73
|
+
try {
|
|
74
|
+
let registry = createPerformanceRegistry()
|
|
75
|
+
registry = recordProvisionOutcome(registry, 'plugin-x', { successBefore: 0.7, successAfter: 0.9 })
|
|
76
|
+
const file = savePerformanceRegistry(cwd, registry)
|
|
77
|
+
assert.ok(fs.existsSync(file))
|
|
78
|
+
const loaded = loadPerformanceRegistry(cwd)
|
|
79
|
+
assert.ok(loaded.providers['plugin-x'])
|
|
80
|
+
assert.equal(evaluateProviderValue(loaded.providers['plugin-x']).label, 'high')
|
|
81
|
+
} finally {
|
|
82
|
+
fs.rmSync(cwd, { recursive: true, force: true })
|
|
83
|
+
}
|
|
84
|
+
})
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import test from 'node:test'
|
|
2
|
+
import assert from 'node:assert/strict'
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
buildContextCapsule,
|
|
6
|
+
expandContextCapsule,
|
|
7
|
+
} from '../src/context-capsule.mjs'
|
|
8
|
+
|
|
9
|
+
const entries = [
|
|
10
|
+
{ name: 'src/auth/session.ts', type: 'file' },
|
|
11
|
+
{ name: 'src/middleware/auth.ts', type: 'file' },
|
|
12
|
+
{ name: 'tests/session-concurrency.test.ts', type: 'file' },
|
|
13
|
+
{ name: 'README.md', type: 'file' },
|
|
14
|
+
]
|
|
15
|
+
const files = {
|
|
16
|
+
'src/auth/session.ts': 'export function refreshSession() { /* refresh logic */ }',
|
|
17
|
+
'src/middleware/auth.ts': 'import { refreshSession } from "../auth/session"',
|
|
18
|
+
'tests/session-concurrency.test.ts': 'test("one refresh per session", () => {})',
|
|
19
|
+
'README.md': '# Project readme',
|
|
20
|
+
}
|
|
21
|
+
const graph = {
|
|
22
|
+
'src/middleware/auth.ts': [{ to: 'src/auth/session.ts', kind: 'import' }],
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
test('buildContextCapsule includes only relevant files and tests', () => {
|
|
26
|
+
const capsule = buildContextCapsule('Fix session refresh race condition', {
|
|
27
|
+
entries,
|
|
28
|
+
files,
|
|
29
|
+
graph,
|
|
30
|
+
needs: ['callers of refreshSession', 'tests related to concurrent refresh'],
|
|
31
|
+
invariants: ['one refresh request per session'],
|
|
32
|
+
failures: ['expected 1 refresh, got 3'],
|
|
33
|
+
maxFiles: 3,
|
|
34
|
+
})
|
|
35
|
+
assert.match(capsule, /src\/auth\/session\.ts/)
|
|
36
|
+
assert.match(capsule, /session-concurrency\.test\.ts/)
|
|
37
|
+
assert.match(capsule, /one refresh request per session/)
|
|
38
|
+
assert.match(capsule, /expected 1 refresh, got 3/)
|
|
39
|
+
assert.doesNotMatch(capsule, /README\.md/)
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
test('buildContextCapsule truncates to maxTotalChars', () => {
|
|
43
|
+
const capsule = buildContextCapsule('x', { entries, files, graph, maxTotalChars: 100 })
|
|
44
|
+
assert.ok(capsule.length <= 100 + 64)
|
|
45
|
+
assert.match(capsule, /truncated/)
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
test('expandContextCapsule appends requested context', () => {
|
|
49
|
+
const base = buildContextCapsule('fix login', { entries, files, graph })
|
|
50
|
+
const expanded = expandContextCapsule(base, ['tests related to concurrent refresh'], { entries, files, graph })
|
|
51
|
+
assert.match(expanded, /additional context requested/)
|
|
52
|
+
assert.match(expanded, /session-concurrency/)
|
|
53
|
+
})
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import test from 'node:test'
|
|
2
|
+
import assert from 'node:assert/strict'
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
INTELLIGENCE_LEVELS,
|
|
6
|
+
decideIntelligenceLevel,
|
|
7
|
+
formatIntelligenceLevel,
|
|
8
|
+
} from '../src/progressive-intelligence.mjs'
|
|
9
|
+
|
|
10
|
+
test('decideIntelligenceLevel maps direct low-risk tasks to L0', () => {
|
|
11
|
+
const level = decideIntelligenceLevel({ complexity: 'direct', risk: 'low', type: 'other' })
|
|
12
|
+
assert.equal(level.level, 'L0')
|
|
13
|
+
assert.equal(level.useDag, false)
|
|
14
|
+
assert.equal(level.verification, 'light')
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
test('decideIntelligenceLevel maps balanced coding tasks to L1', () => {
|
|
18
|
+
const level = decideIntelligenceLevel({ complexity: 'balanced', risk: 'low', type: 'bugfix' })
|
|
19
|
+
assert.equal(level.level, 'L1')
|
|
20
|
+
assert.equal(level.useSubagents, false)
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
test('decideIntelligenceLevel maps plan complexity to L2', () => {
|
|
24
|
+
const level = decideIntelligenceLevel({ complexity: 'plan', risk: 'low', type: 'feature' })
|
|
25
|
+
assert.equal(level.level, 'L2')
|
|
26
|
+
assert.equal(level.useDag, true)
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
test('decideIntelligenceLevel maps high risk to L3 guarded', () => {
|
|
30
|
+
const level = decideIntelligenceLevel({ complexity: 'direct', risk: 'high', type: 'bugfix' })
|
|
31
|
+
assert.equal(level.level, 'L3')
|
|
32
|
+
assert.equal(level.approvalRequired, true)
|
|
33
|
+
assert.equal(level.independentVerify, true)
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
test('INTELLIGENCE_LEVELS and formatter are stable', () => {
|
|
37
|
+
assert.deepEqual(INTELLIGENCE_LEVELS, ['L0', 'L1', 'L2', 'L3'])
|
|
38
|
+
assert.match(formatIntelligenceLevel({ level: 'L2', label: 'Orchestrated', description: 'x' }), /L2 Orchestrated/)
|
|
39
|
+
})
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import test from 'node:test'
|
|
2
|
+
import assert from 'node:assert/strict'
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
nextStrategy,
|
|
6
|
+
shouldShiftStrategy,
|
|
7
|
+
} from '../src/strategy-shift.mjs'
|
|
8
|
+
|
|
9
|
+
test('shouldShiftStrategy returns false for single or different failures', () => {
|
|
10
|
+
assert.equal(shouldShiftStrategy([]), false)
|
|
11
|
+
assert.equal(shouldShiftStrategy([{ category: 'test_failure', reason: 'a' }]), false)
|
|
12
|
+
assert.equal(shouldShiftStrategy([
|
|
13
|
+
{ category: 'test_failure', reason: 'a' },
|
|
14
|
+
{ category: 'build_failure', reason: 'b' },
|
|
15
|
+
]), false)
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
test('shouldShiftStrategy returns true when same category and reason repeat', () => {
|
|
19
|
+
assert.equal(shouldShiftStrategy([
|
|
20
|
+
{ category: 'test_failure', reason: 'same', hypothesis: 'h' },
|
|
21
|
+
{ category: 'test_failure', reason: 'same', hypothesis: 'h' },
|
|
22
|
+
]), true)
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
test('nextStrategy returns a recovery suggestion', () => {
|
|
26
|
+
const strategy = nextStrategy([{ category: 'test_failure' }])
|
|
27
|
+
assert.match(strategy, /Re-investigate/)
|
|
28
|
+
})
|