dsh-omni-router 2.3.0 → 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 +1 -0
- package/README.zh-CN.md +1 -0
- package/package.json +2 -2
- 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/omni-event.mjs +70 -0
- package/src/omni-router.mjs +27 -2
- package/src/recovery-policy.mjs +34 -0
- package/src/task-contract.mjs +51 -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/recovery-policy.test.mjs +33 -0
- package/test/task-contract.test.mjs +33 -0
package/README.md
CHANGED
|
@@ -79,6 +79,7 @@
|
|
|
79
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。
|
|
80
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
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)。
|
|
82
83
|
- **TaskDecision**:`createTaskDecision` 生成唯一决策对象,Policy/Runtime 统一消费,避免重复 classify 分叉。
|
|
83
84
|
- **Evidence Protocol**:`src/evidence.mjs` 用结构化 command/file/test/finding 证据判定 PASS/FAIL;`isQaPass` 已支持从 QA 输出解析 JSON evidence,不再只信文本。
|
|
84
85
|
- **Task Compiler**:`compileTask` 生成 objective / constraints / non-goals / acceptance / hidden assumptions / ambiguities / invariants / risk / artifacts;`compileTaskWithLLM` 可用 LLM 增强。
|
package/README.zh-CN.md
CHANGED
|
@@ -79,6 +79,7 @@
|
|
|
79
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。
|
|
80
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
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)。
|
|
82
83
|
- **TaskDecision**:`createTaskDecision` 生成唯一决策对象,Policy/Runtime 统一消费,避免重复 classify 分叉。
|
|
83
84
|
- **Evidence Protocol**:`src/evidence.mjs` 用结构化 command/file/test/finding 证据判定 PASS/FAIL;`isQaPass` 已支持从 QA 输出解析 JSON evidence,不再只信文本。
|
|
84
85
|
- **Task Compiler**:`compileTask` 生成 objective / constraints / non-goals / acceptance / hidden assumptions / ambiguities / invariants / risk / artifacts;`compileTaskWithLLM` 可用 LLM 增强。
|
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/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",
|
|
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",
|
|
@@ -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
|
+
}
|
|
@@ -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
|
+
}
|
package/src/omni-router.mjs
CHANGED
|
@@ -39,6 +39,10 @@ import { baselineAudit, formatCapabilityAudit, taskTimeAudit } from './capabilit
|
|
|
39
39
|
import { createStaticRegistryAdapter, discoverCandidates, evaluateProvisionPlan, formatProvisionResult, probeCapability, provisionCapabilities } from './capability-provisioner.mjs'
|
|
40
40
|
import { evaluateProviderValue, formatPerformanceRegistry, loadPerformanceRegistry, recommendDemotion, recordProvisionOutcome, savePerformanceRegistry } from './capability-performance.mjs'
|
|
41
41
|
import { decideIntelligenceLevel, formatIntelligenceLevel } from './progressive-intelligence.mjs'
|
|
42
|
+
import { buildTaskContract, formatTaskContract } from './task-contract.mjs'
|
|
43
|
+
import { decideIntervention, formatInterventionGate } from './intervention-gate.mjs'
|
|
44
|
+
import { negotiateHost, formatHostNegotiation } from './host-interface.mjs'
|
|
45
|
+
import { requiredTrustForRisk } from './evidence-trust.mjs'
|
|
42
46
|
import { buildProgressiveContext } from './context-expansion.mjs'
|
|
43
47
|
import { buildDynamicContext } from './dynamic-context.mjs'
|
|
44
48
|
import { classifyFailure } from './failure-taxonomy.mjs'
|
|
@@ -1020,12 +1024,18 @@ export function apply(ctx, config = {}) {
|
|
|
1020
1024
|
thinkingMode: state.thinkingMode || 'balanced',
|
|
1021
1025
|
})
|
|
1022
1026
|
const intelligence = decideIntelligenceLevel(taskDecision)
|
|
1027
|
+
const contract = buildTaskContract({ taskText: state.firstText || '', decision: taskDecision })
|
|
1028
|
+
const intervention = decideIntervention({
|
|
1029
|
+
successGain: intelligence.level === 'L0' ? 0 : 0.15,
|
|
1030
|
+
tokenOverhead: intelligence.level === 'L0' ? 0.05 : 0.2,
|
|
1031
|
+
})
|
|
1023
1032
|
return [
|
|
1024
1033
|
`omni-router: ${state.kind || 'unclassified'}`,
|
|
1025
1034
|
`taskType=${state.taskType || 'unknown'}`,
|
|
1026
1035
|
`thinkingMode=${state.thinkingMode || 'balanced'}`,
|
|
1027
1036
|
`riskLevel=${state.riskLevel || 'unknown'}`,
|
|
1028
1037
|
`intelligenceLevel=${formatIntelligenceLevel(intelligence)}`,
|
|
1038
|
+
`intervention=${formatInterventionGate(intervention)}`,
|
|
1029
1039
|
`planRequested=${state.planRequested}`,
|
|
1030
1040
|
`directOverride=${state.directOverride}`,
|
|
1031
1041
|
`routerStandard=${routerStandard ? 'delegated' : 'not-detected'}`,
|
|
@@ -1054,6 +1064,8 @@ export function apply(ctx, config = {}) {
|
|
|
1054
1064
|
thinkingMode: state.thinkingMode || 'balanced',
|
|
1055
1065
|
})
|
|
1056
1066
|
const intelligence = decideIntelligenceLevel(decision)
|
|
1067
|
+
const contract = buildTaskContract({ taskText: state.firstText || '', decision })
|
|
1068
|
+
const trust = requiredTrustForRisk(decision.risk)
|
|
1057
1069
|
const topic = String(args?.topic || '').toLowerCase()
|
|
1058
1070
|
const lines = [
|
|
1059
1071
|
`Mode: ${intelligence.level} ${intelligence.label}`,
|
|
@@ -1061,8 +1073,9 @@ export function apply(ctx, config = {}) {
|
|
|
1061
1073
|
`Reasoning effort: ${intelligence.reasoningEffort}`,
|
|
1062
1074
|
`Verification: ${intelligence.verification}`,
|
|
1063
1075
|
`Approval required: ${intelligence.approvalRequired ? 'yes' : 'no'}`,
|
|
1064
|
-
`Capabilities needed: ${(
|
|
1065
|
-
`
|
|
1076
|
+
`Capabilities needed: ${(contract.requiredCapabilities || []).join(', ') || 'native tools'}`,
|
|
1077
|
+
`Evidence trust required: ${trust.label}`,
|
|
1078
|
+
`Completion requires: ${contract.acceptance?.join('; ') || 'light verification'}`,
|
|
1066
1079
|
]
|
|
1067
1080
|
if (topic === 'mode' || topic === 'capabilities' || topic === 'verification' || topic === 'completion') {
|
|
1068
1081
|
const idx = lines.findIndex((l) => l.toLowerCase().startsWith(topic))
|
|
@@ -1084,6 +1097,17 @@ export function apply(ctx, config = {}) {
|
|
|
1084
1097
|
brain = loadCapabilityManifests(brain, config.capabilityManifests || [])
|
|
1085
1098
|
const baseline = baselineAudit(brain)
|
|
1086
1099
|
const fs = ctx.get('fs') || ctx.fs
|
|
1100
|
+
const hostCaps = {
|
|
1101
|
+
workflow: !!(ctx.get('workflow') || ctx.workflow),
|
|
1102
|
+
approvals: !!(ctx.get('approvals') || ctx.approvals),
|
|
1103
|
+
skills: !!(ctx.get('skills') || ctx.skills),
|
|
1104
|
+
plugins: !!(ctx.get('plugins') || ctx.plugins),
|
|
1105
|
+
subagents: !!(ctx.get('subagents') || ctx.subagents),
|
|
1106
|
+
toolEvents: !!(ctx.get('events') || ctx.events),
|
|
1107
|
+
testEvents: false,
|
|
1108
|
+
fileEvents: !!fs,
|
|
1109
|
+
}
|
|
1110
|
+
const host = negotiateHost(hostCaps)
|
|
1087
1111
|
const lines = [
|
|
1088
1112
|
`DSH session: ${session ? 'ok' : 'missing'}`,
|
|
1089
1113
|
`Tools registered: ${toolNames.length}`,
|
|
@@ -1091,6 +1115,7 @@ export function apply(ctx, config = {}) {
|
|
|
1091
1115
|
`Missing baseline: ${baseline.missing.join(', ') || '(none)'}`,
|
|
1092
1116
|
`ProjectIndex: ${fs ? 'available' : 'unavailable'}`,
|
|
1093
1117
|
`Evidence hooks: ${ctx.get('evidence') || ctx.evidence ? 'available' : 'not-detected'}`,
|
|
1118
|
+
`Host negotiation: ${host.mode} (degraded: ${host.degraded.join(', ') || 'none'})`,
|
|
1094
1119
|
]
|
|
1095
1120
|
return lines.join('\n')
|
|
1096
1121
|
},
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Recovery Policy.
|
|
3
|
+
*
|
|
4
|
+
* A single small decision procedure for failures: retry, repair, expand
|
|
5
|
+
* context, change hypothesis, replan, escalate, or stop. No new "Recovery
|
|
6
|
+
* Brain"; just one policy.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export function decideRecovery({
|
|
10
|
+
failure = {},
|
|
11
|
+
attemptCount = 1,
|
|
12
|
+
sameFailureCount = 0,
|
|
13
|
+
hasContextGap = false,
|
|
14
|
+
maxAttempts = 3,
|
|
15
|
+
budgetRemaining = true,
|
|
16
|
+
} = {}) {
|
|
17
|
+
if (!budgetRemaining) return { action: 'stop', reason: 'budget exhausted' }
|
|
18
|
+
if (attemptCount >= maxAttempts) return { action: 'escalate', reason: `max attempts (${maxAttempts}) reached` }
|
|
19
|
+
if (sameFailureCount >= 2) {
|
|
20
|
+
return {
|
|
21
|
+
action: hasContextGap ? 'expand_context' : 'change_hypothesis',
|
|
22
|
+
reason: `same failure repeated ${sameFailureCount} times`,
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
if (hasContextGap) return { action: 'expand_context', reason: 'missing context for root cause' }
|
|
26
|
+
if (failure.category === 'test_failure' || failure.category === 'build_failure') {
|
|
27
|
+
return { action: 'repair', reason: 'diagnose and repair with evidence' }
|
|
28
|
+
}
|
|
29
|
+
return { action: 'retry', reason: 'transient or unknown failure, retry once' }
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function formatRecoveryDecision(decision = {}) {
|
|
33
|
+
return `${decision.action} — ${decision.reason || ''}`
|
|
34
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TaskContract.
|
|
3
|
+
*
|
|
4
|
+
* The single immutable contract that all Omni modules consume after Decide.
|
|
5
|
+
* It replaces ad-hoc re-classification across Planner/Runtime/Verifier.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { decideIntelligenceLevel } from './progressive-intelligence.mjs'
|
|
9
|
+
|
|
10
|
+
export function buildTaskContract({
|
|
11
|
+
taskText = '',
|
|
12
|
+
decision = {},
|
|
13
|
+
acceptance = [],
|
|
14
|
+
constraints = [],
|
|
15
|
+
nonGoals = [],
|
|
16
|
+
requiredCapabilities = [],
|
|
17
|
+
} = {}) {
|
|
18
|
+
const intelligence = decideIntelligenceLevel(decision)
|
|
19
|
+
return {
|
|
20
|
+
objective: taskText,
|
|
21
|
+
constraints: Array.isArray(constraints) ? constraints : [],
|
|
22
|
+
nonGoals: Array.isArray(nonGoals) ? nonGoals : [],
|
|
23
|
+
acceptance: Array.isArray(acceptance) && acceptance.length ? acceptance : [`${taskText} is complete`],
|
|
24
|
+
risk: decision.risk || 'low',
|
|
25
|
+
uncertainty: decision.uncertainty ?? 0.1,
|
|
26
|
+
intelligenceLevel: intelligence.level,
|
|
27
|
+
contextBudget: decision.contextBudget || 20000,
|
|
28
|
+
verificationPolicy: {
|
|
29
|
+
level: intelligence.verification,
|
|
30
|
+
independentVerify: intelligence.independentVerify,
|
|
31
|
+
approvalRequired: intelligence.approvalRequired,
|
|
32
|
+
},
|
|
33
|
+
requiredCapabilities: Array.isArray(requiredCapabilities) && requiredCapabilities.length
|
|
34
|
+
? requiredCapabilities
|
|
35
|
+
: (decision.evidenceRequirements || []),
|
|
36
|
+
reasoningEffort: intelligence.reasoningEffort,
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function formatTaskContract(contract = {}) {
|
|
41
|
+
const lines = [
|
|
42
|
+
`Objective: ${contract.objective || ''}`,
|
|
43
|
+
`Risk: ${contract.risk} | Uncertainty: ${contract.uncertainty}`,
|
|
44
|
+
`Intelligence: ${contract.intelligenceLevel}`,
|
|
45
|
+
`Verification: ${contract.verificationPolicy?.level}${contract.verificationPolicy?.independentVerify ? ' + independent' : ''}${contract.verificationPolicy?.approvalRequired ? ' + approval' : ''}`,
|
|
46
|
+
`Context budget: ${contract.contextBudget}`,
|
|
47
|
+
]
|
|
48
|
+
if (contract.acceptance?.length) lines.push(`Acceptance:\n${contract.acceptance.map((c) => `- ${c}`).join('\n')}`)
|
|
49
|
+
if (contract.requiredCapabilities?.length) lines.push(`Required capabilities: ${contract.requiredCapabilities.join(', ')}`)
|
|
50
|
+
return lines.join('\n')
|
|
51
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import test from 'node:test'
|
|
2
|
+
import assert from 'node:assert/strict'
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
attachFingerprint,
|
|
6
|
+
fingerprintFiles,
|
|
7
|
+
fingerprintText,
|
|
8
|
+
fingerprintWorkspace,
|
|
9
|
+
isContextStale,
|
|
10
|
+
} from '../src/context-freshness.mjs'
|
|
11
|
+
|
|
12
|
+
test('fingerprintText is deterministic', () => {
|
|
13
|
+
assert.equal(fingerprintText('abc'), fingerprintText('abc'))
|
|
14
|
+
assert.notEqual(fingerprintText('abc'), fingerprintText('abd'))
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
test('fingerprintFiles changes when content changes', () => {
|
|
18
|
+
const a = fingerprintFiles({ 'a.ts': 'x' })
|
|
19
|
+
const b = fingerprintFiles({ 'a.ts': 'y' })
|
|
20
|
+
assert.notEqual(a, b)
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
test('fingerprintWorkspace combines commit and files', () => {
|
|
24
|
+
const fp = fingerprintWorkspace({ commit: 'abc', files: { 'a.ts': 'x' } })
|
|
25
|
+
assert.ok(fp.length > 0)
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
test('attachFingerprint and isContextStale', () => {
|
|
29
|
+
const context = attachFingerprint({ files: [] }, 'fp1')
|
|
30
|
+
assert.equal(context.workspaceFingerprint, 'fp1')
|
|
31
|
+
assert.equal(isContextStale(context, 'fp1'), false)
|
|
32
|
+
assert.equal(isContextStale(context, 'fp2'), true)
|
|
33
|
+
})
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import test from 'node:test'
|
|
2
|
+
import assert from 'node:assert/strict'
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
TRUST_LEVELS,
|
|
6
|
+
createEvidenceRecord,
|
|
7
|
+
evidenceMeetsTrust,
|
|
8
|
+
formatEvidenceTrust,
|
|
9
|
+
invalidateEvidence,
|
|
10
|
+
isEvidenceStale,
|
|
11
|
+
requiredTrustForRisk,
|
|
12
|
+
} from '../src/evidence-trust.mjs'
|
|
13
|
+
|
|
14
|
+
test('TRUST_LEVELS are ordered', () => {
|
|
15
|
+
assert.ok(TRUST_LEVELS.T3 > TRUST_LEVELS.T1)
|
|
16
|
+
assert.equal(TRUST_LEVELS.T4, 4)
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
test('createEvidenceRecord includes fingerprint and trust', () => {
|
|
20
|
+
const record = createEvidenceRecord({ criterionId: 'c1', workspaceFingerprint: 'fp1', trustLevel: 'T3' })
|
|
21
|
+
assert.equal(record.trustValue, 3)
|
|
22
|
+
assert.equal(record.criterionId, 'c1')
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
test('isEvidenceStale detects fingerprint mismatch', () => {
|
|
26
|
+
const record = createEvidenceRecord({ workspaceFingerprint: 'fp1' })
|
|
27
|
+
assert.equal(isEvidenceStale(record, 'fp1'), false)
|
|
28
|
+
assert.equal(isEvidenceStale(record, 'fp2'), true)
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
test('invalidateEvidence marks stale records', () => {
|
|
32
|
+
const records = [
|
|
33
|
+
createEvidenceRecord({ workspaceFingerprint: 'fp1' }),
|
|
34
|
+
createEvidenceRecord({ workspaceFingerprint: 'fp2' }),
|
|
35
|
+
]
|
|
36
|
+
const invalidated = invalidateEvidence(records, 'fp2')
|
|
37
|
+
assert.equal(invalidated[0].stale, true)
|
|
38
|
+
assert.equal(invalidated[1].stale, false)
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
test('requiredTrustForRisk escalates for high risk', () => {
|
|
42
|
+
assert.equal(requiredTrustForRisk('low').minimum, 'T1')
|
|
43
|
+
assert.equal(requiredTrustForRisk('high').minimum, 'T3')
|
|
44
|
+
assert.equal(requiredTrustForRisk('high').independent, true)
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
test('evidenceMeetsTrust compares trust values', () => {
|
|
48
|
+
const record = createEvidenceRecord({ trustLevel: 'T3' })
|
|
49
|
+
assert.equal(evidenceMeetsTrust(record, 'T2'), true)
|
|
50
|
+
assert.equal(evidenceMeetsTrust(record, 'T4'), false)
|
|
51
|
+
assert.match(formatEvidenceTrust(record), /T3/)
|
|
52
|
+
})
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import test from 'node:test'
|
|
2
|
+
import assert from 'node:assert/strict'
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
createHostAdapter,
|
|
6
|
+
describeHostCapabilities,
|
|
7
|
+
formatHostNegotiation,
|
|
8
|
+
negotiateHost,
|
|
9
|
+
} from '../src/host-interface.mjs'
|
|
10
|
+
|
|
11
|
+
test('createHostAdapter validates describeHost', () => {
|
|
12
|
+
const valid = createHostAdapter({ describeHost: async () => ({}) })
|
|
13
|
+
assert.equal(valid.valid, true)
|
|
14
|
+
const invalid = createHostAdapter({})
|
|
15
|
+
assert.equal(invalid.valid, false)
|
|
16
|
+
assert.ok(invalid.missing.includes('describeHost'))
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
test('describeHostCapabilities defaults to false', () => {
|
|
20
|
+
const caps = describeHostCapabilities({ workflow: true })
|
|
21
|
+
assert.equal(caps.workflow, true)
|
|
22
|
+
assert.equal(caps.skills, false)
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
test('negotiateHost reports supported and degraded features', () => {
|
|
26
|
+
const result = negotiateHost({ workflow: true, approvals: true, skills: false, plugins: false })
|
|
27
|
+
assert.ok(result.supported.includes('workflow'))
|
|
28
|
+
assert.ok(result.degraded.includes('skills'))
|
|
29
|
+
assert.equal(result.mode, 'degraded')
|
|
30
|
+
assert.match(formatHostNegotiation(result), /Host mode: degraded/)
|
|
31
|
+
})
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import test from 'node:test'
|
|
2
|
+
import assert from 'node:assert/strict'
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
decideIntervention,
|
|
6
|
+
expectedInterventionUtility,
|
|
7
|
+
formatInterventionGate,
|
|
8
|
+
interventionEfficiency,
|
|
9
|
+
noOpPrecision,
|
|
10
|
+
shouldIntervene,
|
|
11
|
+
} from '../src/intervention-gate.mjs'
|
|
12
|
+
|
|
13
|
+
test('expectedInterventionUtility favors high gain low overhead', () => {
|
|
14
|
+
const high = expectedInterventionUtility({ successGain: 0.2, tokenOverhead: 0.05 })
|
|
15
|
+
const low = expectedInterventionUtility({ successGain: 0.01, tokenOverhead: 0.8 })
|
|
16
|
+
assert.ok(high > low)
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
test('shouldIntervene respects threshold', () => {
|
|
20
|
+
assert.equal(shouldIntervene(0.2, 0.1), true)
|
|
21
|
+
assert.equal(shouldIntervene(0.05, 0.1), false)
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
test('decideIntervention returns NOOP by default for low utility', () => {
|
|
25
|
+
const result = decideIntervention({ successGain: 0, tokenOverhead: 0.5 })
|
|
26
|
+
assert.equal(result.intervene, false)
|
|
27
|
+
assert.equal(result.mode, 'noop')
|
|
28
|
+
assert.match(formatInterventionGate(result), /NOOP/)
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
test('noOpPrecision measures correct no-op calls', () => {
|
|
32
|
+
const precision = noOpPrecision([
|
|
33
|
+
{ predictedNoop: true, actuallyNeeded: false },
|
|
34
|
+
{ predictedNoop: true, actuallyNeeded: true },
|
|
35
|
+
{ predictedNoop: false, actuallyNeeded: false },
|
|
36
|
+
])
|
|
37
|
+
assert.equal(precision, 0.5)
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
test('interventionEfficiency computes gain per overhead', () => {
|
|
41
|
+
const result = interventionEfficiency({ rawSuccess: 0.6, omniSuccess: 0.78, tokenOverhead: 0.3 })
|
|
42
|
+
assert.equal(result.gain, 0.18)
|
|
43
|
+
assert.equal(result.efficiency, 0.6)
|
|
44
|
+
})
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import test from 'node:test'
|
|
2
|
+
import assert from 'node:assert/strict'
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
OMNI_EVENT_TYPES,
|
|
6
|
+
createOmniEvent,
|
|
7
|
+
normalizeHostEvent,
|
|
8
|
+
} from '../src/omni-event.mjs'
|
|
9
|
+
|
|
10
|
+
test('createOmniEvent builds host-independent event', () => {
|
|
11
|
+
const event = createOmniEvent({ type: 'test.completed', host: 'dsh', sessionId: 's1', taskId: 't1', workspaceFingerprint: 'fp', payload: { passed: 10 } })
|
|
12
|
+
assert.equal(event.type, 'test.completed')
|
|
13
|
+
assert.equal(event.host, 'dsh')
|
|
14
|
+
assert.equal(event.workspaceFingerprint, 'fp')
|
|
15
|
+
assert.ok(event.timestamp)
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
test('normalizeHostEvent maps DSH tool events', () => {
|
|
19
|
+
const event = normalizeHostEvent({ type: 'tool.started', name: 'edit', data: { file: 'a.ts' } }, 'dsh', { sessionId: 's', taskId: 't' })
|
|
20
|
+
assert.equal(event.type, 'tool.started')
|
|
21
|
+
assert.equal(event.sessionId, 's')
|
|
22
|
+
assert.equal(event.payload.file, 'a.ts')
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
test('normalizeHostEvent maps generic start/end to model events', () => {
|
|
26
|
+
const started = normalizeHostEvent({ type: 'start' }, 'codex')
|
|
27
|
+
const ended = normalizeHostEvent({ type: 'finished' }, 'codex')
|
|
28
|
+
assert.equal(started.type, 'model.started')
|
|
29
|
+
assert.equal(ended.type, 'model.completed')
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
test('OMNI_EVENT_TYPES is stable', () => {
|
|
33
|
+
assert.ok(OMNI_EVENT_TYPES.includes('file.changed'))
|
|
34
|
+
assert.ok(OMNI_EVENT_TYPES.includes('approval.completed'))
|
|
35
|
+
})
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import test from 'node:test'
|
|
2
|
+
import assert from 'node:assert/strict'
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
decideRecovery,
|
|
6
|
+
formatRecoveryDecision,
|
|
7
|
+
} from '../src/recovery-policy.mjs'
|
|
8
|
+
|
|
9
|
+
test('decideRecovery repairs test/build failures', () => {
|
|
10
|
+
const decision = decideRecovery({ failure: { category: 'test_failure' }, attemptCount: 1 })
|
|
11
|
+
assert.equal(decision.action, 'repair')
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
test('decideRecovery expands context when gap exists', () => {
|
|
15
|
+
const decision = decideRecovery({ failure: {}, hasContextGap: true })
|
|
16
|
+
assert.equal(decision.action, 'expand_context')
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
test('decideRecovery changes hypothesis after repeated same failure', () => {
|
|
20
|
+
const decision = decideRecovery({ failure: { category: 'test_failure' }, sameFailureCount: 2, hasContextGap: false })
|
|
21
|
+
assert.equal(decision.action, 'change_hypothesis')
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
test('decideRecovery escalates after max attempts', () => {
|
|
25
|
+
const decision = decideRecovery({ attemptCount: 4, maxAttempts: 3 })
|
|
26
|
+
assert.equal(decision.action, 'escalate')
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
test('decideRecovery stops when budget exhausted', () => {
|
|
30
|
+
const decision = decideRecovery({ budgetRemaining: false })
|
|
31
|
+
assert.equal(decision.action, 'stop')
|
|
32
|
+
assert.match(formatRecoveryDecision(decision), /stop/)
|
|
33
|
+
})
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import test from 'node:test'
|
|
2
|
+
import assert from 'node:assert/strict'
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
buildTaskContract,
|
|
6
|
+
formatTaskContract,
|
|
7
|
+
} from '../src/task-contract.mjs'
|
|
8
|
+
|
|
9
|
+
test('buildTaskContract produces a single unified contract', () => {
|
|
10
|
+
const contract = buildTaskContract({
|
|
11
|
+
taskText: 'Fix payment race',
|
|
12
|
+
decision: { complexity: 'plan', risk: 'high', type: 'bugfix' },
|
|
13
|
+
acceptance: ['no double charge', 'tests pass'],
|
|
14
|
+
requiredCapabilities: ['debugging', 'verification'],
|
|
15
|
+
})
|
|
16
|
+
assert.equal(contract.objective, 'Fix payment race')
|
|
17
|
+
assert.equal(contract.intelligenceLevel, 'L3')
|
|
18
|
+
assert.equal(contract.verificationPolicy.independentVerify, true)
|
|
19
|
+
assert.deepEqual(contract.acceptance, ['no double charge', 'tests pass'])
|
|
20
|
+
assert.ok(contract.requiredCapabilities.includes('verification'))
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
test('buildTaskContract fills default acceptance', () => {
|
|
24
|
+
const contract = buildTaskContract({ taskText: 'Fix typo' })
|
|
25
|
+
assert.equal(contract.acceptance.length, 1)
|
|
26
|
+
assert.match(contract.acceptance[0], /Fix typo/)
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
test('formatTaskContract renders readable summary', () => {
|
|
30
|
+
const text = formatTaskContract(buildTaskContract({ taskText: 'x' }))
|
|
31
|
+
assert.match(text, /Objective: x/)
|
|
32
|
+
assert.match(text, /Intelligence: L0/)
|
|
33
|
+
})
|