open-multi-agent-kit 0.78.0 → 0.78.1
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/CHANGELOG.md +44 -15
- package/MATURITY.md +2 -2
- package/README.md +56 -26
- package/ROADMAP.md +36 -28
- package/dist/cli/register-basic-commands.js +3 -2
- package/dist/cli/register-mcp-dag-cron-screenshot-commands.js +2 -0
- package/dist/cli/register-tool-commands.js +11 -0
- package/dist/cli/register-workflow-commands.js +1 -0
- package/dist/cli/registry/tooling.js +3 -2
- package/dist/commands/chat/core.js +5 -0
- package/dist/commands/chat/native-root-loop.js +60 -0
- package/dist/commands/dag-from-spec.d.ts +1 -0
- package/dist/commands/dag-from-spec.js +61 -1
- package/dist/commands/graph.d.ts +62 -0
- package/dist/commands/graph.js +182 -0
- package/dist/commands/merge.d.ts +1 -0
- package/dist/commands/merge.js +88 -0
- package/dist/commands/parallel/core.js +3 -3
- package/dist/commands/provider.js +5 -3
- package/dist/commands/star.js +6 -1
- package/dist/commands/summary.d.ts +4 -1
- package/dist/commands/summary.js +103 -1
- package/dist/commands/team.d.ts +1 -0
- package/dist/commands/team.js +38 -0
- package/dist/contracts/provider-health.d.ts +42 -0
- package/dist/contracts/provider-health.js +9 -0
- package/dist/goal/intent-frame.d.ts +24 -0
- package/dist/goal/intent-frame.js +18 -0
- package/dist/memory/local-graph-memory-store.d.ts +15 -0
- package/dist/memory/local-graph-memory-store.js +176 -0
- package/dist/memory/memory-store.d.ts +18 -0
- package/dist/memory/memory-store.js +18 -0
- package/dist/orchestration/adaptorch-topology.d.ts +59 -0
- package/dist/orchestration/adaptorch-topology.js +194 -0
- package/dist/orchestration/capability-routing.d.ts +23 -0
- package/dist/orchestration/capability-routing.js +56 -0
- package/dist/orchestration/dag-compiler-types.d.ts +3 -0
- package/dist/orchestration/dag-compiler.js +14 -1
- package/dist/orchestration/parallel-orchestrator.d.ts +6 -0
- package/dist/orchestration/parallel-orchestrator.js +31 -0
- package/dist/providers/provider-health.d.ts +39 -0
- package/dist/providers/provider-health.js +161 -0
- package/dist/runtime/context-broker.d.ts +13 -4
- package/dist/runtime/context-broker.js +14 -1
- package/dist/runtime/headroom-policy.d.ts +37 -0
- package/dist/runtime/headroom-policy.js +122 -0
- package/dist/runtime/ouroboros-policy.d.ts +57 -0
- package/dist/runtime/ouroboros-policy.js +134 -0
- package/dist/runtime/runtime-backed-task-runner.js +9 -1
- package/dist/runtime/tool-dispatch-contracts.d.ts +57 -1
- package/dist/runtime/tool-dispatch-contracts.js +79 -3
- package/dist/safety/tool-authority-gate.d.ts +62 -0
- package/dist/safety/tool-authority-gate.js +108 -0
- package/dist/schema/provider.schema.d.ts +4 -4
- package/dist/util/first-run-star.d.ts +9 -0
- package/dist/util/first-run-star.js +42 -1
- package/dist/util/terminal-input.d.ts +20 -0
- package/dist/util/terminal-input.js +32 -0
- package/dist/util/update-check.d.ts +6 -1
- package/dist/util/update-check.js +35 -1
- package/docs/2026-06-08/critical-issues.md +20 -0
- package/docs/2026-06-08/improvements.md +14 -0
- package/docs/2026-06-08/init-checklist.md +25 -0
- package/docs/2026-06-08/plan.md +20 -0
- package/docs/getting-started.md +31 -3
- package/docs/integrations/ouroboros.md +96 -0
- package/docs/provider-maturity.md +1 -1
- package/docs/versioning.md +3 -3
- package/package.json +1 -1
- package/dist/native/linux-x64/omk-safety +0 -0
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AdaptOrch-style topology router — pure TS, no IO, no python dependency.
|
|
3
|
+
*
|
|
4
|
+
* Extracts 5 structural features from a DAG and selects an execution topology
|
|
5
|
+
* using threshold-based rules matching the AdaptOrch TopologyRouter spec.
|
|
6
|
+
*/
|
|
7
|
+
// ─────────────────────────────────────────────
|
|
8
|
+
// Defaults
|
|
9
|
+
// ─────────────────────────────────────────────
|
|
10
|
+
const DEFAULT_THRESHOLDS = {
|
|
11
|
+
parallelRatio: 0.5,
|
|
12
|
+
highCoupling: 0.6,
|
|
13
|
+
hierarchicalSubtasks: 5,
|
|
14
|
+
};
|
|
15
|
+
// ─────────────────────────────────────────────
|
|
16
|
+
// Env gate
|
|
17
|
+
// ─────────────────────────────────────────────
|
|
18
|
+
/**
|
|
19
|
+
* Returns `true` unless OMK_ADAPTORCH_ROUTING is explicitly off/0/false.
|
|
20
|
+
*/
|
|
21
|
+
export function isAdaptorchRoutingEnabled(env) {
|
|
22
|
+
const val = (env ?? process.env)["OMK_ADAPTORCH_ROUTING"];
|
|
23
|
+
if (val === undefined)
|
|
24
|
+
return true;
|
|
25
|
+
const norm = val.trim().toLowerCase();
|
|
26
|
+
return norm !== "0" && norm !== "off" && norm !== "false";
|
|
27
|
+
}
|
|
28
|
+
// ─────────────────────────────────────────────
|
|
29
|
+
// Feature extraction
|
|
30
|
+
// ─────────────────────────────────────────────
|
|
31
|
+
/**
|
|
32
|
+
* Compute structural features of a DAG via Kahn topological sort.
|
|
33
|
+
* Returns layers (waves) and derived metrics. If a cycle is detected,
|
|
34
|
+
* all nodes collapse into a single wave.
|
|
35
|
+
*/
|
|
36
|
+
export function computeTopologyFeatures(nodes, edges) {
|
|
37
|
+
const n = nodes.length;
|
|
38
|
+
if (n === 0) {
|
|
39
|
+
return {
|
|
40
|
+
nodeCount: 0,
|
|
41
|
+
edgeCount: 0,
|
|
42
|
+
width: 0,
|
|
43
|
+
criticalDepth: 0,
|
|
44
|
+
couplingDensity: 0,
|
|
45
|
+
parallelRatio: 0,
|
|
46
|
+
layers: [],
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
// Build adjacency + in-degree
|
|
50
|
+
const inDeg = new Map();
|
|
51
|
+
const adjacency = new Map();
|
|
52
|
+
for (const id of nodes) {
|
|
53
|
+
inDeg.set(id, 0);
|
|
54
|
+
adjacency.set(id, []);
|
|
55
|
+
}
|
|
56
|
+
for (const e of edges) {
|
|
57
|
+
adjacency.get(e.from)?.push(e.to);
|
|
58
|
+
inDeg.set(e.to, (inDeg.get(e.to) ?? 0) + 1);
|
|
59
|
+
}
|
|
60
|
+
// Kahn topological sort
|
|
61
|
+
const layers = [];
|
|
62
|
+
const visited = new Set();
|
|
63
|
+
// Seed: zero-in-degree nodes
|
|
64
|
+
let frontier = nodes.filter((id) => (inDeg.get(id) ?? 0) === 0);
|
|
65
|
+
if (frontier.length === 0) {
|
|
66
|
+
// All nodes are in a cycle — single wave fallback
|
|
67
|
+
return {
|
|
68
|
+
nodeCount: n,
|
|
69
|
+
edgeCount: edges.length,
|
|
70
|
+
width: n,
|
|
71
|
+
criticalDepth: 1,
|
|
72
|
+
couplingDensity: computeCouplingDensity(n, edges.length),
|
|
73
|
+
parallelRatio: 1,
|
|
74
|
+
layers: [nodes.slice()],
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
while (frontier.length > 0) {
|
|
78
|
+
layers.push(frontier.slice());
|
|
79
|
+
for (const id of frontier)
|
|
80
|
+
visited.add(id);
|
|
81
|
+
const nextFrontier = [];
|
|
82
|
+
for (const id of frontier) {
|
|
83
|
+
for (const child of adjacency.get(id) ?? []) {
|
|
84
|
+
const newDeg = (inDeg.get(child) ?? 1) - 1;
|
|
85
|
+
inDeg.set(child, newDeg);
|
|
86
|
+
if (newDeg === 0 && !visited.has(child)) {
|
|
87
|
+
nextFrontier.push(child);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
frontier = nextFrontier;
|
|
92
|
+
}
|
|
93
|
+
// Cycle detection: if any nodes remain unvisited, collapse to single wave
|
|
94
|
+
if (visited.size < n) {
|
|
95
|
+
return {
|
|
96
|
+
nodeCount: n,
|
|
97
|
+
edgeCount: edges.length,
|
|
98
|
+
width: n,
|
|
99
|
+
criticalDepth: 1,
|
|
100
|
+
couplingDensity: computeCouplingDensity(n, edges.length),
|
|
101
|
+
parallelRatio: 1,
|
|
102
|
+
layers: [nodes.slice()],
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
const width = Math.max(...layers.map((l) => l.length));
|
|
106
|
+
const criticalDepth = layers.length;
|
|
107
|
+
const couplingDensity = computeCouplingDensity(n, edges.length);
|
|
108
|
+
const parallelRatio = n > 0 ? width / n : 0;
|
|
109
|
+
return {
|
|
110
|
+
nodeCount: n,
|
|
111
|
+
edgeCount: edges.length,
|
|
112
|
+
width,
|
|
113
|
+
criticalDepth,
|
|
114
|
+
couplingDensity,
|
|
115
|
+
parallelRatio,
|
|
116
|
+
layers,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
// ─────────────────────────────────────────────
|
|
120
|
+
// Topology selection
|
|
121
|
+
// ─────────────────────────────────────────────
|
|
122
|
+
/**
|
|
123
|
+
* Select a topology and emit layered execution waves.
|
|
124
|
+
*
|
|
125
|
+
* Selection rules (in priority order):
|
|
126
|
+
* 0 nodes → singleton
|
|
127
|
+
* 1 node → singleton
|
|
128
|
+
* high coupling → dag (if depth==1) or hierarchical (if depth>1)
|
|
129
|
+
* parallelRatio ≥ θ_ω and nodes > 1 → parallel / map_reduce
|
|
130
|
+
* criticalDepth == nodes → pipeline
|
|
131
|
+
* otherwise → hybrid
|
|
132
|
+
*/
|
|
133
|
+
export function routeTopology(nodeIds, edges, thresholds) {
|
|
134
|
+
const t = { ...DEFAULT_THRESHOLDS, ...thresholds };
|
|
135
|
+
const features = computeTopologyFeatures(nodeIds, edges);
|
|
136
|
+
const { nodeCount, width, criticalDepth, couplingDensity, parallelRatio } = features;
|
|
137
|
+
// 0 or 1 node → singleton
|
|
138
|
+
if (nodeCount <= 1) {
|
|
139
|
+
return {
|
|
140
|
+
topology: "singleton",
|
|
141
|
+
reason: nodeCount === 0 ? "empty DAG" : "single-node DAG",
|
|
142
|
+
features,
|
|
143
|
+
waves: features.layers,
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
// High coupling check
|
|
147
|
+
if (couplingDensity >= t.highCoupling) {
|
|
148
|
+
const topology = criticalDepth > 1 ? "hierarchical" : "dag";
|
|
149
|
+
return {
|
|
150
|
+
topology,
|
|
151
|
+
reason: `high coupling density ${couplingDensity.toFixed(2)} ≥ θ_γ=${t.highCoupling}`,
|
|
152
|
+
features,
|
|
153
|
+
waves: features.layers,
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
// Parallel / map_reduce — wide low-coupling DAG
|
|
157
|
+
if (parallelRatio >= t.parallelRatio && nodeCount > 1) {
|
|
158
|
+
// map_reduce when enough nodes form ≥2 layers with fan-out then fan-in
|
|
159
|
+
const topology = features.layers.length >= 2 && width >= t.hierarchicalSubtasks
|
|
160
|
+
? "map_reduce"
|
|
161
|
+
: "parallel";
|
|
162
|
+
return {
|
|
163
|
+
topology,
|
|
164
|
+
reason: `parallel ratio ${parallelRatio.toFixed(2)} ≥ θ_ω=${t.parallelRatio}, width=${width}`,
|
|
165
|
+
features,
|
|
166
|
+
waves: features.layers,
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
// Linear chain → pipeline
|
|
170
|
+
if (criticalDepth === nodeCount) {
|
|
171
|
+
return {
|
|
172
|
+
topology: "pipeline",
|
|
173
|
+
reason: `linear chain: critical depth (${criticalDepth}) equals node count`,
|
|
174
|
+
features,
|
|
175
|
+
waves: features.layers,
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
// Fall-through → hybrid
|
|
179
|
+
return {
|
|
180
|
+
topology: "hybrid",
|
|
181
|
+
reason: `mixed structure: depth=${criticalDepth}, width=${width}, coupling=${couplingDensity.toFixed(2)}`,
|
|
182
|
+
features,
|
|
183
|
+
waves: features.layers,
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
// ─────────────────────────────────────────────
|
|
187
|
+
// Internal helpers
|
|
188
|
+
// ─────────────────────────────────────────────
|
|
189
|
+
function computeCouplingDensity(n, edgeCount) {
|
|
190
|
+
if (n <= 1)
|
|
191
|
+
return 0;
|
|
192
|
+
const maxEdges = (n * (n - 1)) / 2;
|
|
193
|
+
return maxEdges > 0 ? edgeCount / maxEdges : 0;
|
|
194
|
+
}
|
|
@@ -1,9 +1,20 @@
|
|
|
1
1
|
import type { DagNode, DagNodeRouting } from "./dag.js";
|
|
2
|
+
import type { ProviderAuthorityLevel } from "../contracts/provider-health.js";
|
|
2
3
|
export interface NodeCapabilityScopes {
|
|
3
4
|
readonly skills: readonly string[];
|
|
4
5
|
readonly mcpServers: readonly string[];
|
|
5
6
|
readonly tools: readonly string[];
|
|
6
7
|
readonly hooks: readonly string[];
|
|
8
|
+
/**
|
|
9
|
+
* Provider authority for write/mutation work on this node. Defaults to the
|
|
10
|
+
* authority-provider value ("full") so the primary coder/authority lane is
|
|
11
|
+
* never over-blocked. Advisory/opportunistic providers carry lower levels.
|
|
12
|
+
*/
|
|
13
|
+
readonly writeAuthority: ProviderAuthorityLevel;
|
|
14
|
+
/** Provider authority for shell/CLI work on this node. Defaults to "full". */
|
|
15
|
+
readonly shellAuthority: ProviderAuthorityLevel;
|
|
16
|
+
/** Provider authority for MCP tool work on this node. Defaults to "full". */
|
|
17
|
+
readonly mcpAuthority: ProviderAuthorityLevel;
|
|
7
18
|
}
|
|
8
19
|
export interface CapabilityRoutingEntry extends NodeCapabilityScopes {
|
|
9
20
|
readonly nodeId: string;
|
|
@@ -31,6 +42,18 @@ export interface CapabilityRoutingArtifact {
|
|
|
31
42
|
readonly nodes: readonly CapabilityRoutingEntry[];
|
|
32
43
|
readonly orchestrator: CapabilityRoutingIdentity;
|
|
33
44
|
}
|
|
45
|
+
/**
|
|
46
|
+
* Authority levels are derived from the authority-provider doctrine: when a
|
|
47
|
+
* node does not pin an explicit provider authority it inherits the
|
|
48
|
+
* authority-provider level ("full") so the primary coder lane stays unblocked.
|
|
49
|
+
*/
|
|
50
|
+
export declare const DEFAULT_NODE_AUTHORITY: ProviderAuthorityLevel;
|
|
51
|
+
/**
|
|
52
|
+
* Resolve the write/shell/MCP authority levels for a routing entry. Used both
|
|
53
|
+
* by {@link capabilityScopesFromRouting} and by the live tool-authority gate so
|
|
54
|
+
* the gate consumes the same authority that routing assigned.
|
|
55
|
+
*/
|
|
56
|
+
export declare function resolveNodeToolAuthorities(routing: DagNodeRouting | undefined, fallback?: Partial<NodeCapabilityScopes>): Pick<NodeCapabilityScopes, "writeAuthority" | "shellAuthority" | "mcpAuthority">;
|
|
34
57
|
export declare function uniqueCapabilityNames(values: readonly (string | undefined)[]): string[];
|
|
35
58
|
export declare function capabilityScopesFromRouting(routing: DagNodeRouting | undefined, fallback?: Partial<NodeCapabilityScopes>): NodeCapabilityScopes;
|
|
36
59
|
export declare function mergeCapabilityScopes(...scopes: readonly (Partial<NodeCapabilityScopes> | undefined)[]): NodeCapabilityScopes;
|
|
@@ -1,3 +1,55 @@
|
|
|
1
|
+
/** Authority levels ordered from most restrictive (0) to most permissive (3). */
|
|
2
|
+
const AUTHORITY_RANK = {
|
|
3
|
+
none: 0,
|
|
4
|
+
advisory: 1,
|
|
5
|
+
direct: 2,
|
|
6
|
+
full: 3,
|
|
7
|
+
};
|
|
8
|
+
/**
|
|
9
|
+
* Authority levels are derived from the authority-provider doctrine: when a
|
|
10
|
+
* node does not pin an explicit provider authority it inherits the
|
|
11
|
+
* authority-provider level ("full") so the primary coder lane stays unblocked.
|
|
12
|
+
*/
|
|
13
|
+
export const DEFAULT_NODE_AUTHORITY = "full";
|
|
14
|
+
/**
|
|
15
|
+
* Map a routing `assignedProviderAuthority` token to a {@link ProviderAuthorityLevel}.
|
|
16
|
+
* Returns `undefined` when the routing does not pin an authority so callers can
|
|
17
|
+
* fall back to the authority-provider default.
|
|
18
|
+
*/
|
|
19
|
+
function authorityLevelFromAssigned(assigned) {
|
|
20
|
+
switch (assigned) {
|
|
21
|
+
case "authority":
|
|
22
|
+
return "full";
|
|
23
|
+
case "direct":
|
|
24
|
+
return "direct";
|
|
25
|
+
case "advisory":
|
|
26
|
+
return "advisory";
|
|
27
|
+
case "veto":
|
|
28
|
+
return "none";
|
|
29
|
+
default:
|
|
30
|
+
return undefined;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
/** Pick the most restrictive defined authority level, or the fallback. */
|
|
34
|
+
function mostRestrictiveAuthority(values, fallback = DEFAULT_NODE_AUTHORITY) {
|
|
35
|
+
const defined = values.filter((value) => value !== undefined);
|
|
36
|
+
if (defined.length === 0)
|
|
37
|
+
return fallback;
|
|
38
|
+
return defined.reduce((min, value) => (AUTHORITY_RANK[value] < AUTHORITY_RANK[min] ? value : min));
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Resolve the write/shell/MCP authority levels for a routing entry. Used both
|
|
42
|
+
* by {@link capabilityScopesFromRouting} and by the live tool-authority gate so
|
|
43
|
+
* the gate consumes the same authority that routing assigned.
|
|
44
|
+
*/
|
|
45
|
+
export function resolveNodeToolAuthorities(routing, fallback = {}) {
|
|
46
|
+
const base = authorityLevelFromAssigned(routing?.assignedProviderAuthority);
|
|
47
|
+
return {
|
|
48
|
+
writeAuthority: base ?? fallback.writeAuthority ?? DEFAULT_NODE_AUTHORITY,
|
|
49
|
+
shellAuthority: base ?? fallback.shellAuthority ?? DEFAULT_NODE_AUTHORITY,
|
|
50
|
+
mcpAuthority: base ?? fallback.mcpAuthority ?? DEFAULT_NODE_AUTHORITY,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
1
53
|
export function uniqueCapabilityNames(values) {
|
|
2
54
|
return [...new Set(values.filter((value) => Boolean(value?.trim())).map((value) => value.trim()))];
|
|
3
55
|
}
|
|
@@ -8,6 +60,7 @@ export function capabilityScopesFromRouting(routing, fallback = {}) {
|
|
|
8
60
|
mcpServers: uniqueCapabilityNames(routing?.mcpServers ?? assigned?.mcpServers ?? fallback.mcpServers ?? []),
|
|
9
61
|
tools: uniqueCapabilityNames(routing?.tools ?? assigned?.tools ?? fallback.tools ?? []),
|
|
10
62
|
hooks: uniqueCapabilityNames(routing?.hooks ?? assigned?.hooks ?? fallback.hooks ?? []),
|
|
63
|
+
...resolveNodeToolAuthorities(routing, fallback),
|
|
11
64
|
};
|
|
12
65
|
}
|
|
13
66
|
export function mergeCapabilityScopes(...scopes) {
|
|
@@ -16,6 +69,9 @@ export function mergeCapabilityScopes(...scopes) {
|
|
|
16
69
|
mcpServers: uniqueCapabilityNames(scopes.flatMap((scope) => scope?.mcpServers ?? [])),
|
|
17
70
|
tools: uniqueCapabilityNames(scopes.flatMap((scope) => scope?.tools ?? [])),
|
|
18
71
|
hooks: uniqueCapabilityNames(scopes.flatMap((scope) => scope?.hooks ?? [])),
|
|
72
|
+
writeAuthority: mostRestrictiveAuthority(scopes.map((scope) => scope?.writeAuthority)),
|
|
73
|
+
shellAuthority: mostRestrictiveAuthority(scopes.map((scope) => scope?.shellAuthority)),
|
|
74
|
+
mcpAuthority: mostRestrictiveAuthority(scopes.map((scope) => scope?.mcpAuthority)),
|
|
19
75
|
};
|
|
20
76
|
}
|
|
21
77
|
export function attachAssignedCapabilities(routing) {
|
|
@@ -2,6 +2,7 @@ import type { IntentFrame, GoalSpec } from "../contracts/goal.js";
|
|
|
2
2
|
import type { ExecutionSelectionDecision, ExecutionStrategy, UserIntentV2 } from "../contracts/orchestration.js";
|
|
3
3
|
import type { InputEnvelope } from "../input/input-envelope.js";
|
|
4
4
|
import type { Dag } from "./dag.js";
|
|
5
|
+
import type { TopologyDecision } from "./adaptorch-topology.js";
|
|
5
6
|
export interface DagCompileInput {
|
|
6
7
|
input: InputEnvelope;
|
|
7
8
|
goal?: GoalSpec;
|
|
@@ -26,6 +27,8 @@ export interface DagCompileResult {
|
|
|
26
27
|
intentFrame?: IntentFrame;
|
|
27
28
|
artifacts: DagCompileArtifactSummary;
|
|
28
29
|
compiledAt: string;
|
|
30
|
+
/** AdaptOrch topology routing — additive, optional, non-fatal */
|
|
31
|
+
topology?: TopologyDecision;
|
|
29
32
|
}
|
|
30
33
|
export interface BuildDagCompileResultInput {
|
|
31
34
|
input: InputEnvelope;
|
|
@@ -2,6 +2,7 @@ import { analyzeUserIntentV2 } from "../goal/intent-analyzer.js";
|
|
|
2
2
|
import { buildIntentFrame } from "../goal/intent-frame.js";
|
|
3
3
|
import { createDag } from "./dag.js";
|
|
4
4
|
import { buildRoleSpecificDagNodes, shouldCompileRoleSpecificDag, } from "./dag-compiler-presets.js";
|
|
5
|
+
import { isAdaptorchRoutingEnabled, routeTopology, } from "./adaptorch-topology.js";
|
|
5
6
|
export async function compileInputEnvelopeToDag(input) {
|
|
6
7
|
const intent = input.intent ??
|
|
7
8
|
(await analyzeUserIntentV2({
|
|
@@ -38,7 +39,7 @@ export async function compileInputEnvelopeToDag(input) {
|
|
|
38
39
|
});
|
|
39
40
|
}
|
|
40
41
|
export function buildDagCompileResult(input) {
|
|
41
|
-
|
|
42
|
+
const result = {
|
|
42
43
|
schemaVersion: 1,
|
|
43
44
|
inputId: input.input.inputId,
|
|
44
45
|
runId: input.input.runId,
|
|
@@ -53,6 +54,18 @@ export function buildDagCompileResult(input) {
|
|
|
53
54
|
},
|
|
54
55
|
compiledAt: input.compiledAt ?? new Date().toISOString(),
|
|
55
56
|
};
|
|
57
|
+
// AdaptOrch topology routing — additive, non-fatal
|
|
58
|
+
try {
|
|
59
|
+
if (isAdaptorchRoutingEnabled()) {
|
|
60
|
+
const nodeIds = input.dag.nodes.map((n) => n.id);
|
|
61
|
+
const edges = input.dag.nodes.flatMap((n) => n.dependsOn.map((from) => ({ from, to: n.id })));
|
|
62
|
+
result.topology = routeTopology(nodeIds, edges);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
// non-fatal: topology is optional
|
|
67
|
+
}
|
|
68
|
+
return result;
|
|
56
69
|
}
|
|
57
70
|
function selectExecutionStrategy(requested, intent) {
|
|
58
71
|
return (requested ??
|
|
@@ -85,6 +85,12 @@ export declare class ParallelOrchestrator {
|
|
|
85
85
|
* 오케스트레이션 실행
|
|
86
86
|
*/
|
|
87
87
|
execute(): Promise<ParallelOrchestrationResult>;
|
|
88
|
+
/**
|
|
89
|
+
* Non-fatal finalizer: if a run-manifest.json was persisted for this run,
|
|
90
|
+
* link its run -> providerRoute -> provider / evidence / decision / artifact
|
|
91
|
+
* nodes into the local graph. Failures are swallowed so the run is unaffected.
|
|
92
|
+
*/
|
|
93
|
+
private linkRunManifestToGraph;
|
|
88
94
|
/**
|
|
89
95
|
* 오케스트레이션 중단
|
|
90
96
|
*/
|
|
@@ -161,6 +161,9 @@ export class ParallelOrchestrator {
|
|
|
161
161
|
// 상태 업데이트: 완료
|
|
162
162
|
this.stateManager.setStatus(success ? "completed" : "failed");
|
|
163
163
|
this.stateManager.setCompletedAt(new Date().toISOString());
|
|
164
|
+
// Wave-3 p8: link the finalized run manifest into the local graph.
|
|
165
|
+
// Non-fatal: a graph write failure must never fail the run.
|
|
166
|
+
await this.linkRunManifestToGraph();
|
|
164
167
|
// 결과 반환
|
|
165
168
|
return this.createResult(success);
|
|
166
169
|
}
|
|
@@ -176,6 +179,34 @@ export class ParallelOrchestrator {
|
|
|
176
179
|
await this.cleanup();
|
|
177
180
|
}
|
|
178
181
|
}
|
|
182
|
+
/**
|
|
183
|
+
* Non-fatal finalizer: if a run-manifest.json was persisted for this run,
|
|
184
|
+
* link its run -> providerRoute -> provider / evidence / decision / artifact
|
|
185
|
+
* nodes into the local graph. Failures are swallowed so the run is unaffected.
|
|
186
|
+
*/
|
|
187
|
+
async linkRunManifestToGraph() {
|
|
188
|
+
try {
|
|
189
|
+
const { readFile } = await import("fs/promises");
|
|
190
|
+
const { getRunArtifactPath } = await import("../util/run-store.js");
|
|
191
|
+
let raw;
|
|
192
|
+
try {
|
|
193
|
+
raw = await readFile(getRunArtifactPath(this.runId, "run-manifest.json", this.cwd), "utf-8");
|
|
194
|
+
}
|
|
195
|
+
catch {
|
|
196
|
+
return; // no manifest persisted for this run; nothing to link
|
|
197
|
+
}
|
|
198
|
+
const { RunManifestSchema } = await import("../schema/run-manifest.schema.js");
|
|
199
|
+
const parsed = RunManifestSchema.safeParse(JSON.parse(raw));
|
|
200
|
+
if (!parsed.success)
|
|
201
|
+
return;
|
|
202
|
+
const { linkRunToGraph } = await import("../memory/memory-store.js");
|
|
203
|
+
await linkRunToGraph(this.runId, parsed.data, { projectRoot: this.cwd });
|
|
204
|
+
}
|
|
205
|
+
catch (error) {
|
|
206
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
207
|
+
this.logStreamer.log("warn", `graph link skipped: ${message}`);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
179
210
|
/**
|
|
180
211
|
* 오케스트레이션 중단
|
|
181
212
|
*/
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Maps existing provider doctor payloads onto the shared {@link ProviderHealth}
|
|
3
|
+
* shape. This is additive: callers keep their original JSON keys and embed the
|
|
4
|
+
* result under a `health` key.
|
|
5
|
+
*
|
|
6
|
+
* Only type imports are used so the module has no runtime dependencies and can
|
|
7
|
+
* be unit-tested in isolation. The mapper never surfaces secret values — it
|
|
8
|
+
* relies on boolean signals (e.g. `apiKeySet`) and environment-variable *names*.
|
|
9
|
+
*/
|
|
10
|
+
import type { ProviderHealth } from "../contracts/provider-health.js";
|
|
11
|
+
import type { ProviderDoctorStatus } from "./model-registry.js";
|
|
12
|
+
/** DeepSeek `provider doctor` JSON object shape (balance preflight + config). */
|
|
13
|
+
export interface DeepSeekDoctorHealthInput {
|
|
14
|
+
provider: string;
|
|
15
|
+
available: boolean;
|
|
16
|
+
enabled?: boolean;
|
|
17
|
+
apiKeySet?: boolean;
|
|
18
|
+
checkedAt?: number;
|
|
19
|
+
reason?: string;
|
|
20
|
+
balance?: {
|
|
21
|
+
is_available?: boolean;
|
|
22
|
+
} | null;
|
|
23
|
+
}
|
|
24
|
+
/** Union of the doctor payloads the mapper understands. */
|
|
25
|
+
export type ProviderHealthInput = ProviderDoctorStatus | DeepSeekDoctorHealthInput;
|
|
26
|
+
/** Optional extra context (never carries secrets). */
|
|
27
|
+
export interface ProviderHealthExtras {
|
|
28
|
+
/** Resolvable model override (used when the input lacks a `model`). */
|
|
29
|
+
model?: string;
|
|
30
|
+
/** ISO timestamp override (mainly for deterministic tests). */
|
|
31
|
+
checkedAt?: string;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Maps a provider doctor payload onto the shared {@link ProviderHealth} shape.
|
|
35
|
+
*
|
|
36
|
+
* @param status A {@link ProviderDoctorStatus} or DeepSeek doctor object.
|
|
37
|
+
* @param extras Optional non-sensitive context overrides.
|
|
38
|
+
*/
|
|
39
|
+
export declare function toProviderHealth(status: ProviderHealthInput, extras?: ProviderHealthExtras): ProviderHealth;
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
const SHELL_KINDS = new Set(["external-cli", "codex-cli", "local"]);
|
|
2
|
+
const QUOTA_PATTERN = /balance|quota|insufficient|402|rate[\s_-]*limit/i;
|
|
3
|
+
function toAuthorityLevel(authority) {
|
|
4
|
+
switch (authority) {
|
|
5
|
+
case "authority":
|
|
6
|
+
case "full":
|
|
7
|
+
return "full";
|
|
8
|
+
case "direct":
|
|
9
|
+
return "direct";
|
|
10
|
+
case "advisory":
|
|
11
|
+
case "read-only":
|
|
12
|
+
return "advisory";
|
|
13
|
+
case "veto":
|
|
14
|
+
case "none":
|
|
15
|
+
return "none";
|
|
16
|
+
default:
|
|
17
|
+
return "advisory";
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
function hasResolvableModel(model) {
|
|
21
|
+
return typeof model === "string" && model.trim().length > 0 && model !== "default";
|
|
22
|
+
}
|
|
23
|
+
function isDoctorStatus(input) {
|
|
24
|
+
return typeof input.kind === "string";
|
|
25
|
+
}
|
|
26
|
+
// Classification relies on the already-derived boolean signals (quotaOk/authOk
|
|
27
|
+
// encode the doctor reason where relevant) so free-text reasons never override
|
|
28
|
+
// an explicit signal — and raw reason text is never echoed into remediation.
|
|
29
|
+
function classifyFailure(signals) {
|
|
30
|
+
const { runtimeOk, authOk, modelOk, quotaOk } = signals;
|
|
31
|
+
if (runtimeOk && authOk && modelOk && quotaOk) {
|
|
32
|
+
return { failureKind: "none", remediation: [] };
|
|
33
|
+
}
|
|
34
|
+
if (!quotaOk) {
|
|
35
|
+
return {
|
|
36
|
+
failureKind: "quota",
|
|
37
|
+
remediation: ["Check the provider balance/quota and top up or wait for the quota to reset."],
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
if (!authOk) {
|
|
41
|
+
return {
|
|
42
|
+
failureKind: "auth",
|
|
43
|
+
remediation: [
|
|
44
|
+
signals.apiKeyEnv
|
|
45
|
+
? `Set the ${signals.apiKeyEnv} environment variable, then re-run provider doctor.`
|
|
46
|
+
: "Configure provider authentication, then re-run provider doctor.",
|
|
47
|
+
],
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
if (!runtimeOk) {
|
|
51
|
+
if (signals.codexCliAvailable === false) {
|
|
52
|
+
return {
|
|
53
|
+
failureKind: "runtime",
|
|
54
|
+
remediation: ["Install the Codex CLI and ensure `codex` is on PATH."],
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
if (signals.enabled === false) {
|
|
58
|
+
return {
|
|
59
|
+
failureKind: "policy",
|
|
60
|
+
remediation: ["Enable the provider in OMK configuration before use."],
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
return {
|
|
64
|
+
failureKind: "runtime",
|
|
65
|
+
remediation: ["Verify the provider runtime is installed and reachable."],
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
if (!modelOk) {
|
|
69
|
+
return {
|
|
70
|
+
failureKind: "model",
|
|
71
|
+
remediation: ["Configure a resolvable default model for this provider."],
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
return {
|
|
75
|
+
failureKind: "unknown",
|
|
76
|
+
remediation: ["Review provider doctor output for details."],
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
function fromDoctorStatus(status, extras) {
|
|
80
|
+
const apiKeySet = status.apiKeySet;
|
|
81
|
+
const authMethod = status.authMethod;
|
|
82
|
+
const authOk = typeof apiKeySet === "boolean"
|
|
83
|
+
? apiKeySet
|
|
84
|
+
: authMethod === "api-key-env"
|
|
85
|
+
? false
|
|
86
|
+
: true; // external-cli / oauth / none: auth handled outside OMK.
|
|
87
|
+
const runtimeOk = status.available && status.codexCliAvailable !== false;
|
|
88
|
+
const modelOk = hasResolvableModel(extras?.model ?? status.model);
|
|
89
|
+
const quotaOk = !QUOTA_PATTERN.test(status.reason ?? "");
|
|
90
|
+
const capabilities = status.capabilities ?? [];
|
|
91
|
+
const baseLevel = toAuthorityLevel(status.authority);
|
|
92
|
+
const declaresMcp = capabilities.includes("mcp") || capabilities.includes("tools");
|
|
93
|
+
const { failureKind, remediation } = classifyFailure({
|
|
94
|
+
runtimeOk,
|
|
95
|
+
authOk,
|
|
96
|
+
modelOk,
|
|
97
|
+
quotaOk,
|
|
98
|
+
enabled: status.enabled,
|
|
99
|
+
codexCliAvailable: status.codexCliAvailable,
|
|
100
|
+
apiKeyEnv: status.apiKeyEnv,
|
|
101
|
+
});
|
|
102
|
+
return {
|
|
103
|
+
provider: status.provider,
|
|
104
|
+
checkedAt: extras?.checkedAt ?? new Date().toISOString(),
|
|
105
|
+
runtimeOk,
|
|
106
|
+
authOk,
|
|
107
|
+
modelOk,
|
|
108
|
+
quotaOk,
|
|
109
|
+
writeAuthority: baseLevel,
|
|
110
|
+
shellAuthority: SHELL_KINDS.has(status.kind) ? baseLevel : "none",
|
|
111
|
+
mcpAuthority: declaresMcp
|
|
112
|
+
? baseLevel
|
|
113
|
+
: baseLevel === "full" || baseLevel === "direct"
|
|
114
|
+
? "advisory"
|
|
115
|
+
: "none",
|
|
116
|
+
failureKind,
|
|
117
|
+
remediation,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
function fromDeepSeekDoctor(input, extras) {
|
|
121
|
+
const apiKeySet = input.apiKeySet;
|
|
122
|
+
const authOk = typeof apiKeySet === "boolean" ? apiKeySet : true;
|
|
123
|
+
const runtimeOk = input.available && input.enabled !== false;
|
|
124
|
+
const balanceUnavailable = input.balance?.is_available === false;
|
|
125
|
+
const quotaOk = !balanceUnavailable && !QUOTA_PATTERN.test(input.reason ?? "");
|
|
126
|
+
// DeepSeek is a known provider whose default model always resolves from the
|
|
127
|
+
// registry, so treat the model as resolvable unless an explicit override fails.
|
|
128
|
+
const modelOk = extras?.model ? hasResolvableModel(extras.model) : true;
|
|
129
|
+
const { failureKind, remediation } = classifyFailure({
|
|
130
|
+
runtimeOk,
|
|
131
|
+
authOk,
|
|
132
|
+
modelOk,
|
|
133
|
+
quotaOk,
|
|
134
|
+
enabled: input.enabled,
|
|
135
|
+
});
|
|
136
|
+
const checkedAt = extras?.checkedAt ??
|
|
137
|
+
(typeof input.checkedAt === "number" ? new Date(input.checkedAt).toISOString() : new Date().toISOString());
|
|
138
|
+
// DeepSeek participates as an advisory, read-only opportunistic worker.
|
|
139
|
+
return {
|
|
140
|
+
provider: input.provider,
|
|
141
|
+
checkedAt,
|
|
142
|
+
runtimeOk,
|
|
143
|
+
authOk,
|
|
144
|
+
modelOk,
|
|
145
|
+
quotaOk,
|
|
146
|
+
writeAuthority: "advisory",
|
|
147
|
+
shellAuthority: "none",
|
|
148
|
+
mcpAuthority: "none",
|
|
149
|
+
failureKind,
|
|
150
|
+
remediation,
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Maps a provider doctor payload onto the shared {@link ProviderHealth} shape.
|
|
155
|
+
*
|
|
156
|
+
* @param status A {@link ProviderDoctorStatus} or DeepSeek doctor object.
|
|
157
|
+
* @param extras Optional non-sensitive context overrides.
|
|
158
|
+
*/
|
|
159
|
+
export function toProviderHealth(status, extras) {
|
|
160
|
+
return isDoctorStatus(status) ? fromDoctorStatus(status, extras) : fromDeepSeekDoctor(status, extras);
|
|
161
|
+
}
|
|
@@ -11,15 +11,24 @@ import type { RunState } from "../contracts/orchestration.js";
|
|
|
11
11
|
import { type ContextCapsule } from './context-capsule.js';
|
|
12
12
|
import type { ContextAdjustment } from "../evidence/attempt-record.js";
|
|
13
13
|
import { type ContextBudgetReport } from "./context-budget-optimizer.js";
|
|
14
|
+
import { type HeadroomDecision } from "./headroom-policy.js";
|
|
14
15
|
export interface ContextBrokerOptions {
|
|
15
16
|
readonly projectRoot?: string;
|
|
16
17
|
readonly graphMemoryPath?: string;
|
|
17
18
|
readonly goal?: string;
|
|
18
19
|
readonly system?: string;
|
|
20
|
+
/**
|
|
21
|
+
* Model context window size in tokens. Used for headroom compaction
|
|
22
|
+
* threshold evaluation. Default: OMK_CONTEXT_WINDOW env or 200000.
|
|
23
|
+
*/
|
|
24
|
+
readonly contextWindow?: number;
|
|
25
|
+
}
|
|
26
|
+
export interface ContextBrokerResult {
|
|
27
|
+
readonly capsule: ContextCapsule;
|
|
28
|
+
readonly report: ContextBudgetReport;
|
|
29
|
+
/** Headroom compaction decision — additive; existing consumers unaffected. */
|
|
30
|
+
readonly headroomDecision: HeadroomDecision;
|
|
19
31
|
}
|
|
20
32
|
export declare function createContextBroker(options?: ContextBrokerOptions): {
|
|
21
|
-
buildCapsule: (node: DagNode, state?: RunState, adjustment?: ContextAdjustment) => Promise<
|
|
22
|
-
capsule: ContextCapsule;
|
|
23
|
-
report: ContextBudgetReport;
|
|
24
|
-
}>;
|
|
33
|
+
buildCapsule: (node: DagNode, state?: RunState, adjustment?: ContextAdjustment) => Promise<ContextBrokerResult>;
|
|
25
34
|
};
|
|
@@ -4,6 +4,8 @@ import { join } from "path";
|
|
|
4
4
|
import { mkdir, readFile, stat, writeFile } from "fs/promises";
|
|
5
5
|
import { createContextBudgetOptimizer } from "./context-budget-optimizer.js";
|
|
6
6
|
import { createDecisionTraceStore } from "../evidence/decision-trace.js";
|
|
7
|
+
import { evaluateHeadroom } from "./headroom-policy.js";
|
|
8
|
+
const DEFAULT_CONTEXT_WINDOW = 200_000;
|
|
7
9
|
function resolveBudget(node) {
|
|
8
10
|
const preset = node.routing?.contextBudget ?? "small";
|
|
9
11
|
return CONTEXT_BUDGET_PRESETS[preset] ?? DEFAULT_CONTEXT_BUDGET;
|
|
@@ -302,7 +304,18 @@ export function createContextBroker(options = {}) {
|
|
|
302
304
|
attemptId: `${node.id}__${attemptId}`,
|
|
303
305
|
});
|
|
304
306
|
}
|
|
305
|
-
|
|
307
|
+
// Evaluate headroom compaction threshold (advisory only — never blocks)
|
|
308
|
+
const resolvedContextWindow = options.contextWindow
|
|
309
|
+
?? Number(process.env.OMK_CONTEXT_WINDOW ?? DEFAULT_CONTEXT_WINDOW);
|
|
310
|
+
const headroomDecision = evaluateHeadroom({
|
|
311
|
+
usedTokens: optimized.report.totalTokensEstimated,
|
|
312
|
+
contextWindow: resolvedContextWindow,
|
|
313
|
+
});
|
|
314
|
+
return {
|
|
315
|
+
capsule: optimized.capsule,
|
|
316
|
+
report: optimized.report,
|
|
317
|
+
headroomDecision,
|
|
318
|
+
};
|
|
306
319
|
}
|
|
307
320
|
return { buildCapsule };
|
|
308
321
|
}
|