dsh-omni-router 2.2.1 → 2.4.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 +7 -1
- package/README.zh-CN.md +7 -1
- package/package.json +2 -2
- package/src/agent-runtime.mjs +13 -2
- package/src/context-capsule.mjs +113 -0
- package/src/context-freshness.mjs +37 -0
- package/src/evidence-trust.mjs +63 -0
- package/src/host-interface.mjs +62 -0
- package/src/intervention-gate.mjs +84 -0
- package/src/mission-dag.mjs +7 -4
- package/src/omni-event.mjs +70 -0
- package/src/omni-router.mjs +98 -0
- package/src/progressive-intelligence.mjs +85 -0
- package/src/recovery-policy.mjs +34 -0
- package/src/strategy-shift.mjs +34 -0
- package/src/task-contract.mjs +51 -0
- package/test/agent-runtime.test.mjs +12 -0
- package/test/context-capsule.test.mjs +53 -0
- package/test/context-freshness.test.mjs +33 -0
- package/test/evidence-trust.test.mjs +52 -0
- package/test/host-interface.test.mjs +31 -0
- package/test/intervention-gate.test.mjs +44 -0
- package/test/omni-event.test.mjs +35 -0
- package/test/progressive-intelligence.test.mjs +39 -0
- package/test/recovery-policy.test.mjs +33 -0
- package/test/strategy-shift.test.mjs +28 -0
- package/test/task-contract.test.mjs +33 -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,8 @@
|
|
|
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` 用户入口。
|
|
82
|
+
- **2.4 Reliability Kernel(优化9)**:TaskContract 统一任务契约;Intervention Gate(默认 NOOP,按 Expected Utility 决定是否介入);OmniEvent / Host Interface(宿主无关事件与能力协商);Evidence Trust(T0-T4)与 Invalidation;Context Freshness(workspace fingerprint);Recovery Policy(retry/repair/expand/replan/escalate/stop)。
|
|
77
83
|
- **TaskDecision**:`createTaskDecision` 生成唯一决策对象,Policy/Runtime 统一消费,避免重复 classify 分叉。
|
|
78
84
|
- **Evidence Protocol**:`src/evidence.mjs` 用结构化 command/file/test/finding 证据判定 PASS/FAIL;`isQaPass` 已支持从 QA 输出解析 JSON evidence,不再只信文本。
|
|
79
85
|
- **Task Compiler**:`compileTask` 生成 objective / constraints / non-goals / acceptance / hidden assumptions / ambiguities / invariants / risk / artifacts;`compileTaskWithLLM` 可用 LLM 增强。
|
|
@@ -155,7 +161,7 @@ Then restart DSH and select **Omni Router** in a new session.
|
|
|
155
161
|
- `/omni direct` — enter direct mode.
|
|
156
162
|
- `/omni mode spec|react|balanced` — set thinking mode.
|
|
157
163
|
- `/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_capability_performance`, `omni_visual_check`.
|
|
164
|
+
- 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
165
|
|
|
160
166
|
## Configuration
|
|
161
167
|
|
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,8 @@
|
|
|
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` 用户入口。
|
|
82
|
+
- **2.4 Reliability Kernel(优化9)**:TaskContract 统一任务契约;Intervention Gate(默认 NOOP,按 Expected Utility 决定是否介入);OmniEvent / Host Interface(宿主无关事件与能力协商);Evidence Trust(T0-T4)与 Invalidation;Context Freshness(workspace fingerprint);Recovery Policy(retry/repair/expand/replan/escalate/stop)。
|
|
77
83
|
- **TaskDecision**:`createTaskDecision` 生成唯一决策对象,Policy/Runtime 统一消费,避免重复 classify 分叉。
|
|
78
84
|
- **Evidence Protocol**:`src/evidence.mjs` 用结构化 command/file/test/finding 证据判定 PASS/FAIL;`isQaPass` 已支持从 QA 输出解析 JSON evidence,不再只信文本。
|
|
79
85
|
- **Task Compiler**:`compileTask` 生成 objective / constraints / non-goals / acceptance / hidden assumptions / ambiguities / invariants / risk / artifacts;`compileTaskWithLLM` 可用 LLM 增强。
|
|
@@ -155,7 +161,7 @@ node scripts/install-preset.mjs
|
|
|
155
161
|
- `/omni direct` — 进入直接执行模式。
|
|
156
162
|
- `/omni mode spec|react|balanced` — 设置思维模式。
|
|
157
163
|
- `/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_capability_performance` / `omni_visual_check`。
|
|
164
|
+
- 模型工具:`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
165
|
|
|
160
166
|
## 配置
|
|
161
167
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-omni-router",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.4.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/task-contract.test.mjs && node test/intervention-gate.test.mjs && node test/omni-event.test.mjs && node test/host-interface.test.mjs && node test/evidence-trust.test.mjs && node test/context-freshness.test.mjs && node test/recovery-policy.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)
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Context Freshness.
|
|
3
|
+
*
|
|
4
|
+
* Context Capsules and evidence records must be bound to a workspace
|
|
5
|
+
* fingerprint. After files change, old context/evidence is stale and cannot be
|
|
6
|
+
* used as reliable input.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import crypto from 'node:crypto'
|
|
10
|
+
|
|
11
|
+
export function fingerprintText(text) {
|
|
12
|
+
return crypto.createHash('sha1').update(String(text || '')).digest('hex').slice(0, 16)
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function fingerprintFiles(files = {}) {
|
|
16
|
+
const hash = crypto.createHash('sha1')
|
|
17
|
+
for (const [name, content] of Object.entries(files)) {
|
|
18
|
+
hash.update(name)
|
|
19
|
+
hash.update('\0')
|
|
20
|
+
hash.update(String(content || ''))
|
|
21
|
+
hash.update('\0')
|
|
22
|
+
}
|
|
23
|
+
return hash.digest('hex').slice(0, 24)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function fingerprintWorkspace({ commit = '', files = {} } = {}) {
|
|
27
|
+
return fingerprintText(`${commit || ''}|${fingerprintFiles(files)}`)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function attachFingerprint(context = {}, fingerprint = '') {
|
|
31
|
+
return { ...context, workspaceFingerprint: fingerprint }
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function isContextStale(context = {}, currentFingerprint = '') {
|
|
35
|
+
if (!currentFingerprint) return false
|
|
36
|
+
return Boolean(context.workspaceFingerprint && context.workspaceFingerprint !== currentFingerprint)
|
|
37
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Evidence Trust & Invalidation.
|
|
3
|
+
*
|
|
4
|
+
* Assigns every evidence record a trust level and a workspace fingerprint.
|
|
5
|
+
* If the workspace changes after evidence was produced, that evidence becomes
|
|
6
|
+
* stale and must not count toward completion.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export const TRUST_LEVELS = {
|
|
10
|
+
T0: 0, // model claim
|
|
11
|
+
T1: 1, // agent observation
|
|
12
|
+
T2: 2, // host/tool output
|
|
13
|
+
T3: 3, // deterministic execution
|
|
14
|
+
T4: 4, // independent/hidden verifier
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function createEvidenceRecord({
|
|
18
|
+
id = `E-${Date.now().toString(36)}`,
|
|
19
|
+
criterionId = '',
|
|
20
|
+
workspaceFingerprint = '',
|
|
21
|
+
producer = 'unknown',
|
|
22
|
+
trustLevel = 'T0',
|
|
23
|
+
createdAt = new Date().toISOString(),
|
|
24
|
+
payload = {},
|
|
25
|
+
} = {}) {
|
|
26
|
+
return {
|
|
27
|
+
evidenceId: id,
|
|
28
|
+
criterionId,
|
|
29
|
+
workspaceFingerprint,
|
|
30
|
+
producer,
|
|
31
|
+
trustLevel,
|
|
32
|
+
trustValue: TRUST_LEVELS[trustLevel] ?? 0,
|
|
33
|
+
createdAt,
|
|
34
|
+
payload,
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function isEvidenceStale(record = {}, currentFingerprint = '') {
|
|
39
|
+
if (!currentFingerprint) return false
|
|
40
|
+
return Boolean(record.workspaceFingerprint && record.workspaceFingerprint !== currentFingerprint)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function invalidateEvidence(records = [], currentFingerprint = '') {
|
|
44
|
+
return records.map((record) => ({
|
|
45
|
+
...record,
|
|
46
|
+
stale: isEvidenceStale(record, currentFingerprint),
|
|
47
|
+
}))
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function requiredTrustForRisk(risk = 'low') {
|
|
51
|
+
if (risk === 'high' || risk === 'critical') return { minimum: 'T3', independent: true, label: 'T3 + independent T4' }
|
|
52
|
+
if (risk === 'medium') return { minimum: 'T3', independent: false, label: 'T3' }
|
|
53
|
+
return { minimum: 'T1', independent: false, label: 'T1/T2' }
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function evidenceMeetsTrust(record = {}, requiredLevel = 'T0') {
|
|
57
|
+
const required = TRUST_LEVELS[requiredLevel] ?? 0
|
|
58
|
+
return (record.trustValue ?? 0) >= required
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function formatEvidenceTrust(record = {}) {
|
|
62
|
+
return `E${record.evidenceId || '?'} T${record.trustLevel || '0'} ${record.stale ? 'STALE' : 'fresh'} criterion=${record.criterionId || '-'}`
|
|
63
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OmniHost Interface.
|
|
3
|
+
*
|
|
4
|
+
* The small contract a host (DSH, Codex, Claude, ...) implements so Omni's
|
|
5
|
+
* kernel stays host-independent. Omni never executes; it decides, prepares,
|
|
6
|
+
* and verifies through this boundary.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export const HOST_FEATURES = [
|
|
10
|
+
'workflow',
|
|
11
|
+
'approvals',
|
|
12
|
+
'skills',
|
|
13
|
+
'plugins',
|
|
14
|
+
'subagents',
|
|
15
|
+
'toolEvents',
|
|
16
|
+
'testEvents',
|
|
17
|
+
'fileEvents',
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
export function createHostAdapter(host = {}) {
|
|
21
|
+
const missing = []
|
|
22
|
+
for (const method of ['describeHost']) {
|
|
23
|
+
if (typeof host[method] !== 'function') missing.push(method)
|
|
24
|
+
}
|
|
25
|
+
return {
|
|
26
|
+
...host,
|
|
27
|
+
missing,
|
|
28
|
+
valid: missing.length === 0,
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function describeHostCapabilities(capabilities = {}) {
|
|
33
|
+
const result = {}
|
|
34
|
+
for (const feature of HOST_FEATURES) {
|
|
35
|
+
result[feature] = capabilities[feature] === true
|
|
36
|
+
}
|
|
37
|
+
return result
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function negotiateHost(hostCapabilities = {}, required = HOST_FEATURES) {
|
|
41
|
+
const caps = describeHostCapabilities(hostCapabilities)
|
|
42
|
+
const supported = []
|
|
43
|
+
const degraded = []
|
|
44
|
+
for (const feature of required) {
|
|
45
|
+
if (caps[feature]) supported.push(feature)
|
|
46
|
+
else degraded.push(feature)
|
|
47
|
+
}
|
|
48
|
+
return {
|
|
49
|
+
supported,
|
|
50
|
+
degraded,
|
|
51
|
+
capabilities: caps,
|
|
52
|
+
mode: degraded.length ? 'degraded' : 'full',
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function formatHostNegotiation(result = {}) {
|
|
57
|
+
return [
|
|
58
|
+
`Host mode: ${result.mode}`,
|
|
59
|
+
`Supported: ${result.supported.join(', ') || '(none)'}`,
|
|
60
|
+
`Degraded: ${result.degraded.join(', ') || '(none)'}`,
|
|
61
|
+
].join('\n')
|
|
62
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Intervention Gate.
|
|
3
|
+
*
|
|
4
|
+
* Decides whether Omni should intervene at all. The default answer is NOOP:
|
|
5
|
+
* if the host can already do the job without extra tokens/latency/context
|
|
6
|
+
* pollution, Omni stays out of the way.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export function expectedInterventionUtility({
|
|
10
|
+
successGain = 0,
|
|
11
|
+
tokenOverhead = 0,
|
|
12
|
+
latency = 0,
|
|
13
|
+
contextPollution = 0,
|
|
14
|
+
overlap = 0,
|
|
15
|
+
failureSurface = 0,
|
|
16
|
+
taskFrequency = 1,
|
|
17
|
+
} = {}) {
|
|
18
|
+
const utility = (
|
|
19
|
+
successGain * 1.0 -
|
|
20
|
+
tokenOverhead * 0.5 -
|
|
21
|
+
latency * 0.3 -
|
|
22
|
+
contextPollution * 0.5 -
|
|
23
|
+
overlap * 0.4 -
|
|
24
|
+
failureSurface * 0.4
|
|
25
|
+
) * Math.max(0.1, taskFrequency)
|
|
26
|
+
return Math.round(utility * 1000) / 1000
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function shouldIntervene(utility = 0, threshold = 0.1) {
|
|
30
|
+
return utility > threshold
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function decideIntervention({
|
|
34
|
+
successGain,
|
|
35
|
+
tokenOverhead,
|
|
36
|
+
latency,
|
|
37
|
+
contextPollution,
|
|
38
|
+
overlap,
|
|
39
|
+
failureSurface,
|
|
40
|
+
taskFrequency,
|
|
41
|
+
threshold = 0.1,
|
|
42
|
+
} = {}) {
|
|
43
|
+
const utility = expectedInterventionUtility({
|
|
44
|
+
successGain,
|
|
45
|
+
tokenOverhead,
|
|
46
|
+
latency,
|
|
47
|
+
contextPollution,
|
|
48
|
+
overlap,
|
|
49
|
+
failureSurface,
|
|
50
|
+
taskFrequency,
|
|
51
|
+
})
|
|
52
|
+
return {
|
|
53
|
+
utility,
|
|
54
|
+
threshold,
|
|
55
|
+
intervene: shouldIntervene(utility, threshold),
|
|
56
|
+
mode: shouldIntervene(utility, threshold) ? 'intervene' : 'noop',
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function noOpPrecision(decisions = []) {
|
|
61
|
+
// decision: { predictedNoop: boolean, actuallyNeeded: boolean }
|
|
62
|
+
let predictedNoop = 0
|
|
63
|
+
let correctNoop = 0
|
|
64
|
+
for (const d of decisions) {
|
|
65
|
+
if (!d.predictedNoop) continue
|
|
66
|
+
predictedNoop += 1
|
|
67
|
+
if (!d.actuallyNeeded) correctNoop += 1
|
|
68
|
+
}
|
|
69
|
+
return predictedNoop ? Math.round((correctNoop / predictedNoop) * 1000) / 1000 : 1
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function interventionEfficiency({ rawSuccess = 0, omniSuccess = 0, tokenOverhead = 0 }) {
|
|
73
|
+
const gain = omniSuccess - rawSuccess
|
|
74
|
+
const overhead = Math.max(0.01, tokenOverhead)
|
|
75
|
+
return {
|
|
76
|
+
gain: Math.round(gain * 1000) / 1000,
|
|
77
|
+
tokenOverhead,
|
|
78
|
+
efficiency: Math.round((gain / overhead) * 1000) / 1000,
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function formatInterventionGate(result = {}) {
|
|
83
|
+
return `${result.intervene ? 'INTERVENE' : 'NOOP'} (utility=${result.utility}, threshold=${result.threshold})`
|
|
84
|
+
}
|
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.
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OmniEvent.
|
|
3
|
+
*
|
|
4
|
+
* Host-independent event model. DSH, Codex, Claude, etc. normalize their own
|
|
5
|
+
* events into OmniEvent so the Evidence/Recovery/Memory kernels never depend on
|
|
6
|
+
* a specific host.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export const OMNI_EVENT_TYPES = [
|
|
10
|
+
'model.started',
|
|
11
|
+
'model.completed',
|
|
12
|
+
'tool.started',
|
|
13
|
+
'tool.completed',
|
|
14
|
+
'command.completed',
|
|
15
|
+
'test.completed',
|
|
16
|
+
'file.changed',
|
|
17
|
+
'approval.requested',
|
|
18
|
+
'approval.completed',
|
|
19
|
+
'agent.started',
|
|
20
|
+
'agent.completed',
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
export function createOmniEvent({
|
|
24
|
+
type,
|
|
25
|
+
host = 'unknown',
|
|
26
|
+
sessionId = '',
|
|
27
|
+
taskId = '',
|
|
28
|
+
workspaceFingerprint = '',
|
|
29
|
+
payload = {},
|
|
30
|
+
} = {}) {
|
|
31
|
+
if (!type) throw new Error('OmniEvent type is required')
|
|
32
|
+
return {
|
|
33
|
+
type,
|
|
34
|
+
host,
|
|
35
|
+
sessionId,
|
|
36
|
+
taskId,
|
|
37
|
+
workspaceFingerprint,
|
|
38
|
+
timestamp: new Date().toISOString(),
|
|
39
|
+
payload,
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function normalizeHostEvent(hostEvent = {}, host = 'unknown', context = {}) {
|
|
44
|
+
const rawType = String(hostEvent.type || hostEvent.event || hostEvent.name || 'unknown')
|
|
45
|
+
const type = mapHostEventType(rawType, host)
|
|
46
|
+
return createOmniEvent({
|
|
47
|
+
type,
|
|
48
|
+
host: hostEvent.host || host,
|
|
49
|
+
sessionId: hostEvent.sessionId || context.sessionId || '',
|
|
50
|
+
taskId: hostEvent.taskId || context.taskId || '',
|
|
51
|
+
workspaceFingerprint: hostEvent.workspaceFingerprint || context.workspaceFingerprint || '',
|
|
52
|
+
payload: hostEvent.payload || hostEvent.data || hostEvent,
|
|
53
|
+
})
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function mapHostEventType(raw, host) {
|
|
57
|
+
const value = String(raw).toLowerCase()
|
|
58
|
+
if (host === 'dsh') {
|
|
59
|
+
if (/tool|command/.test(value) && /start/.test(value)) return 'tool.started'
|
|
60
|
+
if (/tool|command/.test(value) && /end|done|complete/.test(value)) return 'tool.completed'
|
|
61
|
+
if (/test/.test(value)) return 'test.completed'
|
|
62
|
+
if (/file|change/.test(value)) return 'file.changed'
|
|
63
|
+
if (/approval/.test(value)) return 'approval.requested'
|
|
64
|
+
if (/agent|subagent/.test(value) && /start/.test(value)) return 'agent.started'
|
|
65
|
+
if (/agent|subagent/.test(value) && /end|done|complete/.test(value)) return 'agent.completed'
|
|
66
|
+
}
|
|
67
|
+
if (/start|begin/.test(value)) return 'model.started'
|
|
68
|
+
if (/end|done|complete|finish/.test(value)) return 'model.completed'
|
|
69
|
+
return OMNI_EVENT_TYPES.includes(raw) ? raw : 'model.completed'
|
|
70
|
+
}
|