@ludi-uni/ludi-agent-kit 0.1.0 → 0.1.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/README.md +3 -2
- package/adapters/pi/README.md +5 -3
- package/adapters/pi/lib/model-registry.mjs +12 -0
- package/docs/model-maintenance.md +2 -2
- package/docs/orchestrator.md +2 -0
- package/lib/orchestrator/api.mjs +6 -6
- package/lib/orchestrator/orchestrator.mjs +14 -4
- package/lib/registry.mjs +11 -6
- package/package.json +1 -1
- package/scripts/e2e-investigate-repro.mjs +2 -2
- package/scripts/model-maintenance-job.mjs +2 -1
- package/scripts/observe-models.mjs +2 -1
- package/scripts/orchestrate.mjs +4 -2
- package/scripts/reevaluate-models.mjs +2 -1
- package/scripts/resolve-capabilities.mjs +2 -1
- package/scripts/run-pipeline.mjs +2 -2
- package/tests/e2e-real-orchestrator-resume.mjs +2 -2
- package/tests/e2e-real-orchestrator.mjs +2 -2
- package/tests/e2e-real-pi.mjs +3 -3
- package/tests/e2e-real-tool-orchestrator.mjs +2 -2
- package/tests/orchestrator-persist.test.mjs +38 -1
- package/tests/pipeline.test.mjs +27 -1
package/README.md
CHANGED
|
@@ -77,7 +77,8 @@ Requirements for local validation: Windows, PowerShell 7, Git and Node.js with `
|
|
|
77
77
|
## Executable path (Phase 2)
|
|
78
78
|
|
|
79
79
|
```powershell
|
|
80
|
-
|
|
80
|
+
New-Item -ItemType Directory -Force "$HOME/.pi/agent/ludi-agent-kit" | Out-Null
|
|
81
|
+
Copy-Item adapters/pi/models.local.example.json "$HOME/.pi/agent/ludi-agent-kit/models.local.json" # edit with `pi --list-models` ids; use $env:PI_CODING_AGENT_DIR/ludi-agent-kit if set
|
|
81
82
|
node scripts/resolve-capabilities.mjs # chains + out/settings.proposal.json
|
|
82
83
|
node scripts/run-pipeline.mjs --repo <fixture-copy> --task "Fix the failing test" --dry-run
|
|
83
84
|
node scripts/run-pipeline.mjs --repo <fixture-copy> --task "Fix the failing test" # real: scout -> pack -> coder -> tests
|
|
@@ -90,7 +91,7 @@ never the same modelId twice, failures appended to `previous_attempts`. See `doc
|
|
|
90
91
|
|
|
91
92
|
## Orchestration and safety
|
|
92
93
|
|
|
93
|
-
`node scripts/orchestrate.mjs --dry-run "<request>"` previews a rules-based plan without launching agents. A live invocation can call external models and consume quota; review the plan, model availability and `docs/orchestrator.md` before using it. `sync-pi.ps1` is dry-run by default; `-Apply` writes to the chosen pi agent directory. The optional real E2E scripts are not part of routine validation.
|
|
94
|
+
`node scripts/orchestrate.mjs --dry-run "<request>"` previews a rules-based plan without launching agents. A live invocation can call external models and consume quota; review the plan, model availability and `docs/orchestrator.md` before using it. Before creating a run, the orchestrator checks that each planned capability has at least one concrete model binding (and checks `orchestration` before model planning); a missing binding reports the user-level file to edit. This is a binding check, not an authentication/quota probe. `sync-pi.ps1` is dry-run by default; `-Apply` writes to the chosen pi agent directory. The optional real E2E scripts are not part of routine validation.
|
|
94
95
|
|
|
95
96
|
## Distribution checklist
|
|
96
97
|
|
package/adapters/pi/README.md
CHANGED
|
@@ -6,7 +6,8 @@ Binds the neutral kit to a native-Windows pi installation (`~/.pi/agent`, or
|
|
|
6
6
|
| File | Role |
|
|
7
7
|
| --- | --- |
|
|
8
8
|
| `models.json` | shared TEMPLATE: logical backend -> pi `provider`/`model`/`thinking`. Ships with `TODO-*` placeholders that the resolver skips. |
|
|
9
|
-
|
|
|
9
|
+
| `~/.pi/agent/ludi-agent-kit/models.local.json` | **Recommended durable user-level bindings**, outside the npm install (use `$env:PI_CODING_AGENT_DIR/ludi-agent-kit/models.local.json` when set). Copy `models.local.example.json` and fill from `pi --list-models`. Overrides package-local bindings per field and may add backends. Allowed keys: `provider, model, thinking, vision, note`; credentials are rejected. The kit reads this file but never writes it. |
|
|
10
|
+
| `models.local.json` | **gitignored package-local migration fallback**; still overrides `models.json` when present, but is overridden by the user-level file. Existing machine-local files continue to work without migration; a package upgrade may remove this file. |
|
|
10
11
|
| `lib/invoke.mjs` | one-shot model call through the installed pi CLI (`node <pi cli.js> -p --model provider/id:thinking --no-tools --no-session --no-approve --system-prompt ...`). Uses pi's own auth store. Verified against `qoder/Qwen3.8-Flash` and `devin/swe-2-high` — no provider-specific runner is needed; any provider registered in pi's model store works. |
|
|
11
12
|
| `lib/subagent.mjs` | tool-capable child (`pi --mode json -p`). **Do not pass `--no-extensions`**: providers registered by pi extensions (qoder, devin) are absent from the static model store and only resolve once their extension has loaded; with `--no-extensions` `--model` fails `Model ... not found`. Kit extensions the child needs are passed explicitly (`-e shell-gate`). |
|
|
12
13
|
| `lib/settings-proposal.mjs` | renders resolved agents into `subagents.agentOverrides.<agent>.{model,thinking}` — the shape verified in pi-subagents 0.68.0 (`docs/models.md`, `src/agents/agents.ts parseBuiltinOverrideEntry`). pi-subagents removed `fallbackModels`, so fallback chains are *not* expressible there; the kit runner owns escalation. |
|
|
@@ -25,14 +26,15 @@ Binds the neutral kit to a native-Windows pi installation (`~/.pi/agent`, or
|
|
|
25
26
|
```
|
|
26
27
|
agents/<name>.md capability
|
|
27
28
|
-> routing/routing.json primary + fallback backends
|
|
28
|
-
-> models.json
|
|
29
|
+
-> models.json < package-local models.local.json < user-level models.local.json
|
|
30
|
+
provider/model/thinking per backend (later source overrides fields)
|
|
29
31
|
-> "provider/model:thinking" (pi --model syntax / pi-subagents model syntax)
|
|
30
32
|
```
|
|
31
33
|
|
|
32
34
|
Logical backends include `qoder` (cheap-first, bound to `qoder/Qwen3.8-Flash` on this
|
|
33
35
|
machine) and `devin` (strong-first, bound to `devin/swe-2-high`). Both are ordinary
|
|
34
36
|
pi providers — the kit never forks a runner per provider; bindings live only in
|
|
35
|
-
`models.local.json` and facts in `model-catalog.json`. If Codex quota is exhausted,
|
|
37
|
+
the user-level `models.local.json` (or legacy package-local file) and facts in `model-catalog.json`. If Codex quota is exhausted,
|
|
36
38
|
cheap-code still resolves `qoder -> cheap -> local -> sol` and strong-code
|
|
37
39
|
`devin -> qoder -> sol -> codex -> local`, so the orchestrator keeps running on
|
|
38
40
|
Qoder/Devin/FreeToken without any code change.
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// Pi's durable model bindings live in the user agent directory, outside the npm package.
|
|
2
|
+
import { homedir } from 'node:os';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { loadRegistry } from '../../../lib/registry.mjs';
|
|
5
|
+
|
|
6
|
+
export function piUserModelsPath(env = process.env, home = homedir()) {
|
|
7
|
+
return join(env.PI_CODING_AGENT_DIR || join(home, '.pi', 'agent'), 'ludi-agent-kit', 'models.local.json');
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function loadPiRegistry(kit, routing, { env = process.env, home = homedir() } = {}) {
|
|
11
|
+
return loadRegistry(join(kit, 'adapters/pi/models.json'), join(kit, 'adapters/pi/models.local.json'), routing, piUserModelsPath(env, home));
|
|
12
|
+
}
|
|
@@ -12,7 +12,7 @@ automatically.
|
|
|
12
12
|
| --- | --- | --- |
|
|
13
13
|
| routing | `routing/routing.json` | capabilities, backend tiers, `requires.vision` |
|
|
14
14
|
| agents | `agents/*.md` | capability → agent names (for `affected.agents`) |
|
|
15
|
-
| model bindings | `adapters
|
|
15
|
+
| model bindings | `adapters/pi/models.json` < legacy `adapters/pi/models.local.json` < `~/.pi/agent/ludi-agent-kit/models.local.json` (`PI_CODING_AGENT_DIR` respected) | current provider/model per backend; user-level overrides survive npm upgrades |
|
|
16
16
|
| catalog | `adapters/<x>/model-catalog.json` | model facts: status, cost, contextK, vision, toolUse, quality scores, `location`/`local` power metadata |
|
|
17
17
|
| exec policy | `adapters/<x>/maintenance-policy.json` | Phase 2: requiredQuality per tier, capabilityRequirements (coding/reasoning floors), cost weights, electricity price, escalation thresholds |
|
|
18
18
|
| events | `--events <file>` | what changed (see event types below) |
|
|
@@ -86,7 +86,7 @@ binding embedded plus the manual steps to restore it.
|
|
|
86
86
|
## Applying and rolling back
|
|
87
87
|
|
|
88
88
|
The task writes only `adapters/<x>/out/model-maintenance.proposal.json`. To apply:
|
|
89
|
-
edit `
|
|
89
|
+
edit the user-level `ludi-agent-kit/models.local.json` per `proposedModel`, re-run
|
|
90
90
|
`scripts/resolve-capabilities.mjs`, then merge `out/settings.proposal.json` into
|
|
91
91
|
`~/.pi/agent/settings.json` yourself. Rollback is the same flow using
|
|
92
92
|
`rollback.previousBinding`. It never writes `settings.json`, `models.json`,
|
package/docs/orchestrator.md
CHANGED
|
@@ -11,6 +11,8 @@ node scripts/orchestrate.mjs --repo <dir> --apply "..." # coder t
|
|
|
11
11
|
[--planner rules|model] [--policy <file>] [--out <dir>] [--json] [--trace]
|
|
12
12
|
```
|
|
13
13
|
|
|
14
|
+
Before starting a persistent run, the planner's capability (for `--planner model`) and every capability in the validated task plan must have at least one non-placeholder binding. Otherwise the call fails without creating a run and reports the user-level `models.local.json` path. Existing runs can still be listed/shown; resuming pending tasks checks their bindings. The check does not guarantee provider authentication, quota, or model availability. Configure `~/.pi/agent/ludi-agent-kit/models.local.json` (or `$env:PI_CODING_AGENT_DIR/ludi-agent-kit/models.local.json`) and see `adapters/pi/README.md` for migration precedence.
|
|
15
|
+
|
|
14
16
|
## Response language
|
|
15
17
|
|
|
16
18
|
All agents default to **Japanese** for user-facing output (summaries, reports, decisions,
|
package/lib/orchestrator/api.mjs
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// the caller supplies `invoke` / `runner`. Natural-language UI can call these functions later.
|
|
3
3
|
import { join } from 'node:path';
|
|
4
4
|
import { loadRouting } from '../routing.mjs';
|
|
5
|
-
import {
|
|
5
|
+
import { loadPiRegistry, piUserModelsPath } from '../../adapters/pi/lib/model-registry.mjs';
|
|
6
6
|
import { loadAgents } from '../agents.mjs';
|
|
7
7
|
import { loadPolicy } from './policy.mjs';
|
|
8
8
|
import { openStore } from './store.mjs';
|
|
@@ -15,15 +15,15 @@ export function defaultStorePath(kit, env = process.env) {
|
|
|
15
15
|
return env.LUDI_ORCHESTRATION_STORE || join(kit, '.orchestration', 'state.db');
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
-
export function loadOrchestrationContext({ kit, storePath, policyPath = null, localPolicyPath = null, clientContext = null }) {
|
|
18
|
+
export function loadOrchestrationContext({ kit, storePath, policyPath = null, localPolicyPath = null, clientContext = null, modelOptions = {} }) {
|
|
19
19
|
const routing = loadRouting(join(kit, 'routing/routing.json'));
|
|
20
|
-
const { registry } =
|
|
20
|
+
const { registry } = loadPiRegistry(kit, routing, modelOptions);
|
|
21
21
|
const { agents, errors } = loadAgents(join(kit, 'agents'), routing);
|
|
22
22
|
const base = join(kit, 'orchestration/decision-policy.json');
|
|
23
23
|
const { policy } = loadPolicy(base, policyPath ?? localPolicyPath);
|
|
24
24
|
const session = openStore(storePath);
|
|
25
25
|
const activity = createActivityTracker({ kit, clientContext });
|
|
26
|
-
return { kit, routing, registry, agents, errors, policy, session, storePath, clientContext, activity };
|
|
26
|
+
return { kit, routing, registry, agents, errors, policy, session, storePath, clientContext, activity, bindingPath: piUserModelsPath(modelOptions.env, modelOptions.home) };
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
/** Public live activity for one run (in-memory projection; null when unknown). */
|
|
@@ -137,14 +137,14 @@ export function createRunRunner(ctx, { invoke, runSubagent = null, repoRoot, out
|
|
|
137
137
|
export async function startOrchestration(ctx, { request, repoRoot = null, planner = 'rules', plan = null, runner, invoke = null, health = null, projectStore }) {
|
|
138
138
|
return orchestrate({
|
|
139
139
|
request, planner, plan, agents: ctx.agents, routing: ctx.routing, registry: ctx.registry, policy: ctx.policy,
|
|
140
|
-
runner, invoke, repoRoot, session: ctx.session, health, projectStore, activity: ctx.activity,
|
|
140
|
+
runner, invoke, repoRoot, session: ctx.session, health, projectStore, activity: ctx.activity, bindingPath: ctx.bindingPath,
|
|
141
141
|
});
|
|
142
142
|
}
|
|
143
143
|
|
|
144
144
|
export async function resumeOrchestration(ctx, { runId, answers = [], repoRoot = null, runner, invoke = null, health = null, projectStore }) {
|
|
145
145
|
return orchestrate({
|
|
146
146
|
request: '', resumeRunId: runId, answers, agents: ctx.agents, routing: ctx.routing, registry: ctx.registry,
|
|
147
|
-
policy: ctx.policy, runner, invoke, repoRoot, session: ctx.session, health, projectStore, activity: ctx.activity,
|
|
147
|
+
policy: ctx.policy, runner, invoke, repoRoot, session: ctx.session, health, projectStore, activity: ctx.activity, bindingPath: ctx.bindingPath,
|
|
148
148
|
});
|
|
149
149
|
}
|
|
150
150
|
|
|
@@ -8,6 +8,7 @@ import { evaluateResult } from './evaluator.mjs';
|
|
|
8
8
|
import { evaluateDecision, decisionKey } from './escalation.mjs';
|
|
9
9
|
import { classifyRun, REASSIGN_CLASSES, isProtocolFailure, shouldMarkTaskGlobalFailure } from './failures.mjs';
|
|
10
10
|
import { declaredMode, accessOf, piToolsForAccess, workspaceOf } from './permissions.mjs';
|
|
11
|
+
import { resolveCapability } from '../resolve.mjs';
|
|
11
12
|
|
|
12
13
|
const STOP = new Set(['failed', 'blocked', 'waiting_for_user']);
|
|
13
14
|
|
|
@@ -87,7 +88,7 @@ export async function orchestrate(options) {
|
|
|
87
88
|
}
|
|
88
89
|
}
|
|
89
90
|
|
|
90
|
-
async function orchestrateImpl({ request: requestArg, planner = 'rules', plan = null, agents, routing, registry, policy, runner, invoke = null, repoRoot = null, projectStore = createNullProjectStore(), now = () => new Date().toISOString(), session = null, resumeRunId = null, answers = [], health = null, activity = null, onRunCreated = () => {} }) {
|
|
91
|
+
async function orchestrateImpl({ request: requestArg, planner = 'rules', plan = null, agents, routing, registry, policy, runner, invoke = null, repoRoot = null, projectStore = createNullProjectStore(), now = () => new Date().toISOString(), session = null, resumeRunId = null, answers = [], health = null, activity = null, bindingPath = null, onRunCreated = () => {} }) {
|
|
91
92
|
let request = requestArg;
|
|
92
93
|
let activePolicy = policy;
|
|
93
94
|
let dp = activePolicy.decision_policy, limits = activePolicy.limits;
|
|
@@ -95,6 +96,13 @@ async function orchestrateImpl({ request: requestArg, planner = 'rules', plan =
|
|
|
95
96
|
const trace = [], autoDecisions = [], escalations = [], unresolved = [], decisionLog = [];
|
|
96
97
|
const limitsHit = new Set();
|
|
97
98
|
let store = null, planning = true, recovered = [];
|
|
99
|
+
const requireModels = capabilities => {
|
|
100
|
+
for (const capability of new Set(capabilities)) {
|
|
101
|
+
if (!resolveCapability(routing, registry, capability).candidates.length) {
|
|
102
|
+
throw new Error(`No bound model for required capability "${capability}". Set a provider/model binding in ${bindingPath ?? 'your adapter models.local.json'} (see adapters/pi/models.local.example.json).`);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
};
|
|
98
106
|
const scopeKeyOf = () => repoRoot ?? (runId ? session?.getRun(runId)?.scopeKey : null) ?? 'default';
|
|
99
107
|
// Live-activity bridge: the tracker decides whether an event type is significant
|
|
100
108
|
// (persist to the run trace) or high-frequency (live snapshot only, e.g. turns).
|
|
@@ -141,6 +149,7 @@ async function orchestrateImpl({ request: requestArg, planner = 'rules', plan =
|
|
|
141
149
|
if (res.idempotent && !res.same) unresolved.push(`decision ${a.decisionId} already answered; kept the first answer`);
|
|
142
150
|
}
|
|
143
151
|
store = session.openTaskStore(runId);
|
|
152
|
+
requireModels(store.list().filter(t => !['completed', 'failed', 'cancelled'].includes(t.status)).map(t => t.capability));
|
|
144
153
|
releaseRecoverable(store);
|
|
145
154
|
planning = store.size() === 0;
|
|
146
155
|
for (const d of session.listDecisions(runId, 'pending')) escalations.push(toEscalation(d));
|
|
@@ -149,17 +158,18 @@ async function orchestrateImpl({ request: requestArg, planner = 'rules', plan =
|
|
|
149
158
|
|
|
150
159
|
try {
|
|
151
160
|
if (planning) {
|
|
161
|
+
if (!plan && planner === 'model') requireModels(['orchestration']);
|
|
162
|
+
p = await makePlan(request, { planner, plan, agents, routing, registry, policy: activePolicy, invoke, cwd: repoRoot ?? process.cwd(), trace, health });
|
|
163
|
+
const v = validatePlan(p.tasks, { agents, routing, policy: activePolicy });
|
|
164
|
+
if (!v.errors.length) requireModels(v.tasks.map(t => t.capability));
|
|
152
165
|
if (session && !runId) {
|
|
153
166
|
runId = session.createRun({ request, policy, repoRoot, planner, scopeKey: repoRoot ?? 'default' });
|
|
154
167
|
onRunCreated(runId);
|
|
155
|
-
// Bind the owning client immediately so its public snapshot exists from the start.
|
|
156
168
|
activity?.bindRun?.({ runId, repoRoot });
|
|
157
169
|
}
|
|
158
170
|
health?.bindRun?.(runId);
|
|
159
171
|
store = store ?? (session ? session.openTaskStore(runId) : createTaskStore());
|
|
160
|
-
p = await makePlan(request, { planner, plan, agents, routing, registry, policy: activePolicy, invoke, cwd: repoRoot ?? process.cwd(), trace, health });
|
|
161
172
|
if (p.fallbackReason) decide({ subject: 'planner', choice: 'rules planner', reason: `model plan unusable (${p.fallbackReason}); reversible`, step: 'reversible' });
|
|
162
|
-
const v = validatePlan(p.tasks, { agents, routing, policy: activePolicy });
|
|
163
173
|
event('plan', { planner: p.planner, tasks: v.tasks.map(t => ({ id: t.id, agent: t.assignedAgent, capability: t.capability, dependencies: t.dependencies })), errors: v.errors });
|
|
164
174
|
if (v.errors.length) return finish('plan-invalid', v.errors);
|
|
165
175
|
for (const t of v.tasks) {
|
package/lib/registry.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// Adapter model registry:
|
|
1
|
+
// Adapter model registry: shared template, package-local migration file, then durable user bindings.
|
|
2
2
|
// Validates against routing backends. Contains no provider/model names.
|
|
3
3
|
import { readFileSync, existsSync } from 'node:fs';
|
|
4
4
|
|
|
@@ -46,10 +46,10 @@ export function isPlaceholder(binding) {
|
|
|
46
46
|
}
|
|
47
47
|
|
|
48
48
|
/**
|
|
49
|
-
* Load
|
|
50
|
-
* Throws on validation errors
|
|
49
|
+
* Load the template, then optional package-local and user-level bindings (highest priority).
|
|
50
|
+
* Throws on validation errors in any present file.
|
|
51
51
|
*/
|
|
52
|
-
export function loadRegistry(modelsPath, localPath, routing) {
|
|
52
|
+
export function loadRegistry(modelsPath, localPath, routing, userPath = null) {
|
|
53
53
|
const base = JSON.parse(readFileSync(modelsPath, 'utf8'));
|
|
54
54
|
const errors = validateRegistry(base, routing, 'models.json');
|
|
55
55
|
let local = null;
|
|
@@ -57,7 +57,12 @@ export function loadRegistry(modelsPath, localPath, routing) {
|
|
|
57
57
|
local = JSON.parse(readFileSync(localPath, 'utf8'));
|
|
58
58
|
errors.push(...validateRegistry(local, routing, 'models.local.json'));
|
|
59
59
|
}
|
|
60
|
+
let user = null;
|
|
61
|
+
if (userPath && existsSync(userPath)) {
|
|
62
|
+
user = JSON.parse(readFileSync(userPath, 'utf8'));
|
|
63
|
+
errors.push(...validateRegistry(user, routing, userPath));
|
|
64
|
+
}
|
|
60
65
|
if (errors.length) throw new Error(errors.join('\n'));
|
|
61
|
-
const merged = mergeRegistries(base, local);
|
|
62
|
-
return { registry: merged, sources: { models: modelsPath, local: local ? localPath : null } };
|
|
66
|
+
const merged = mergeRegistries(mergeRegistries(base, local), user);
|
|
67
|
+
return { registry: merged, sources: { models: modelsPath, local: local ? localPath : null, user: user ? userPath : null } };
|
|
63
68
|
}
|
package/package.json
CHANGED
|
@@ -10,7 +10,7 @@ import { join, resolve, dirname } from 'node:path';
|
|
|
10
10
|
import { fileURLToPath } from 'node:url';
|
|
11
11
|
import assert from 'node:assert/strict';
|
|
12
12
|
import { loadRouting } from '../lib/routing.mjs';
|
|
13
|
-
import {
|
|
13
|
+
import { loadPiRegistry } from '../adapters/pi/lib/model-registry.mjs';
|
|
14
14
|
import { loadAgents } from '../lib/agents.mjs';
|
|
15
15
|
import { loadPolicy, mergePolicy } from '../lib/orchestrator/policy.mjs';
|
|
16
16
|
import { openStore } from '../lib/orchestrator/store.mjs';
|
|
@@ -35,7 +35,7 @@ const plan = [
|
|
|
35
35
|
];
|
|
36
36
|
|
|
37
37
|
const routing = loadRouting(join(kit, 'routing/routing.json'));
|
|
38
|
-
const { registry } =
|
|
38
|
+
const { registry } = loadPiRegistry(kit, routing);
|
|
39
39
|
const { agents } = loadAgents(join(kit, 'agents'), routing);
|
|
40
40
|
const { policy } = loadPolicy(join(kit, 'orchestration/decision-policy.json'));
|
|
41
41
|
const active = mergePolicy(policy, { limits: { max_retries: 1 }, agent_runtime: { max_runtime_ms: 300000, max_tool_calls: 25, max_turns: 8 } });
|
|
@@ -18,6 +18,7 @@ import { resolve, join, dirname } from 'node:path';
|
|
|
18
18
|
import { fileURLToPath } from 'node:url';
|
|
19
19
|
import { loadRouting } from '../lib/routing.mjs';
|
|
20
20
|
import { loadRegistry } from '../lib/registry.mjs';
|
|
21
|
+
import { piUserModelsPath } from '../adapters/pi/lib/model-registry.mjs';
|
|
21
22
|
import { loadAgents } from '../lib/agents.mjs';
|
|
22
23
|
import { loadCatalog } from '../lib/maintenance.mjs';
|
|
23
24
|
import { loadExecPolicy } from '../lib/maintenance-exec.mjs';
|
|
@@ -32,7 +33,7 @@ const adapterDir = join(kit, 'adapters', adapter);
|
|
|
32
33
|
const outDir = join(adapterDir, 'out');
|
|
33
34
|
|
|
34
35
|
const routing = loadRouting(join(kit, 'routing/routing.json'));
|
|
35
|
-
const { registry } = loadRegistry(join(adapterDir, 'models.json'), join(adapterDir, 'models.local.json'), routing);
|
|
36
|
+
const { registry } = loadRegistry(join(adapterDir, 'models.json'), join(adapterDir, 'models.local.json'), routing, adapter === 'pi' ? piUserModelsPath() : null);
|
|
36
37
|
const { agents, errors } = loadAgents(join(kit, 'agents'), routing);
|
|
37
38
|
if (errors.length) { console.error(errors.join('\n')); process.exit(1); }
|
|
38
39
|
const catalog = loadCatalog(join(adapterDir, 'model-catalog.json'));
|
|
@@ -17,6 +17,7 @@ import { resolve, join, dirname } from 'node:path';
|
|
|
17
17
|
import { fileURLToPath } from 'node:url';
|
|
18
18
|
import { loadRouting } from '../lib/routing.mjs';
|
|
19
19
|
import { loadRegistry } from '../lib/registry.mjs';
|
|
20
|
+
import { piUserModelsPath } from '../adapters/pi/lib/model-registry.mjs';
|
|
20
21
|
import { loadAgents } from '../lib/agents.mjs';
|
|
21
22
|
import { loadCatalog } from '../lib/maintenance.mjs';
|
|
22
23
|
import { ingestObservations, loadObservationStore, productionObservations } from '../lib/observe/observation.mjs';
|
|
@@ -80,7 +81,7 @@ let previewFile = null;
|
|
|
80
81
|
if (has('preview-maintenance')) {
|
|
81
82
|
const hypothetical = applyProposalToCatalog(catalog, proposal);
|
|
82
83
|
const routing = loadRouting(join(kit, 'routing/routing.json'));
|
|
83
|
-
const { registry } = loadRegistry(join(adapterDir, 'models.json'), join(adapterDir, 'models.local.json'), routing);
|
|
84
|
+
const { registry } = loadRegistry(join(adapterDir, 'models.json'), join(adapterDir, 'models.local.json'), routing, adapter === 'pi' ? piUserModelsPath() : null);
|
|
84
85
|
const { agents, errors } = loadAgents(join(kit, 'agents'), routing);
|
|
85
86
|
if (errors.length) { console.error(errors.join('\n')); process.exit(1); }
|
|
86
87
|
const policy = loadExecPolicy(join(adapterDir, 'maintenance-policy.json'));
|
package/scripts/orchestrate.mjs
CHANGED
|
@@ -18,7 +18,8 @@ import { resolve, join, dirname } from 'node:path';
|
|
|
18
18
|
import { fileURLToPath } from 'node:url';
|
|
19
19
|
import { mkdirSync, writeFileSync } from 'node:fs';
|
|
20
20
|
import { loadRouting } from '../lib/routing.mjs';
|
|
21
|
-
import {
|
|
21
|
+
import { loadPiRegistry, piUserModelsPath } from '../adapters/pi/lib/model-registry.mjs';
|
|
22
|
+
import { resolveCapability } from '../lib/resolve.mjs';
|
|
22
23
|
import { loadAgents } from '../lib/agents.mjs';
|
|
23
24
|
import { loadPolicy } from '../lib/orchestrator/policy.mjs';
|
|
24
25
|
import { dryRun, formatPlan } from '../lib/orchestrator/orchestrator.mjs';
|
|
@@ -63,7 +64,7 @@ const planner = opts.planner ?? 'rules';
|
|
|
63
64
|
if (!['rules', 'model'].includes(planner)) { console.error(`unknown planner "${planner}"`); process.exit(2); }
|
|
64
65
|
|
|
65
66
|
const routing = loadRouting(join(kit, 'routing/routing.json'));
|
|
66
|
-
const { registry } =
|
|
67
|
+
const { registry } = loadPiRegistry(kit, routing);
|
|
67
68
|
const { agents, errors } = loadAgents(join(kit, 'agents'), routing);
|
|
68
69
|
if (errors.length) { console.error(errors.join('\n')); process.exit(1); }
|
|
69
70
|
const { policy } = opts.policy
|
|
@@ -74,6 +75,7 @@ const outDir = resolve(opts.out ?? join(kit, 'adapters/pi/out/orchestrate'));
|
|
|
74
75
|
|
|
75
76
|
if (opts['dry-run'] && !cleanupMode) {
|
|
76
77
|
const invoke = planner === 'model' ? createPiInvoker() : null;
|
|
78
|
+
if (planner === 'model' && !resolveCapability(routing, registry, 'orchestration').candidates.length) throw new Error(`No bound model for orchestration; edit ${piUserModelsPath()}`);
|
|
77
79
|
const dry = await dryRun(request, { planner, agents, routing, registry, policy, invoke, cwd: repoRoot ?? process.cwd() });
|
|
78
80
|
console.log(opts.json ? JSON.stringify(dry, null, 2) : formatPlan(dry));
|
|
79
81
|
process.exitCode = dry.errors.length ? 1 : 0;
|
|
@@ -25,6 +25,7 @@ import { resolve, join, dirname } from 'node:path';
|
|
|
25
25
|
import { fileURLToPath } from 'node:url';
|
|
26
26
|
import { loadRouting } from '../lib/routing.mjs';
|
|
27
27
|
import { loadRegistry } from '../lib/registry.mjs';
|
|
28
|
+
import { piUserModelsPath } from '../adapters/pi/lib/model-registry.mjs';
|
|
28
29
|
import { loadAgents } from '../lib/agents.mjs';
|
|
29
30
|
import { loadCatalog, loadEvents, loadAvailabilityFile } from '../lib/maintenance.mjs';
|
|
30
31
|
import { runMaintenancePlan, loadExecPolicy } from '../lib/maintenance-exec.mjs';
|
|
@@ -37,7 +38,7 @@ const adapter = opt('adapter', 'pi');
|
|
|
37
38
|
const adapterDir = join(kit, 'adapters', adapter);
|
|
38
39
|
|
|
39
40
|
const routing = loadRouting(join(kit, 'routing/routing.json'));
|
|
40
|
-
const { registry, sources } = loadRegistry(join(adapterDir, 'models.json'), join(adapterDir, 'models.local.json'), routing);
|
|
41
|
+
const { registry, sources } = loadRegistry(join(adapterDir, 'models.json'), join(adapterDir, 'models.local.json'), routing, adapter === 'pi' ? piUserModelsPath() : null);
|
|
41
42
|
const { agents, errors } = loadAgents(join(kit, 'agents'), routing);
|
|
42
43
|
if (errors.length) { console.error(errors.join('\n')); process.exit(1); }
|
|
43
44
|
const catalog = loadCatalog(join(adapterDir, 'model-catalog.json'));
|
|
@@ -8,6 +8,7 @@ import { fileURLToPath } from 'node:url';
|
|
|
8
8
|
import { homedir } from 'node:os';
|
|
9
9
|
import { loadRouting } from '../lib/routing.mjs';
|
|
10
10
|
import { loadRegistry } from '../lib/registry.mjs';
|
|
11
|
+
import { piUserModelsPath } from '../adapters/pi/lib/model-registry.mjs';
|
|
11
12
|
import { loadAgents } from '../lib/agents.mjs';
|
|
12
13
|
import { resolveAgents, resolveCapability } from '../lib/resolve.mjs';
|
|
13
14
|
|
|
@@ -17,7 +18,7 @@ const opt = (name, def) => { const i = args.indexOf(`--${name}`); return i >= 0
|
|
|
17
18
|
const adapter = opt('adapter', 'pi');
|
|
18
19
|
const adapterDir = join(kit, 'adapters', adapter);
|
|
19
20
|
const routing = loadRouting(join(kit, 'routing/routing.json'));
|
|
20
|
-
const { registry, sources } = loadRegistry(join(adapterDir, 'models.json'), join(adapterDir, 'models.local.json'), routing);
|
|
21
|
+
const { registry, sources } = loadRegistry(join(adapterDir, 'models.json'), join(adapterDir, 'models.local.json'), routing, adapter === 'pi' ? piUserModelsPath() : null);
|
|
21
22
|
const { agents, errors } = loadAgents(join(kit, 'agents'), routing);
|
|
22
23
|
if (errors.length) { console.error(errors.join('\n')); process.exit(1); }
|
|
23
24
|
|
package/scripts/run-pipeline.mjs
CHANGED
|
@@ -8,7 +8,7 @@ import { resolve, join, dirname } from 'node:path';
|
|
|
8
8
|
import { fileURLToPath } from 'node:url';
|
|
9
9
|
import { writeFileSync, mkdirSync, readFileSync } from 'node:fs';
|
|
10
10
|
import { loadRouting } from '../lib/routing.mjs';
|
|
11
|
-
import {
|
|
11
|
+
import { loadPiRegistry } from '../adapters/pi/lib/model-registry.mjs';
|
|
12
12
|
import { loadAgents } from '../lib/agents.mjs';
|
|
13
13
|
import { resolveAgents, resolveCapability } from '../lib/resolve.mjs';
|
|
14
14
|
import { runPipeline, surveyRepo, scoutPrompt, detectTestCommand, runTests, coderPrompt } from '../lib/pipeline.mjs';
|
|
@@ -25,7 +25,7 @@ const outDir = resolve(opt('out', join(kit, 'adapters/pi/out/pipeline')));
|
|
|
25
25
|
const maxAttempts = Number(opt('max-attempts', 2));
|
|
26
26
|
|
|
27
27
|
const routing = loadRouting(join(kit, 'routing/routing.json'));
|
|
28
|
-
const { registry, sources } =
|
|
28
|
+
const { registry, sources } = loadPiRegistry(kit, routing);
|
|
29
29
|
const { agents, errors } = loadAgents(join(kit, 'agents'), routing);
|
|
30
30
|
if (errors.length) { console.error(errors.join('\n')); process.exit(1); }
|
|
31
31
|
const resolved = resolveAgents(agents, routing, registry);
|
|
@@ -9,7 +9,7 @@ import { join, resolve, dirname } from 'node:path';
|
|
|
9
9
|
import { fileURLToPath } from 'node:url';
|
|
10
10
|
import assert from 'node:assert/strict';
|
|
11
11
|
import { loadRouting } from '../lib/routing.mjs';
|
|
12
|
-
import {
|
|
12
|
+
import { loadPiRegistry } from '../adapters/pi/lib/model-registry.mjs';
|
|
13
13
|
import { loadAgents } from '../lib/agents.mjs';
|
|
14
14
|
import { loadPolicy, mergePolicy } from '../lib/orchestrator/policy.mjs';
|
|
15
15
|
import { openStore } from '../lib/orchestrator/store.mjs';
|
|
@@ -38,7 +38,7 @@ const plan = [
|
|
|
38
38
|
|
|
39
39
|
function context(repo) {
|
|
40
40
|
const routing = loadRouting(join(kit, 'routing/routing.json'));
|
|
41
|
-
const { registry } =
|
|
41
|
+
const { registry } = loadPiRegistry(kit, routing);
|
|
42
42
|
const { agents } = loadAgents(join(kit, 'agents'), routing);
|
|
43
43
|
const { policy } = loadPolicy(join(kit, 'orchestration/decision-policy.json'));
|
|
44
44
|
// Stay on cheap-code so a hosted usage limit falls through to the local candidate instead of the hosted-only ladder.
|
|
@@ -8,7 +8,7 @@ import { join, resolve, dirname } from 'node:path';
|
|
|
8
8
|
import { fileURLToPath } from 'node:url';
|
|
9
9
|
import assert from 'node:assert/strict';
|
|
10
10
|
import { loadRouting } from '../lib/routing.mjs';
|
|
11
|
-
import {
|
|
11
|
+
import { loadPiRegistry } from '../adapters/pi/lib/model-registry.mjs';
|
|
12
12
|
import { loadAgents } from '../lib/agents.mjs';
|
|
13
13
|
import { loadPolicy } from '../lib/orchestrator/policy.mjs';
|
|
14
14
|
import { createAgentRunner } from '../lib/orchestrator/runner.mjs';
|
|
@@ -17,7 +17,7 @@ import { createPiInvoker } from '../adapters/pi/lib/invoke.mjs';
|
|
|
17
17
|
|
|
18
18
|
const kit = resolve(dirname(fileURLToPath(import.meta.url)), '..');
|
|
19
19
|
const routing = loadRouting(join(kit, 'routing/routing.json'));
|
|
20
|
-
const { registry } =
|
|
20
|
+
const { registry } = loadPiRegistry(kit, routing);
|
|
21
21
|
const { agents } = loadAgents(join(kit, 'agents'), routing);
|
|
22
22
|
const { policy } = loadPolicy(join(kit, 'orchestration/decision-policy.json'));
|
|
23
23
|
const repo = mkdtempSync(join(tmpdir(), 'ludi-real-orch-'));
|
package/tests/e2e-real-pi.mjs
CHANGED
|
@@ -1,21 +1,21 @@
|
|
|
1
1
|
// Opt-in real E2E through the installed pi CLI and the machine-local model registry.
|
|
2
2
|
// Not part of `node --test`; it spends real model quota. Run:
|
|
3
3
|
// node tests/e2e-real-pi.mjs
|
|
4
|
-
// Requires
|
|
4
|
+
// Requires user-level (or legacy package-local) models.local.json with ready providers (`pi auth check --provider <p>`).
|
|
5
5
|
import { mkdtempSync, cpSync } from 'node:fs';
|
|
6
6
|
import { tmpdir } from 'node:os';
|
|
7
7
|
import { join, resolve, dirname } from 'node:path';
|
|
8
8
|
import { fileURLToPath } from 'node:url';
|
|
9
9
|
import assert from 'node:assert/strict';
|
|
10
10
|
import { loadRouting } from '../lib/routing.mjs';
|
|
11
|
-
import {
|
|
11
|
+
import { loadPiRegistry } from '../adapters/pi/lib/model-registry.mjs';
|
|
12
12
|
import { loadAgents } from '../lib/agents.mjs';
|
|
13
13
|
import { runPipeline, runTests } from '../lib/pipeline.mjs';
|
|
14
14
|
import { createPiInvoker } from '../adapters/pi/lib/invoke.mjs';
|
|
15
15
|
|
|
16
16
|
const kit = resolve(dirname(fileURLToPath(import.meta.url)), '..');
|
|
17
17
|
const routing = loadRouting(join(kit, 'routing/routing.json'));
|
|
18
|
-
const { registry } =
|
|
18
|
+
const { registry } = loadPiRegistry(kit, routing);
|
|
19
19
|
const { agents } = loadAgents(join(kit, 'agents'), routing);
|
|
20
20
|
const repo = mkdtempSync(join(tmpdir(), 'ludi-real-e2e-'));
|
|
21
21
|
cpSync(join(kit, 'tests/fixtures/math-repo'), repo, { recursive: true });
|
|
@@ -9,7 +9,7 @@ import { join, resolve, dirname } from 'node:path';
|
|
|
9
9
|
import { fileURLToPath } from 'node:url';
|
|
10
10
|
import assert from 'node:assert/strict';
|
|
11
11
|
import { loadRouting } from '../lib/routing.mjs';
|
|
12
|
-
import {
|
|
12
|
+
import { loadPiRegistry } from '../adapters/pi/lib/model-registry.mjs';
|
|
13
13
|
import { loadAgents } from '../lib/agents.mjs';
|
|
14
14
|
import { loadPolicy, mergePolicy } from '../lib/orchestrator/policy.mjs';
|
|
15
15
|
import { openStore } from '../lib/orchestrator/store.mjs';
|
|
@@ -37,7 +37,7 @@ const plan = [
|
|
|
37
37
|
];
|
|
38
38
|
|
|
39
39
|
const routing = loadRouting(join(kit, 'routing/routing.json'));
|
|
40
|
-
const { registry } =
|
|
40
|
+
const { registry } = loadPiRegistry(kit, routing);
|
|
41
41
|
const { agents } = loadAgents(join(kit, 'agents'), routing);
|
|
42
42
|
const { policy } = loadPolicy(join(kit, 'orchestration/decision-policy.json'));
|
|
43
43
|
const active = mergePolicy(policy, { decision_policy: { reassign_on_failure: false }, limits: { max_retries: 1 }, agent_runtime: { max_runtime_ms: 180000, max_tool_calls: 25, max_turns: 8 } });
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import test from 'node:test';
|
|
3
3
|
import assert from 'node:assert/strict';
|
|
4
4
|
import { spawnSync } from 'node:child_process';
|
|
5
|
-
import { mkdtempSync } from 'node:fs';
|
|
5
|
+
import { mkdtempSync, cpSync, mkdirSync, copyFileSync } from 'node:fs';
|
|
6
6
|
import { tmpdir } from 'node:os';
|
|
7
7
|
import { join, resolve, dirname } from 'node:path';
|
|
8
8
|
import { fileURLToPath } from 'node:url';
|
|
@@ -15,6 +15,7 @@ import { newTask } from '../lib/orchestrator/task-store.mjs';
|
|
|
15
15
|
import { classifyBackendFailure, createHealthMonitor } from '../lib/orchestrator/health.mjs';
|
|
16
16
|
import { evaluateDecision } from '../lib/orchestrator/escalation.mjs';
|
|
17
17
|
import { orchestrate } from '../lib/orchestrator/orchestrator.mjs';
|
|
18
|
+
import { loadOrchestrationContext, startOrchestration } from '../lib/orchestrator/api.mjs';
|
|
18
19
|
|
|
19
20
|
const kit = resolve(dirname(fileURLToPath(import.meta.url)), '..');
|
|
20
21
|
const routing = loadRouting(join(kit, 'routing/routing.json'));
|
|
@@ -34,6 +35,42 @@ const completed = (task, extra = {}) => ({ ok: true, structured: true, modelId:
|
|
|
34
35
|
const blocked = decisions => ({ ok: true, structured: true, result: { status: 'blocked', summary: 'need a choice', artifacts: [], verification: [], acceptance: [], remainingIssues: [], decisions, newTasks: [] } });
|
|
35
36
|
const dbPath = () => join(mkdtempSync(join(tmpdir(), 'ludi-orch-')), 'state.db');
|
|
36
37
|
|
|
38
|
+
test('unbound task capability fails before a run is created with the user binding path', async () => {
|
|
39
|
+
const path = dbPath();
|
|
40
|
+
const session = openStore(path);
|
|
41
|
+
const userPath = join(dirname(path), 'agent', 'ludi-agent-kit', 'models.local.json');
|
|
42
|
+
try {
|
|
43
|
+
await assert.rejects(orchestrate({ request: 'implement', plan: [spec('a', 'coder')], agents, routing,
|
|
44
|
+
registry: { version: 1, backends: { devin: { provider: 'TODO-provider', model: 'TODO-model' } } },
|
|
45
|
+
bindingPath: userPath, policy: DEFAULT_POLICY, session, runner: { async run() { throw new Error('should not run'); } },
|
|
46
|
+
}), error => error.message.includes('strong-code') && error.message.includes(userPath));
|
|
47
|
+
assert.deepEqual(session.listRuns(), []);
|
|
48
|
+
await assert.rejects(orchestrate({ request: 'plan', planner: 'model', agents, routing,
|
|
49
|
+
registry: { version: 1, backends: {} }, bindingPath: userPath, policy: DEFAULT_POLICY, session,
|
|
50
|
+
invoke: async () => { throw new Error('should not plan'); },
|
|
51
|
+
}), /required capability "orchestration"/);
|
|
52
|
+
assert.deepEqual(session.listRuns(), []);
|
|
53
|
+
} finally { session.close(); }
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test('API start uses the user-level binding path without touching the real agent directory', async () => {
|
|
57
|
+
const home = mkdtempSync(join(tmpdir(), 'ludi-model-home-'));
|
|
58
|
+
const isolatedKit = mkdtempSync(join(tmpdir(), 'ludi-model-kit-'));
|
|
59
|
+
cpSync(join(kit, 'routing'), join(isolatedKit, 'routing'), { recursive: true, filter: path => !path.endsWith('.local.json') });
|
|
60
|
+
cpSync(join(kit, 'agents'), join(isolatedKit, 'agents'), { recursive: true });
|
|
61
|
+
mkdirSync(join(isolatedKit, 'adapters', 'pi'), { recursive: true });
|
|
62
|
+
mkdirSync(join(isolatedKit, 'orchestration'), { recursive: true });
|
|
63
|
+
copyFileSync(join(kit, 'adapters/pi/models.json'), join(isolatedKit, 'adapters/pi/models.json'));
|
|
64
|
+
copyFileSync(join(kit, 'orchestration/decision-policy.json'), join(isolatedKit, 'orchestration/decision-policy.json'));
|
|
65
|
+
const ctx = loadOrchestrationContext({ kit: isolatedKit, storePath: dbPath(), modelOptions: { env: {}, home } });
|
|
66
|
+
try {
|
|
67
|
+
await assert.rejects(startOrchestration(ctx, { request: 'implement', plan: [spec('a', 'coder')],
|
|
68
|
+
runner: { async run() { throw new Error('should not run'); } } }),
|
|
69
|
+
error => error.message.includes(join(home, '.pi', 'agent', 'ludi-agent-kit', 'models.local.json')));
|
|
70
|
+
assert.deepEqual(ctx.session.listRuns(), []);
|
|
71
|
+
} finally { ctx.session.close(); }
|
|
72
|
+
});
|
|
73
|
+
|
|
37
74
|
test('persistent run creation stores the run, tasks and trace', async () => {
|
|
38
75
|
const session = openStore(dbPath());
|
|
39
76
|
const r = await orchestrate({
|
package/tests/pipeline.test.mjs
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// and a fixture-repo E2E with a scripted invoker (no model calls).
|
|
3
3
|
import test from 'node:test';
|
|
4
4
|
import assert from 'node:assert/strict';
|
|
5
|
-
import { mkdtempSync, cpSync, readFileSync, writeFileSync, existsSync } from 'node:fs';
|
|
5
|
+
import { mkdtempSync, mkdirSync, rmSync, cpSync, readFileSync, writeFileSync, existsSync } from 'node:fs';
|
|
6
6
|
import { tmpdir } from 'node:os';
|
|
7
7
|
import { join, resolve, dirname } from 'node:path';
|
|
8
8
|
import { fileURLToPath } from 'node:url';
|
|
@@ -14,6 +14,7 @@ import { normalizeContextPack, classifyPath, relativizeText } from '../lib/norma
|
|
|
14
14
|
import { withEscalation, runPipeline, parseFileBlocks, runTests } from '../lib/pipeline.mjs';
|
|
15
15
|
import { buildSettingsProposal } from '../adapters/pi/lib/settings-proposal.mjs';
|
|
16
16
|
import { createScriptedInvoker } from '../adapters/pi/lib/invoke.mjs';
|
|
17
|
+
import { loadPiRegistry, piUserModelsPath } from '../adapters/pi/lib/model-registry.mjs';
|
|
17
18
|
|
|
18
19
|
const kit = resolve(dirname(fileURLToPath(import.meta.url)), '..');
|
|
19
20
|
const routing = loadRouting(join(kit, 'routing/routing.json'));
|
|
@@ -60,6 +61,31 @@ test('registry: loadRegistry merges files and rejects invalid local file', () =>
|
|
|
60
61
|
assert.equal(shipped.sources.local, null);
|
|
61
62
|
});
|
|
62
63
|
|
|
64
|
+
test('pi user bindings override package-local bindings without writing to the agent directory', () => {
|
|
65
|
+
const dir = mkdtempSync(join(tmpdir(), 'ludi-pi-reg-'));
|
|
66
|
+
const adapter = join(dir, 'adapters', 'pi');
|
|
67
|
+
const home = join(dir, 'home');
|
|
68
|
+
mkdirSync(adapter, { recursive: true });
|
|
69
|
+
const userPath = piUserModelsPath({}, home);
|
|
70
|
+
mkdirSync(dirname(userPath), { recursive: true });
|
|
71
|
+
const file = (path, backends) => writeFileSync(path, JSON.stringify({ version: 1, backends }));
|
|
72
|
+
file(join(adapter, 'models.json'), { devin: { provider: 'TODO-provider', model: 'TODO-model', thinking: 'low' } });
|
|
73
|
+
file(join(adapter, 'models.local.json'), { devin: { provider: 'package', model: 'old' }, sol: { provider: 'package', model: 'fallback' } });
|
|
74
|
+
file(userPath, { devin: { provider: 'user', model: 'new' } });
|
|
75
|
+
const merged = loadPiRegistry(dir, routing, { env: {}, home });
|
|
76
|
+
assert.equal(merged.registry.backends.devin.model, 'new');
|
|
77
|
+
assert.equal(merged.registry.backends.devin.thinking, 'low');
|
|
78
|
+
assert.equal(merged.registry.backends.sol.model, 'fallback');
|
|
79
|
+
assert.equal(merged.sources.user, userPath);
|
|
80
|
+
rmSync(join(adapter, 'models.local.json'));
|
|
81
|
+
assert.equal(loadPiRegistry(dir, routing, { env: {}, home }).registry.backends.devin.model, 'new');
|
|
82
|
+
file(join(adapter, 'models.local.json'), { devin: { provider: 'package', model: 'old' } });
|
|
83
|
+
assert.equal(loadPiRegistry(dir, routing, { env: { PI_CODING_AGENT_DIR: join(dir, 'empty') }, home }).sources.user, null);
|
|
84
|
+
assert.equal(piUserModelsPath({ PI_CODING_AGENT_DIR: join(dir, 'custom') }, home), join(dir, 'custom', 'ludi-agent-kit', 'models.local.json'));
|
|
85
|
+
file(userPath, { devin: { apiKey: 'bad' } });
|
|
86
|
+
assert.throws(() => loadPiRegistry(dir, routing, { env: {}, home }), /must not store credentials/);
|
|
87
|
+
});
|
|
88
|
+
|
|
63
89
|
// ---------- resolution ----------
|
|
64
90
|
test('resolve: agent -> capability -> backend chain -> concrete model; placeholders skipped', () => {
|
|
65
91
|
const r = resolveAgents(agents, routing, REG);
|