open-multi-agent-kit 0.90.4 → 0.90.5
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 +22 -0
- package/README.md +8 -8
- package/dist/core/adaptorch-bridge.d.ts +14 -21
- package/dist/core/adaptorch-bridge.d.ts.map +1 -1
- package/dist/core/adaptorch-bridge.js +11 -18
- package/dist/core/adaptorch-bridge.js.map +1 -1
- package/dist/core/agent-session.d.ts +6 -39
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +7 -99
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/context-budget-v2-planner.d.ts.map +1 -1
- package/dist/core/context-budget-v2-planner.js +10 -2
- package/dist/core/context-budget-v2-planner.js.map +1 -1
- package/dist/core/context-budget-v2-types.d.ts +7 -1
- package/dist/core/context-budget-v2-types.d.ts.map +1 -1
- package/dist/core/context-budget-v2-types.js.map +1 -1
- package/dist/core/reasoning-router-bias.d.ts +11 -23
- package/dist/core/reasoning-router-bias.d.ts.map +1 -1
- package/dist/core/reasoning-router-bias.js +9 -18
- package/dist/core/reasoning-router-bias.js.map +1 -1
- package/dist/core/reasoning-router-resolver.d.ts +44 -0
- package/dist/core/reasoning-router-resolver.d.ts.map +1 -0
- package/dist/core/reasoning-router-resolver.js +79 -0
- package/dist/core/reasoning-router-resolver.js.map +1 -0
- package/dist/core/reasoning-router-v4-weights.d.ts +28 -72
- package/dist/core/reasoning-router-v4-weights.d.ts.map +1 -1
- package/dist/core/reasoning-router-v4-weights.js +16 -60
- package/dist/core/reasoning-router-v4-weights.js.map +1 -1
- package/dist/core/reasoning-router-v4.d.ts +25 -70
- package/dist/core/reasoning-router-v4.d.ts.map +1 -1
- package/dist/core/reasoning-router-v4.js +46 -144
- package/dist/core/reasoning-router-v4.js.map +1 -1
- package/dist/core/router-feedback-collector.d.ts +13 -16
- package/dist/core/router-feedback-collector.d.ts.map +1 -1
- package/dist/core/router-feedback-collector.js +5 -8
- package/dist/core/router-feedback-collector.js.map +1 -1
- package/dist/core/slash-commands.d.ts.map +1 -1
- package/dist/core/slash-commands.js +1 -1
- package/dist/core/slash-commands.js.map +1 -1
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +16 -98
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/docs/usage.md +11 -12
- package/examples/extensions/custom-provider-anthropic/package-lock.json +2 -2
- package/examples/extensions/custom-provider-anthropic/package.json +1 -1
- package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
- package/examples/extensions/gondolin/package-lock.json +2 -2
- package/examples/extensions/gondolin/package.json +1 -1
- package/examples/extensions/sandbox/package-lock.json +2 -2
- package/examples/extensions/sandbox/package.json +1 -1
- package/examples/extensions/subagent/agent-capability-router.test.ts +123 -0
- package/examples/extensions/subagent/agent-capability-router.ts +274 -0
- package/examples/extensions/subagent/agents/planner.md +3 -0
- package/examples/extensions/subagent/agents/reviewer.md +3 -0
- package/examples/extensions/subagent/agents/scout.md +3 -0
- package/examples/extensions/subagent/agents/worker.md +2 -0
- package/examples/extensions/subagent/agents.ts +14 -0
- package/examples/extensions/subagent/capabilities.ts +368 -0
- package/examples/extensions/subagent/domain-profiles.ts +1089 -0
- package/examples/extensions/subagent/index.ts +71 -3
- package/examples/extensions/with-deps/package-lock.json +2 -2
- package/examples/extensions/with-deps/package.json +1 -1
- package/examples/sdk/12-full-control.ts +1 -1
- package/npm-shrinkwrap.json +12 -12
- package/package.json +4 -4
- package/dist/core/compactor.d.ts +0 -42
- package/dist/core/compactor.d.ts.map +0 -1
- package/dist/core/compactor.js +0 -128
- package/dist/core/compactor.js.map +0 -1
- package/dist/core/reasoning-router-v2.d.ts +0 -94
- package/dist/core/reasoning-router-v2.d.ts.map +0 -1
- package/dist/core/reasoning-router-v2.js +0 -288
- package/dist/core/reasoning-router-v2.js.map +0 -1
- package/dist/core/reasoning-router-v3.d.ts +0 -15
- package/dist/core/reasoning-router-v3.d.ts.map +0 -1
- package/dist/core/reasoning-router-v3.js +0 -193
- package/dist/core/reasoning-router-v3.js.map +0 -1
- package/dist/core/reasoning-router-weights.d.ts +0 -155
- package/dist/core/reasoning-router-weights.d.ts.map +0 -1
- package/dist/core/reasoning-router-weights.js +0 -204
- package/dist/core/reasoning-router-weights.js.map +0 -1
- package/dist/core/reasoning-router.d.ts +0 -44
- package/dist/core/reasoning-router.d.ts.map +0 -1
- package/dist/core/reasoning-router.js +0 -138
- package/dist/core/reasoning-router.js.map +0 -1
- package/dist/core/token-optimizer.d.ts +0 -58
- package/dist/core/token-optimizer.d.ts.map +0 -1
- package/dist/core/token-optimizer.js +0 -179
- package/dist/core/token-optimizer.js.map +0 -1
package/docs/usage.md
CHANGED
|
@@ -38,7 +38,7 @@ Type `/` in the editor to open command completion. Extensions can register custo
|
|
|
38
38
|
|---------|-------------|
|
|
39
39
|
| `/login`, `/logout` | Manage OAuth or API-key credentials |
|
|
40
40
|
| `/model` | Switch models, then choose thinking level |
|
|
41
|
-
| `/think` | Choose thinking level, or `auto
|
|
41
|
+
| `/think` | Choose thinking level, or `auto` to route per task through v4 |
|
|
42
42
|
| `/scoped-models` | Enable/disable models for Ctrl+P cycling |
|
|
43
43
|
| `/settings` | Thinking level, theme, message delivery, transport |
|
|
44
44
|
| `/resume` | Pick from previous sessions |
|
|
@@ -61,35 +61,34 @@ Type `/` in the editor to open command completion. Extensions can register custo
|
|
|
61
61
|
|
|
62
62
|
`/think <level>` sets the thinking level manually. Manual levels always win: choosing a concrete level leaves auto routing, so no auto router runs.
|
|
63
63
|
|
|
64
|
-
`/think auto`
|
|
64
|
+
`/think auto` enables the deterministic local v4 router (no network calls). Versioned auto commands are no longer accepted; choose `/think auto` for automatic routing or `/think <level>` for a manual override.
|
|
65
65
|
|
|
66
|
-
The
|
|
66
|
+
The auto router classifies each prompt into a task class (trivial, simple edit, code generation, debug, refactor, review, plan) and maps it to a recommended level, from `minimal` for trivial prompts up to `xhigh` for planning work.
|
|
67
67
|
|
|
68
68
|
The routing core is deterministic and local. It looks only at bounded turn signals such as:
|
|
69
69
|
|
|
70
70
|
- prompt length
|
|
71
71
|
- presence of code fences or diff markers
|
|
72
|
-
- keyword families
|
|
73
|
-
- leading intent, localized edit objects, diagnostic evidence, review scope, plan briefs, refactor cues, and implementation objects
|
|
74
|
-
- bounded negation of
|
|
75
|
-
- recent auto-router task history in the session
|
|
76
|
-
- context pressure buckets
|
|
72
|
+
- keyword families
|
|
73
|
+
- leading intent, localized edit objects, diagnostic evidence, review scope, plan briefs, refactor cues, and implementation objects
|
|
74
|
+
- bounded negation of whole-prompt matches and short-range compound-intent detection across a leading conjunction
|
|
75
|
+
- recent auto-router task history in the session
|
|
76
|
+
- context pressure buckets
|
|
77
77
|
- the subagent lane type, when one is set
|
|
78
78
|
|
|
79
|
-
v4
|
|
79
|
+
v4 reports a confidence band (`high`, `medium`, `low`), the score margin between the top two task classes, and a fallback reason on every turn, none of which carry prompt text. When confidence is low or no weighted signal decided the class, v4 raises the resolved level by one step above what the same task class would otherwise resolve to; confidence never lowers it. v4 is checked against a gold-set evaluation harness with fixed train/dev/holdout splits and accuracy, macro-F1, severe-under-allocation, and class-flip/McNemar checks.
|
|
80
80
|
|
|
81
81
|
Precedence:
|
|
82
82
|
|
|
83
83
|
- Manual `/think <level>` always wins.
|
|
84
84
|
- The router only resolves levels while `auto` mode is active.
|
|
85
85
|
- Auto-resolved levels apply per turn only and never overwrite the persisted default thinking level in settings.
|
|
86
|
-
- v1 remains the default auto router; v2, v3, and v4 must be selected explicitly for the current session.
|
|
87
86
|
|
|
88
87
|
Resolved levels are clamped to the model's capabilities: models without `xhigh`/`max` are capped at their highest supported level, and models without reasoning support bypass the router entirely.
|
|
89
88
|
|
|
90
|
-
The v4 learning path is available only through the global `reasoningRouterLearning` setting and is off by default. When `reasoningRouterLearning.enabled` is `true`, auto
|
|
89
|
+
The v4 learning path is available only through the global `reasoningRouterLearning` setting and is off by default. When `reasoningRouterLearning.enabled` is `true`, `/think auto` loads one validated bias snapshot for the session, applies a bounded `-2..2` ladder-step bias, and appends a privacy-safe feedback record containing only bounded enums, booleans, and buckets. Project-local settings cannot enable or redirect this feature, and the ledger never stores raw prompts, file paths, diffs, session identifiers, model/provider payloads, tool output, or hook output.
|
|
91
90
|
|
|
92
|
-
The Adaptorch advisory bridge module still ships as default-off groundwork only. It has no settings key, command, transport, or session call site yet, so it does not affect `/think auto
|
|
91
|
+
The Adaptorch advisory bridge module still ships as default-off groundwork only. It has no settings key, command, transport, or session call site yet, so it does not affect `/think auto` until a future transport and security review explicitly wire it.
|
|
93
92
|
|
|
94
93
|
## Message Queue
|
|
95
94
|
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "omk-extension-custom-provider",
|
|
3
|
-
"version": "0.90.
|
|
3
|
+
"version": "0.90.5",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "omk-extension-custom-provider",
|
|
9
|
-
"version": "0.90.
|
|
9
|
+
"version": "0.90.5",
|
|
10
10
|
"dependencies": {
|
|
11
11
|
"@anthropic-ai/sdk": "^0.52.0"
|
|
12
12
|
}
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "omk-extension-gondolin",
|
|
3
|
-
"version": "0.90.
|
|
3
|
+
"version": "0.90.5",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "omk-extension-gondolin",
|
|
9
|
-
"version": "0.90.
|
|
9
|
+
"version": "0.90.5",
|
|
10
10
|
"dependencies": {
|
|
11
11
|
"@earendil-works/gondolin": "0.12.0"
|
|
12
12
|
}
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "omk-extension-sandbox",
|
|
3
|
-
"version": "0.90.
|
|
3
|
+
"version": "0.90.5",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "omk-extension-sandbox",
|
|
9
|
-
"version": "0.90.
|
|
9
|
+
"version": "0.90.5",
|
|
10
10
|
"dependencies": {
|
|
11
11
|
"@anthropic-ai/sandbox-runtime": "0.0.26"
|
|
12
12
|
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deterministic unit tests for the agent → capability router.
|
|
3
|
+
*
|
|
4
|
+
* The router is fully deterministic (no LLM, no randomness, no I/O inside the
|
|
5
|
+
* router itself), so every assertion here is an exact expected value. The only
|
|
6
|
+
* I/O is the one-time module-scope catalog build, which scans the live skill
|
|
7
|
+
* tree under the agent directory.
|
|
8
|
+
*/
|
|
9
|
+
import { describe, expect, it } from "vitest";
|
|
10
|
+
import { auditCapabilities, classifyAgent, deriveCapabilities } from "./agent-capability-router.ts";
|
|
11
|
+
import { buildCapabilityCatalog } from "./capabilities.ts";
|
|
12
|
+
|
|
13
|
+
// Built once at module scope from the live skill tree under the agent
|
|
14
|
+
// directory. This is populated during local verification but empty in CI (the
|
|
15
|
+
// agent skill catalog is not checked out on the runner), so the skill-catalog
|
|
16
|
+
// -dependent assertions below are guarded with `skipIf(!hasSkillCatalog)`. The
|
|
17
|
+
// MCP/hooks sets are a fixed OMK list and stay populated everywhere.
|
|
18
|
+
const catalog = buildCapabilityCatalog({ agentDir: `${process.env.HOME ?? "/home/yu"}/.omk/agent` });
|
|
19
|
+
const hasSkillCatalog = catalog.skills.size > 0;
|
|
20
|
+
|
|
21
|
+
describe("classifyAgent", () => {
|
|
22
|
+
it("classifies by name token: seo-specialist → marketing-content", () => {
|
|
23
|
+
expect(classifyAgent("seo-specialist", "x")?.id).toBe("marketing-content");
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it("classifies smart-contract-auditor → security", () => {
|
|
27
|
+
expect(classifyAgent("smart-contract-auditor", "x")?.id).toBe("security");
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("classifies react-developer → frontend-web", () => {
|
|
31
|
+
expect(classifyAgent("react-developer", "x")?.id).toBe("frontend-web");
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("guards the gin→engineer substring regression: ml-engineer is NOT backend-api", () => {
|
|
35
|
+
// "engineer" contains "gin"; a naive first-match classifier would route
|
|
36
|
+
// this to backend-api. Boundary-anchored voting must not.
|
|
37
|
+
expect(classifyAgent("ml-engineer", "AI/ML")?.id).not.toBe("backend-api");
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("guards the gis→strategist substring regression: account-strategist is NOT gis-spatial", () => {
|
|
41
|
+
// "strategist" contains "gis"; must not flip into the GIS domain.
|
|
42
|
+
expect(classifyAgent("account-strategist", "x")?.id).not.toBe("gis-spatial");
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("returns null when no domain matches", () => {
|
|
46
|
+
expect(classifyAgent("zzz-nonexistent-xyz", "nothing relevant here")).toBeNull();
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
describe("deriveCapabilities", () => {
|
|
51
|
+
it.skipIf(!hasSkillCatalog)(
|
|
52
|
+
"weights the name token: react-developer's first derived skill is react-patterns",
|
|
53
|
+
() => {
|
|
54
|
+
const { skills } = deriveCapabilities("react-developer", "React frontend", catalog);
|
|
55
|
+
expect(skills[0]).toBe("react-patterns");
|
|
56
|
+
},
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
it.skipIf(!hasSkillCatalog)("includes the literal domain skill for seo-specialist", () => {
|
|
60
|
+
const { skills } = deriveCapabilities("seo-specialist", "SEO", catalog);
|
|
61
|
+
expect(skills).toContain("seo");
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("returns empty skills for an unmatched domain", () => {
|
|
65
|
+
const { skills } = deriveCapabilities("zzz-unknown", "x", catalog);
|
|
66
|
+
expect(skills.length).toBe(0);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it.skipIf(!hasSkillCatalog)("only emits skills that exist in the live catalog", () => {
|
|
70
|
+
const { skills } = deriveCapabilities("react-developer", "React frontend", catalog);
|
|
71
|
+
expect(skills.length).toBeGreaterThan(0);
|
|
72
|
+
for (const skill of skills) {
|
|
73
|
+
expect(catalog.skills.has(skill)).toBe(true);
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("derived MCP is a subset of the catalog MCP set", () => {
|
|
78
|
+
const { mcp } = deriveCapabilities("devops-automator", "kubernetes", catalog);
|
|
79
|
+
expect(mcp.length).toBeGreaterThan(0);
|
|
80
|
+
for (const server of mcp) {
|
|
81
|
+
expect(catalog.mcp.has(server)).toBe(true);
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
describe("auditCapabilities", () => {
|
|
87
|
+
it("identical sets → jaccard 1.0 and verdict match", () => {
|
|
88
|
+
const caps = { skills: ["react-patterns"], mcp: [], hooks: [] };
|
|
89
|
+
const result = auditCapabilities(caps, caps, catalog);
|
|
90
|
+
expect(result.jaccard).toBe(1.0);
|
|
91
|
+
expect(result.verdict).toBe("match");
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("disjoint sets → jaccard 0 and verdict divergent", () => {
|
|
95
|
+
const declared = { skills: ["react-patterns"], mcp: [], hooks: [] };
|
|
96
|
+
const derived = { skills: ["seo"], mcp: [], hooks: [] };
|
|
97
|
+
const result = auditCapabilities(declared, derived, catalog);
|
|
98
|
+
expect(result.jaccard).toBe(0);
|
|
99
|
+
expect(result.verdict).toBe("divergent");
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it("half overlap → jaccard within [0,1] and verdict drift or match", () => {
|
|
103
|
+
const declared = { skills: ["react-patterns", "seo"], mcp: [], hooks: [] };
|
|
104
|
+
const derived = { skills: ["react-patterns"], mcp: [], hooks: [] };
|
|
105
|
+
const result = auditCapabilities(declared, derived, catalog);
|
|
106
|
+
expect(result.jaccard).toBeGreaterThanOrEqual(0);
|
|
107
|
+
expect(result.jaccard).toBeLessThanOrEqual(1);
|
|
108
|
+
expect(["drift", "match"]).toContain(result.verdict);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it("surfaces declared-but-unknown skills in declaredUnknownSkills", () => {
|
|
112
|
+
const declared = { skills: ["fake-skill-xyz"], mcp: [], hooks: [] };
|
|
113
|
+
const derived = { skills: [], mcp: [], hooks: [] };
|
|
114
|
+
const result = auditCapabilities(declared, derived, catalog);
|
|
115
|
+
expect(result.declaredUnknownSkills).toContain("fake-skill-xyz");
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it("both-empty skills → jaccard 1.0", () => {
|
|
119
|
+
const empty = { skills: [], mcp: [], hooks: [] };
|
|
120
|
+
const result = auditCapabilities(empty, empty, catalog);
|
|
121
|
+
expect(result.jaccard).toBe(1.0);
|
|
122
|
+
});
|
|
123
|
+
});
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deterministic agent → capability router.
|
|
3
|
+
*
|
|
4
|
+
* Classifies an agent (by name + description) into a {@link DomainProfile},
|
|
5
|
+
* derives a baseline capability set (skills / MCP / hooks) from the live
|
|
6
|
+
* {@link CapabilityCatalog}, and audits a declared capability set against the
|
|
7
|
+
* deterministic derivation (Jaccard overlap + drift verdict).
|
|
8
|
+
*
|
|
9
|
+
* Fully deterministic: same inputs always yield same outputs. No LLM calls,
|
|
10
|
+
* no randomness, no I/O. Depends only on {@link DOMAIN_PROFILES} and the
|
|
11
|
+
* passed-in catalog.
|
|
12
|
+
*
|
|
13
|
+
* Classification is **name-weighted vote scoring**: a domain's keywords are
|
|
14
|
+
* tested against the agent name and description; matches inside the name (the
|
|
15
|
+
* strongest identity signal) count triple. The domain with the highest score
|
|
16
|
+
* wins (ties resolved by array order, so specific domains should come first).
|
|
17
|
+
* This is robust to the substring false positives that plague first-match
|
|
18
|
+
* classification (e.g. `chem` matching "s**chem**a", `gis` matching
|
|
19
|
+
* "strate**gis**t"): a single accidental description hit cannot outvote a
|
|
20
|
+
* domain that matches the name.
|
|
21
|
+
*
|
|
22
|
+
* @module agent-capability-router
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import type { AgentCapabilities, CapabilityCatalog } from "./capabilities.ts";
|
|
26
|
+
import { validateCapabilities } from "./capabilities.ts";
|
|
27
|
+
import type { DomainProfile } from "./domain-profiles.ts";
|
|
28
|
+
import { DOMAIN_PROFILES } from "./domain-profiles.ts";
|
|
29
|
+
|
|
30
|
+
/** Maximum number of scored skills to grant. */
|
|
31
|
+
const MAX_SKILLS = 6;
|
|
32
|
+
/** Number of pool skills granted as a fallback when nothing scores above zero. */
|
|
33
|
+
const FALLBACK_SKILLS = 4;
|
|
34
|
+
/** Jaccard threshold (inclusive) for a "match" verdict. */
|
|
35
|
+
const JACCARD_MATCH = 0.5;
|
|
36
|
+
/** Jaccard threshold (inclusive) for a "drift" verdict. */
|
|
37
|
+
const JACCARD_DRIFT = 0.15;
|
|
38
|
+
/** Weight multiplier when a keyword or skill token matches an agent *name* token. */
|
|
39
|
+
const NAME_WEIGHT = 3;
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Generic role-suffix tokens that carry no domain signal and must not be
|
|
43
|
+
* counted as matches (otherwise e.g. "react-developer" would tie
|
|
44
|
+
* "angular-developer" on the "developer" token). These are stripped before
|
|
45
|
+
* both classification voting and derive scoring.
|
|
46
|
+
*/
|
|
47
|
+
const GENERIC_ROLE_TOKENS: ReadonlySet<string> = new Set([
|
|
48
|
+
"agent",
|
|
49
|
+
"analyst",
|
|
50
|
+
"architect",
|
|
51
|
+
"auditor",
|
|
52
|
+
"builder",
|
|
53
|
+
"coach",
|
|
54
|
+
"consultant",
|
|
55
|
+
"coordinator",
|
|
56
|
+
"creator",
|
|
57
|
+
"designer",
|
|
58
|
+
"developer",
|
|
59
|
+
"director",
|
|
60
|
+
"editor",
|
|
61
|
+
"engineer",
|
|
62
|
+
"expert",
|
|
63
|
+
"guardian",
|
|
64
|
+
"lead",
|
|
65
|
+
"manager",
|
|
66
|
+
"master",
|
|
67
|
+
"mentor",
|
|
68
|
+
"navigator",
|
|
69
|
+
"officer",
|
|
70
|
+
"operator",
|
|
71
|
+
"planner",
|
|
72
|
+
"producer",
|
|
73
|
+
"reviewer",
|
|
74
|
+
"specialist",
|
|
75
|
+
"steward",
|
|
76
|
+
"strategist",
|
|
77
|
+
"tester",
|
|
78
|
+
"writer",
|
|
79
|
+
"engineers",
|
|
80
|
+
"specialists",
|
|
81
|
+
"managers",
|
|
82
|
+
"developers",
|
|
83
|
+
]);
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Classify an agent by name + description against {@link DOMAIN_PROFILES} using
|
|
87
|
+
* name-weighted vote scoring. Returns the highest-scoring domain (ties resolve
|
|
88
|
+
* to the earliest in {@link DOMAIN_PROFILES}), or `null` when every domain
|
|
89
|
+
* scores zero — the explicit "No direct OMK skill match" sentinel.
|
|
90
|
+
*/
|
|
91
|
+
export function classifyAgent(name: string, description: string): DomainProfile | null {
|
|
92
|
+
const nameLower = name.toLowerCase();
|
|
93
|
+
const text = `${name} ${description}`.toLowerCase();
|
|
94
|
+
|
|
95
|
+
let best: DomainProfile | null = null;
|
|
96
|
+
let bestScore = 0;
|
|
97
|
+
for (const domain of DOMAIN_PROFILES) {
|
|
98
|
+
let score = 0;
|
|
99
|
+
for (const re of domain.keywords) {
|
|
100
|
+
// Boundary-anchored match: a keyword only counts when it lands on a
|
|
101
|
+
// word edge, so bare short keywords ("gin", "gis", "chem") cannot fire
|
|
102
|
+
// inside unrelated words ("engineer", "strategist", "schema").
|
|
103
|
+
if (!keywordHits(re, text)) continue;
|
|
104
|
+
// A keyword that also matches the agent name is a strong identity
|
|
105
|
+
// signal (the name is the most reliable classifier); otherwise it
|
|
106
|
+
// is a weaker description-only hit.
|
|
107
|
+
score += keywordHits(re, nameLower) ? NAME_WEIGHT : 1;
|
|
108
|
+
}
|
|
109
|
+
if (score > bestScore) {
|
|
110
|
+
bestScore = score;
|
|
111
|
+
best = domain;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return best;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** Split lowercased text into identity tokens: non-word boundaries, length >= 2. */
|
|
118
|
+
function tokenize(lowerText: string): string[] {
|
|
119
|
+
const tokens: string[] = [];
|
|
120
|
+
for (const raw of lowerText.split(/[^a-z0-9+#]+/)) {
|
|
121
|
+
if (raw.length >= 2) tokens.push(raw);
|
|
122
|
+
}
|
|
123
|
+
return tokens;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** Identity tokens with generic role suffixes removed. */
|
|
127
|
+
function identityTokens(lowerText: string): string[] {
|
|
128
|
+
return tokenize(lowerText).filter((t) => !GENERIC_ROLE_TOKENS.has(t));
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Boundary-anchored keyword test. A keyword must start at a left word edge
|
|
133
|
+
* (start-of-text or a non-`[a-z0-9]` neighbor). A **right** boundary is added
|
|
134
|
+
* only for short, all-alphabetic keywords (length <= 4, e.g. "gin", "gis",
|
|
135
|
+
* "chem", "seo"): those are the ones that cause substring false positives
|
|
136
|
+
* ("gin" -> "engineer", "gis" -> "strategist", "chem" -> "schema"). Longer and
|
|
137
|
+
* pattern keywords ("react", "3d-", "smart-?contract", "document-?generat")
|
|
138
|
+
* use prefix semantics (left edge only) so intentional prefix/partial matches
|
|
139
|
+
* still fire. Node >= 10 supports lookbehind.
|
|
140
|
+
*/
|
|
141
|
+
function keywordHits(re: RegExp, text: string): boolean {
|
|
142
|
+
const src = re.source;
|
|
143
|
+
const shortBare = src.length <= 4 && /^[a-z]+$/i.test(src);
|
|
144
|
+
const right = shortBare ? "(?![a-z0-9])" : "";
|
|
145
|
+
return new RegExp(`(?<![a-z0-9])${src}${right}`, "i").test(text);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Score a candidate skill against the agent's name and description tokens.
|
|
150
|
+
* The skill's **first** `-`-split token is the technology word (e.g. "react"
|
|
151
|
+
* in "react-patterns") and is weighted heaviest; a match in the agent name is
|
|
152
|
+
* worth {@link NAME_WEIGHT}x a description match. Generic tokens are skipped.
|
|
153
|
+
*/
|
|
154
|
+
function skillMatchScore(
|
|
155
|
+
skill: string,
|
|
156
|
+
nameTokens: ReadonlySet<string>,
|
|
157
|
+
descTokens: ReadonlySet<string>,
|
|
158
|
+
nameLower: string,
|
|
159
|
+
descLower: string,
|
|
160
|
+
): number {
|
|
161
|
+
const parts = skill.split("-");
|
|
162
|
+
let score = 0;
|
|
163
|
+
for (let i = 0; i < parts.length; i++) {
|
|
164
|
+
const part = parts[i];
|
|
165
|
+
if (part.length === 0 || GENERIC_ROLE_TOKENS.has(part)) continue;
|
|
166
|
+
const isTech = i === 0; // first token = the technology word
|
|
167
|
+
const nameHit = nameTokens.has(part);
|
|
168
|
+
const descHit = descTokens.has(part);
|
|
169
|
+
if (nameHit) score += (isTech ? 5 : 2) * NAME_WEIGHT;
|
|
170
|
+
else if (descHit) score += isTech ? 5 : 2;
|
|
171
|
+
}
|
|
172
|
+
// Full skill string appearing verbatim in name or description is a strong signal.
|
|
173
|
+
if (nameLower.includes(skill)) score += 3 * NAME_WEIGHT;
|
|
174
|
+
else if (descLower.includes(skill)) score += 3;
|
|
175
|
+
return score;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** Dedupe a list preserving first-seen order. */
|
|
179
|
+
function dedupe(items: readonly string[]): string[] {
|
|
180
|
+
const seen = new Set<string>();
|
|
181
|
+
const out: string[] = [];
|
|
182
|
+
for (const item of items) {
|
|
183
|
+
if (seen.has(item)) continue;
|
|
184
|
+
seen.add(item);
|
|
185
|
+
out.push(item);
|
|
186
|
+
}
|
|
187
|
+
return out;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Deterministically derive a baseline capability set for an agent. When no
|
|
192
|
+
* domain matches, all three lists are empty. Otherwise skills are picked from
|
|
193
|
+
* the domain's pool by name/description relevance (top {@link MAX_SKILLS} with
|
|
194
|
+
* a positive score, falling back to the first {@link FALLBACK_SKILLS} pool
|
|
195
|
+
* entries when nothing scores above zero), and MCP/hooks are the domain
|
|
196
|
+
* defaults intersected with the live catalog.
|
|
197
|
+
*/
|
|
198
|
+
export function deriveCapabilities(name: string, description: string, catalog: CapabilityCatalog): AgentCapabilities {
|
|
199
|
+
const domain = classifyAgent(name, description);
|
|
200
|
+
if (domain === null) return { skills: [], mcp: [], hooks: [] };
|
|
201
|
+
|
|
202
|
+
const nameLower = name.toLowerCase();
|
|
203
|
+
const descLower = description.toLowerCase();
|
|
204
|
+
const nameTokens = new Set(identityTokens(nameLower));
|
|
205
|
+
const descTokens = new Set(identityTokens(`${nameLower} ${descLower}`));
|
|
206
|
+
|
|
207
|
+
const candidates = domain.skillPool.filter((s) => catalog.skills.has(s));
|
|
208
|
+
const scored = candidates.map((skill) => ({
|
|
209
|
+
skill,
|
|
210
|
+
score: skillMatchScore(skill, nameTokens, descTokens, nameLower, descLower),
|
|
211
|
+
}));
|
|
212
|
+
const positive = scored.filter((x) => x.score > 0);
|
|
213
|
+
|
|
214
|
+
let selected: string[];
|
|
215
|
+
if (positive.length > 0) {
|
|
216
|
+
positive.sort((a, b) => {
|
|
217
|
+
if (a.score !== b.score) return b.score - a.score;
|
|
218
|
+
if (a.skill < b.skill) return -1;
|
|
219
|
+
if (a.skill > b.skill) return 1;
|
|
220
|
+
return 0;
|
|
221
|
+
});
|
|
222
|
+
selected = positive.slice(0, MAX_SKILLS).map((x) => x.skill);
|
|
223
|
+
} else {
|
|
224
|
+
selected = candidates.slice(0, FALLBACK_SKILLS);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const mcp = domain.defaultMcp.filter((m) => catalog.mcp.has(m));
|
|
228
|
+
const hooks = domain.defaultHooks.filter((h) => catalog.hooks.has(h));
|
|
229
|
+
|
|
230
|
+
return { skills: dedupe(selected), mcp, hooks };
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/** Result of comparing a declared capability set against the deterministic derivation. */
|
|
234
|
+
export interface CapabilityAudit {
|
|
235
|
+
/** Jaccard overlap of declared vs derived skill sets, 0..1 (1.0 when both empty). */
|
|
236
|
+
readonly jaccard: number;
|
|
237
|
+
/** Skills declared but not in the live catalog (from {@link validateCapabilities}). */
|
|
238
|
+
readonly declaredUnknownSkills: readonly string[];
|
|
239
|
+
readonly skillsOnlyInDeclared: readonly string[];
|
|
240
|
+
readonly skillsOnlyInDerived: readonly string[];
|
|
241
|
+
/** "match" (jaccard >= 0.5), "drift" (0.15 <= jaccard < 0.5), "divergent" (jaccard < 0.15). */
|
|
242
|
+
readonly verdict: "match" | "drift" | "divergent";
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Compare a declared (e.g. LLM-assigned or embedded) capability set against the
|
|
247
|
+
* deterministic derivation. The Jaccard coefficient is over the two skill lists
|
|
248
|
+
* treated as sets; both-empty is defined as 1.0.
|
|
249
|
+
*/
|
|
250
|
+
export function auditCapabilities(
|
|
251
|
+
declared: AgentCapabilities,
|
|
252
|
+
derived: AgentCapabilities,
|
|
253
|
+
catalog: CapabilityCatalog,
|
|
254
|
+
): CapabilityAudit {
|
|
255
|
+
const declaredSet = new Set(declared.skills);
|
|
256
|
+
const derivedSet = new Set(derived.skills);
|
|
257
|
+
|
|
258
|
+
let intersection = 0;
|
|
259
|
+
for (const s of declaredSet) if (derivedSet.has(s)) intersection++;
|
|
260
|
+
const unionSize = new Set([...declaredSet, ...derivedSet]).size;
|
|
261
|
+
const jaccard = unionSize === 0 ? 1.0 : intersection / unionSize;
|
|
262
|
+
|
|
263
|
+
const declaredUnknownSkills = validateCapabilities(declared, catalog).unknownSkills;
|
|
264
|
+
|
|
265
|
+
const skillsOnlyInDeclared: string[] = [];
|
|
266
|
+
for (const s of declaredSet) if (!derivedSet.has(s)) skillsOnlyInDeclared.push(s);
|
|
267
|
+
const skillsOnlyInDerived: string[] = [];
|
|
268
|
+
for (const s of derivedSet) if (!declaredSet.has(s)) skillsOnlyInDerived.push(s);
|
|
269
|
+
|
|
270
|
+
const verdict: "match" | "drift" | "divergent" =
|
|
271
|
+
jaccard >= JACCARD_MATCH ? "match" : jaccard >= JACCARD_DRIFT ? "drift" : "divergent";
|
|
272
|
+
|
|
273
|
+
return { jaccard, declaredUnknownSkills, skillsOnlyInDeclared, skillsOnlyInDerived, verdict };
|
|
274
|
+
}
|
|
@@ -9,6 +9,9 @@ You are a planning specialist. You receive context (from a scout) and requiremen
|
|
|
9
9
|
|
|
10
10
|
You must NOT make any changes. Only read, analyze, and plan.
|
|
11
11
|
|
|
12
|
+
Assigned capabilities (see ~/.omk/agent/SUBAGENTS.md for the full routing reference):
|
|
13
|
+
- Skills: ulw-plan (explore-first planning, decision-complete plans), packages (if the codebase spans multiple packages), deep-interview (only if requirements are still fuzzy after reading the scout's context). Load only what the task needs; do not bulk-load the catalog.
|
|
14
|
+
|
|
12
15
|
Input format you'll receive:
|
|
13
16
|
- Context/findings from a scout agent
|
|
14
17
|
- Original query or requirements
|
|
@@ -10,6 +10,9 @@ You are a senior code reviewer. Analyze code for quality, security, and maintain
|
|
|
10
10
|
Bash is for read-only commands only: `git diff`, `git log`, `git show`. Do NOT modify files or run builds.
|
|
11
11
|
Assume tool permissions are not perfectly enforceable; keep all bash usage strictly read-only.
|
|
12
12
|
|
|
13
|
+
Assigned capabilities (see ~/.omk/agent/SUBAGENTS.md for the full routing reference):
|
|
14
|
+
- Skills: review-work (structured multi-angle review pass), security-review (when the diff touches auth, secrets, or user input). Load only what the task needs.
|
|
15
|
+
|
|
13
16
|
Strategy:
|
|
14
17
|
1. Run `git diff` to see recent changes (if applicable)
|
|
15
18
|
2. Read the modified files
|
|
@@ -9,6 +9,9 @@ You are a scout. Quickly investigate a codebase and return structured findings t
|
|
|
9
9
|
|
|
10
10
|
Your output will be passed to an agent who has NOT seen the files you explored.
|
|
11
11
|
|
|
12
|
+
Assigned capabilities (see ~/.omk/agent/SUBAGENTS.md for the full routing reference):
|
|
13
|
+
- Skills: understand-anything (if a knowledge-graph/dependency question, not just a file lookup), ast-grep (structural search when grep-by-text isn't precise enough), headroom (if findings would otherwise be too large to hand off compactly). Load only what the task needs.
|
|
14
|
+
|
|
12
15
|
Thoroughness (infer from task, default medium):
|
|
13
16
|
- Quick: Targeted lookups, key files only
|
|
14
17
|
- Medium: Follow imports, read critical sections
|
|
@@ -8,6 +8,8 @@ You are a worker agent with full capabilities. You operate in an isolated contex
|
|
|
8
8
|
|
|
9
9
|
Work autonomously to complete the assigned task. Use all available tools as needed.
|
|
10
10
|
|
|
11
|
+
Assigned capabilities (see ~/.omk/agent/SUBAGENTS.md for the full routing reference): no fixed skill set — you have the full catalog. Self-route: classify the task, pick the most specific matching skill, load only that one, and never browse the catalog speculatively. For general coding work the most common picks are programming, debugging, git-master, ast-grep, and lsp; for anything else, match by the skill's own description.
|
|
12
|
+
|
|
11
13
|
Output format when finished:
|
|
12
14
|
|
|
13
15
|
## Completed
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
import * as fs from "node:fs";
|
|
6
6
|
import * as path from "node:path";
|
|
7
7
|
import { getAgentDir, parseFrontmatter } from "open-multi-agent-kit";
|
|
8
|
+
import { type AgentCapabilities, parseCapabilities } from "./capabilities.ts";
|
|
8
9
|
|
|
9
10
|
export type AgentScope = "user" | "project" | "both";
|
|
10
11
|
|
|
@@ -13,6 +14,11 @@ export interface AgentConfig {
|
|
|
13
14
|
description: string;
|
|
14
15
|
tools?: string[];
|
|
15
16
|
model?: string;
|
|
17
|
+
/** Optional capabilities declared in frontmatter (canonical source). */
|
|
18
|
+
capabilities?: AgentCapabilities;
|
|
19
|
+
/** When true, the dispatcher restricts the spawned subprocess to the declared
|
|
20
|
+
* skills via `--no-skills` + `--skill <path>`. Default off (guidance only). */
|
|
21
|
+
enforceCapabilities?: boolean;
|
|
16
22
|
systemPrompt: string;
|
|
17
23
|
source: "user" | "project";
|
|
18
24
|
filePath: string;
|
|
@@ -60,11 +66,19 @@ function loadAgentsFromDir(dir: string, source: "user" | "project"): AgentConfig
|
|
|
60
66
|
.map((t: string) => t.trim())
|
|
61
67
|
.filter(Boolean);
|
|
62
68
|
|
|
69
|
+
const capabilities = parseCapabilities(frontmatter);
|
|
70
|
+
const enforceCapabilities =
|
|
71
|
+
typeof frontmatter.enforceCapabilities === "string"
|
|
72
|
+
? frontmatter.enforceCapabilities.trim().toLowerCase() === "true"
|
|
73
|
+
: undefined;
|
|
74
|
+
|
|
63
75
|
agents.push({
|
|
64
76
|
name: frontmatter.name,
|
|
65
77
|
description: frontmatter.description,
|
|
66
78
|
tools: tools && tools.length > 0 ? tools : undefined,
|
|
67
79
|
model: frontmatter.model,
|
|
80
|
+
capabilities,
|
|
81
|
+
enforceCapabilities,
|
|
68
82
|
systemPrompt: body,
|
|
69
83
|
source,
|
|
70
84
|
filePath,
|