@ludi-uni/ludi-agent-kit 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +55 -0
- package/LICENSE +21 -0
- package/README.md +107 -0
- package/adapters/codex/README.md +24 -0
- package/adapters/codex/skill-metadata/visual-verification/agents/openai.yaml +7 -0
- package/adapters/pi/README.md +88 -0
- package/adapters/pi/browser/agent-browser.mjs +193 -0
- package/adapters/pi/lib/invoke.mjs +55 -0
- package/adapters/pi/lib/list-models.mjs +29 -0
- package/adapters/pi/lib/settings-proposal.mjs +34 -0
- package/adapters/pi/lib/subagent.mjs +175 -0
- package/adapters/pi/loop-guard/index.js +51 -0
- package/adapters/pi/maintenance-policy.json +36 -0
- package/adapters/pi/mcp.template.json +4 -0
- package/adapters/pi/model-catalog.json +97 -0
- package/adapters/pi/models.json +13 -0
- package/adapters/pi/models.local.example.json +14 -0
- package/adapters/pi/orchestrator-ext/command.mjs +14 -0
- package/adapters/pi/orchestrator-ext/index.js +150 -0
- package/adapters/pi/settings.template.json +7 -0
- package/adapters/pi/shell-gate/index.js +70 -0
- package/adapters/pi/sync-pi.ps1 +137 -0
- package/agents/README.md +26 -0
- package/agents/browser.md +64 -0
- package/agents/coder.md +31 -0
- package/agents/orchestrator.md +37 -0
- package/agents/reviewer.md +32 -0
- package/agents/scout.md +35 -0
- package/agents/tester.md +28 -0
- package/agents/visual.md +28 -0
- package/context-pack/SPEC.md +101 -0
- package/context-pack/context-pack.schema.json +79 -0
- package/context-pack/examples/example-fix.md +44 -0
- package/docs/architecture.md +55 -0
- package/docs/migration-from-codex-setting.md +44 -0
- package/docs/model-maintenance.md +401 -0
- package/docs/orchestrator.md +155 -0
- package/docs/phase2-report.md +39 -0
- package/docs/roadmap.md +27 -0
- package/docs/third-party.md +15 -0
- package/lib/agents.mjs +79 -0
- package/lib/context-pack.mjs +215 -0
- package/lib/job.mjs +312 -0
- package/lib/language-policy.mjs +27 -0
- package/lib/maintenance-exec.mjs +377 -0
- package/lib/maintenance-runner.mjs +266 -0
- package/lib/maintenance.mjs +422 -0
- package/lib/normalize.mjs +101 -0
- package/lib/observe/differ.mjs +185 -0
- package/lib/observe/observation.mjs +147 -0
- package/lib/observe/observers.mjs +134 -0
- package/lib/observe/sources.mjs +154 -0
- package/lib/orchestrator/activity.mjs +249 -0
- package/lib/orchestrator/api.mjs +151 -0
- package/lib/orchestrator/contract.mjs +68 -0
- package/lib/orchestrator/escalation.mjs +84 -0
- package/lib/orchestrator/evaluator.mjs +92 -0
- package/lib/orchestrator/failures.mjs +88 -0
- package/lib/orchestrator/health.mjs +53 -0
- package/lib/orchestrator/orchestrator.mjs +483 -0
- package/lib/orchestrator/permissions.mjs +64 -0
- package/lib/orchestrator/planner.mjs +194 -0
- package/lib/orchestrator/policy.mjs +134 -0
- package/lib/orchestrator/router.mjs +45 -0
- package/lib/orchestrator/runner.mjs +278 -0
- package/lib/orchestrator/shell-policy.mjs +52 -0
- package/lib/orchestrator/store.mjs +581 -0
- package/lib/orchestrator/task-store.mjs +79 -0
- package/lib/orchestrator/turn-budget.mjs +63 -0
- package/lib/orchestrator/worktree.mjs +72 -0
- package/lib/pipeline.mjs +279 -0
- package/lib/registry.mjs +63 -0
- package/lib/resolve.mjs +35 -0
- package/lib/routing.mjs +137 -0
- package/lib/telemetry.mjs +222 -0
- package/mcp/README.md +11 -0
- package/mcp/servers.json +13 -0
- package/orchestration/decision-policy.json +66 -0
- package/package.json +56 -0
- package/routing/README.md +24 -0
- package/routing/routing.json +81 -0
- package/routing/routing.schema.json +66 -0
- package/rules/README.md +10 -0
- package/rules/common.md +52 -0
- package/rules/loop-prevention.md +15 -0
- package/rules/repo-local.md +6 -0
- package/scripts/check-environment.ps1 +22 -0
- package/scripts/context-pack.mjs +17 -0
- package/scripts/e2e-investigate-repro.mjs +66 -0
- package/scripts/model-maintenance-job.mjs +59 -0
- package/scripts/observe-models.mjs +97 -0
- package/scripts/orchestrate.mjs +137 -0
- package/scripts/reevaluate-models.mjs +95 -0
- package/scripts/report-model-maintenance.mjs +70 -0
- package/scripts/resolve-capabilities.mjs +39 -0
- package/scripts/run-pipeline.mjs +56 -0
- package/scripts/sync-agents-md.ps1 +10 -0
- package/scripts/validate.mjs +71 -0
- package/skills/README.md +14 -0
- package/skills/pi-workflow/SKILL.md +26 -0
- package/skills/pi-workflow/references/code-investigation-and-fix.md +16 -0
- package/skills/pi-workflow/references/research.md +14 -0
- package/skills/pi-workflow/references/review.md +11 -0
- package/skills/pi-workflow/references/visual-work.md +14 -0
- package/skills/project-management/SKILL.md +106 -0
- package/skills/project-management/references/operations.md +52 -0
- package/skills/visual-verification/SKILL.md +88 -0
- package/skills/visual-verification/scripts/analyze-speech.ps1 +346 -0
- package/skills/visual-verification/scripts/backends/whisperx_backend.py +234 -0
- package/skills/visual-verification/scripts/common.ps1 +387 -0
- package/skills/visual-verification/scripts/contact-sheet.ps1 +121 -0
- package/skills/visual-verification/scripts/desktop-discover.ps1 +45 -0
- package/skills/visual-verification/scripts/desktop-inspect.ps1 +67 -0
- package/skills/visual-verification/scripts/desktop-record.ps1 +97 -0
- package/skills/visual-verification/scripts/desktop-screenshot.ps1 +65 -0
- package/skills/visual-verification/scripts/evaluate-sync.ps1 +249 -0
- package/skills/visual-verification/scripts/extract-frames.ps1 +79 -0
- package/skills/visual-verification/scripts/inspect-media.ps1 +138 -0
- package/skills/visual-verification/scripts/record-av.ps1 +102 -0
- package/skills/visual-verification/scripts/record.ps1 +72 -0
- package/skills/visual-verification/scripts/screenshot.ps1 +44 -0
- package/skills/visual-verification/scripts/waveform.ps1 +450 -0
- package/skills/visual-verification/scripts/winapp-common.ps1 +465 -0
- package/tests/activity.test.mjs +252 -0
- package/tests/attempt-budget.test.mjs +102 -0
- package/tests/browser.test.mjs +121 -0
- package/tests/context-pack.test.mjs +98 -0
- package/tests/dirty-gate.test.mjs +211 -0
- package/tests/e2e-browser.mjs +66 -0
- package/tests/e2e-real-orchestrator-resume.mjs +101 -0
- package/tests/e2e-real-orchestrator.mjs +41 -0
- package/tests/e2e-real-pi.mjs +27 -0
- package/tests/e2e-real-tool-orchestrator.mjs +66 -0
- package/tests/fixtures/browser-page/index.html +20 -0
- package/tests/fixtures/maintenance/availability.txt +5 -0
- package/tests/fixtures/maintenance/catalog.json +74 -0
- package/tests/fixtures/maintenance/events.json +13 -0
- package/tests/fixtures/math-repo/README.md +3 -0
- package/tests/fixtures/math-repo/package.json +7 -0
- package/tests/fixtures/math-repo/src/math.js +11 -0
- package/tests/fixtures/math-repo/test/math.test.js +7 -0
- package/tests/fixtures/observe/announcements.json +8 -0
- package/tests/fixtures/orch-concurrent-child.mjs +44 -0
- package/tests/fixtures/orch-persist-child.mjs +61 -0
- package/tests/job.test.mjs +230 -0
- package/tests/kit.test.mjs +79 -0
- package/tests/language-policy.test.mjs +93 -0
- package/tests/loop-guard.test.mjs +60 -0
- package/tests/maintenance-exec.test.mjs +218 -0
- package/tests/maintenance-runner.test.mjs +222 -0
- package/tests/maintenance.test.mjs +195 -0
- package/tests/observe.test.mjs +283 -0
- package/tests/observer-registry.test.mjs +157 -0
- package/tests/orchestrator-cleanup.test.mjs +358 -0
- package/tests/orchestrator-command.test.mjs +14 -0
- package/tests/orchestrator-persist.test.mjs +375 -0
- package/tests/orchestrator-tools.test.mjs +215 -0
- package/tests/orchestrator.test.mjs +396 -0
- package/tests/package.test.mjs +37 -0
- package/tests/pipeline.test.mjs +239 -0
- package/tests/planner-classification.test.mjs +81 -0
- package/tests/planner-split.test.mjs +67 -0
- package/tests/qoder-observer.test.mjs +266 -0
- package/tests/reassign-progression.test.mjs +104 -0
- package/tests/retry-escalation.test.mjs +120 -0
- package/tests/routing.test.mjs +110 -0
- package/tests/sqlite-concurrency.test.mjs +178 -0
- package/tests/task-global-e2e.test.mjs +63 -0
- package/tests/task-global-failed.test.mjs +134 -0
- package/tests/telemetry.test.mjs +173 -0
- package/tests/test-sync-pi.ps1 +56 -0
- package/tests/turn-budget.test.mjs +106 -0
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# Architecture
|
|
2
|
+
|
|
3
|
+
## Layers
|
|
4
|
+
|
|
5
|
+
```
|
|
6
|
+
┌─────────────────────────────────────────────────────────────┐
|
|
7
|
+
│ common (backend-neutral) │
|
|
8
|
+
│ rules/ skills/ agents/ routing/ context-pack/ lib/ │
|
|
9
|
+
│ mcp/servers.json │
|
|
10
|
+
├─────────────────────────────────────────────────────────────┤
|
|
11
|
+
│ adapters/<backend-runtime>/ (pi, codex, later: local, …) │
|
|
12
|
+
│ models.json templates sync script runtime-specific glue │
|
|
13
|
+
├─────────────────────────────────────────────────────────────┤
|
|
14
|
+
│ user environment (~/.pi/agent, ~/.codex) — never owned │
|
|
15
|
+
└─────────────────────────────────────────────────────────────┘
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
- **rules/** — text policy every agent gets. Two files: `common.md`, `loop-prevention.md`.
|
|
19
|
+
- **skills/** — knowledge + scripts that any runtime with Agent-Skills support can load.
|
|
20
|
+
- **agents/** — role contracts. Frontmatter is pi-subagents compatible; the only kit-specific
|
|
21
|
+
key is `capability`.
|
|
22
|
+
- **routing/** — capability → backend, with fallback and escalation ladders.
|
|
23
|
+
- **context-pack/** — the interchange format between cheap and expensive agents.
|
|
24
|
+
- **lib/** — validators/loaders; plain Node ESM, no dependencies, used by scripts and tests.
|
|
25
|
+
- **adapters/** — everything concrete. pi is the first-class runtime; Codex is just a backend.
|
|
26
|
+
|
|
27
|
+
## Data flow (target state)
|
|
28
|
+
|
|
29
|
+
1. User task → parent (pi main session).
|
|
30
|
+
2. Parent classifies the task into a capability (manual today; a classifier later).
|
|
31
|
+
3. `scout` (cheap-code) explores and emits a **Context Pack**.
|
|
32
|
+
4. Parent validates the pack (`lib/context-pack.mjs`) and hands it to `coder` / `visual` /
|
|
33
|
+
`reviewer`, whose capability resolves through routing → adapter model map.
|
|
34
|
+
5. On failure, escalation ladder moves to the next capability (e.g. cheap-code → strong-code).
|
|
35
|
+
|
|
36
|
+
The orchestrator (`lib/orchestrator/`, `docs/orchestrator.md`) runs steps 2 to 5 as a bounded loop.
|
|
37
|
+
It uses the same agents, routing and adapter invoker. Sub-agent decision requests go through
|
|
38
|
+
`orchestration/decision-policy.json` before the user is involved. A run can be stored in SQLite
|
|
39
|
+
and resumed from a later process; see `docs/orchestrator.md`.
|
|
40
|
+
|
|
41
|
+
Separately, `scripts/reevaluate-models.mjs` is a periodic maintenance task: when provider
|
|
42
|
+
conditions change (free-campaign end, quota/rate-limit change, deprecation, price change,
|
|
43
|
+
new model), it re-scores each capability's binding against `adapters/<x>/model-catalog.json`
|
|
44
|
+
and writes a proposal to `adapters/<x>/out/model-maintenance.proposal.json`. Phase 2 wraps it
|
|
45
|
+
in execution tiers — monitor / evaluate / reconfigure — each selecting a model under a
|
|
46
|
+
free -> cheapest-sufficient -> local-fallback policy (`lib/maintenance-exec.mjs`,
|
|
47
|
+
`adapters/<x>/maintenance-policy.json`), audited in `out/model-maintenance.run.json`.
|
|
48
|
+
Proposals only — applying is a manual `models.local.json` edit + re-resolve.
|
|
49
|
+
See `docs/model-maintenance.md`.
|
|
50
|
+
|
|
51
|
+
## Windows-native constraints
|
|
52
|
+
|
|
53
|
+
- PowerShell 7 scripts; Junctions (no symlink privilege needed); no WSL.
|
|
54
|
+
- Paths derive from `$env:USERPROFILE`, `$env:PI_CODING_AGENT_DIR`, `$PSScriptRoot`; never a
|
|
55
|
+
fixed user name (enforced by `tests/kit.test.mjs`).
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# Migration from codex-setting
|
|
2
|
+
|
|
3
|
+
`codex-setting` is frozen: no new features; it continues to work as the Codex installer
|
|
4
|
+
and as the current provider of the `~/.pi/agent` Junctions on this machine.
|
|
5
|
+
|
|
6
|
+
## Classification of codex-setting files
|
|
7
|
+
|
|
8
|
+
### Reused (copied into this kit)
|
|
9
|
+
| codex-setting | ludi-agent-kit |
|
|
10
|
+
| --- | --- |
|
|
11
|
+
| `skills/visual-verification/**` (minus `agents/openai.yaml`) | `skills/visual-verification/` (SKILL.md path examples de-Codexed) |
|
|
12
|
+
| `skills/project-management/**` | `skills/project-management/` |
|
|
13
|
+
| `pi/harness/skills/pi-workflow/**` | `skills/pi-workflow/` |
|
|
14
|
+
| `pi/harness/AGENTS.md` | `rules/common.md` (plus a Context-Pack sentence) |
|
|
15
|
+
| `shared/loop-prevention.md` | `rules/loop-prevention.md` |
|
|
16
|
+
| `pi/extensions/index.js` | `adapters/pi/loop-guard/index.js` (policy path updated) |
|
|
17
|
+
| `tests/test-swe-loop-guard.mjs` | `tests/loop-guard.test.mjs` (fixture provider names neutralized) |
|
|
18
|
+
| `skills/visual-verification/agents/openai.yaml` | `adapters/codex/skill-metadata/...` |
|
|
19
|
+
| `LICENSE`, `.gitattributes` | same |
|
|
20
|
+
|
|
21
|
+
### Left in codex-setting (Codex-specific)
|
|
22
|
+
`scripts/install.ps1`, `update.ps1`, `check.ps1`, `common.ps1`, `tests/test-installation.ps1`,
|
|
23
|
+
`skills/subagent-management/`, `codex/README.md`, `shared/resources.json`
|
|
24
|
+
(`commonPolicyEndHeading`, `~/.codex/AGENTS.md` projection).
|
|
25
|
+
|
|
26
|
+
### Re-implemented rather than copied
|
|
27
|
+
| codex-setting | reason | kit replacement |
|
|
28
|
+
| --- | --- | --- |
|
|
29
|
+
| `scripts/install-pi.ps1`, `migrate-pi-harness.ps1`, `activate-pi-harness.ps1` | coupled to codex-setting layout, Codex AGENTS projection, PATH/pi-web edits | `adapters/pi/sync-pi.ps1` (dry-run default, no settings/PATH writes) |
|
|
30
|
+
| `scripts/check-pi.ps1/.mjs`, `tests/test-pi-*.{ps1,mjs}` | read `codex-settings/pi.json` state | `tests/test-sync-pi.ps1`; RPC startup check deferred |
|
|
31
|
+
| `pi/agents/{main,coder,vision,reviewer}.md` | hardcoded `model: swe-2-high` | `agents/{scout,coder,visual,reviewer}.md` with `capability` |
|
|
32
|
+
| `pi/README.md`, `HARNESS-DESIGN.md`, `VALIDATION.md` | historical | `docs/` |
|
|
33
|
+
|
|
34
|
+
## Cut-over on this machine (not performed)
|
|
35
|
+
|
|
36
|
+
1. `pwsh adapters/pi/sync-pi.ps1` — review `adapters/pi/out/plan.json`.
|
|
37
|
+
2. `pwsh adapters/pi/sync-pi.ps1 -Apply -BackupConflicts` — re-points skill Junctions,
|
|
38
|
+
writes generated AGENTS.md (old one backed up), adds `agents/ludi-agent-kit` and
|
|
39
|
+
`extensions/ludi-agent-kit`.
|
|
40
|
+
3. Manually remove the old `agents` and `extensions/codex-settings` Junctions (link only)
|
|
41
|
+
to avoid a duplicate loop guard.
|
|
42
|
+
4. Restart pi; `pi` → `/reload`; confirm skills appear and the loop guard loads once.
|
|
43
|
+
5. Rollback: restore `ludi-agent-kit/backup-*/AGENTS.md`, recreate the old Junctions from
|
|
44
|
+
codex-setting (`scripts/install-pi.ps1 -Compact`).
|
|
@@ -0,0 +1,401 @@
|
|
|
1
|
+
# Model maintenance — periodic subagent re-evaluation
|
|
2
|
+
|
|
3
|
+
Provider conditions change: free campaigns end, quotas and rate limits move, models are
|
|
4
|
+
deprecated or removed, prices change, new models appear. `scripts/reevaluate-models.mjs`
|
|
5
|
+
is the periodic maintenance task that re-checks whether each capability's current model
|
|
6
|
+
binding is still rational — and produces a **proposal only**. Nothing is applied
|
|
7
|
+
automatically.
|
|
8
|
+
|
|
9
|
+
## What it reads
|
|
10
|
+
|
|
11
|
+
| Input | Path | Role |
|
|
12
|
+
| --- | --- | --- |
|
|
13
|
+
| routing | `routing/routing.json` | capabilities, backend tiers, `requires.vision` |
|
|
14
|
+
| agents | `agents/*.md` | capability → agent names (for `affected.agents`) |
|
|
15
|
+
| model bindings | `adapters/<x>/models.json` + `models.local.json` | current provider/model per backend |
|
|
16
|
+
| catalog | `adapters/<x>/model-catalog.json` | model facts: status, cost, contextK, vision, toolUse, quality scores, `location`/`local` power metadata |
|
|
17
|
+
| exec policy | `adapters/<x>/maintenance-policy.json` | Phase 2: requiredQuality per tier, capabilityRequirements (coding/reasoning floors), cost weights, electricity price, escalation thresholds |
|
|
18
|
+
| events | `--events <file>` | what changed (see event types below) |
|
|
19
|
+
| availability | `--check-availability` or `--availability-file` | optional `pi --list-models` probe |
|
|
20
|
+
|
|
21
|
+
## Free-capacity planning
|
|
22
|
+
|
|
23
|
+
Each maintenance run includes `freeCapacityPlan` even if no provider event arrived.
|
|
24
|
+
For each capability it lists free cloud candidates that meet configured coding/reasoning
|
|
25
|
+
floors, required vision, tool-use and known availability. It reports the suggested
|
|
26
|
+
candidate, campaign cutoff, current primary and existing fallback backends; this is
|
|
27
|
+
**advisory**, not a routing or live settings change. Catalog scores are heuristic:
|
|
28
|
+
validate task success before a binding change. A missing quality requirement produces
|
|
29
|
+
no automatic recommendation. If no free model qualifies, retain the existing route
|
|
30
|
+
or review a paid fallback rather than weakening the capability requirement.
|
|
31
|
+
|
|
32
|
+
`freeUntil` is an exclusive UTC timestamp. At/after the cutoff the catalog's effective
|
|
33
|
+
view uses `postCampaignCost` (or unknown cost), and a maintenance run detects the
|
|
34
|
+
expiry even without an explicit event. Check the provider's actual entitlement and
|
|
35
|
+
billing: catalog dates, especially user-reported campaign dates, are not a guarantee.
|
|
36
|
+
For Devin SWE-2-high, "through 10/10" is interpreted as through 2026-10-10 JST,
|
|
37
|
+
with the exclusive cutoff 2026-10-11 00:00 JST. Confirm year/timezone with Devin
|
|
38
|
+
before relying on that boundary. Qoder's campaign end remains unknown, so verify
|
|
39
|
+
its live free entitlement before using it at scale.
|
|
40
|
+
|
|
41
|
+
## Event types
|
|
42
|
+
|
|
43
|
+
`free-campaign-ended`, `free-quota-changed`, `rate-limits-changed`, `price-changed`,
|
|
44
|
+
`deprecated`, `removed`, `model-added`. One event file:
|
|
45
|
+
|
|
46
|
+
```json
|
|
47
|
+
{"version":1,"events":[{"type":"free-campaign-ended","provider":"qoder","model":"Qwen3.8-Flash","asOf":"2026-03-01"}]}
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## What it does
|
|
51
|
+
|
|
52
|
+
For every backend that is the **primary** of at least one capability:
|
|
53
|
+
|
|
54
|
+
1. Apply events to the catalog entry (`free-campaign-ended` re-prices to
|
|
55
|
+
`postCampaignCost`; `deprecated`/`removed` change status).
|
|
56
|
+
2. Score current and candidate models with tier-weighted dimensions — cost, coding,
|
|
57
|
+
reasoning, speed, context, tool use (`lib/maintenance.mjs` `TIER_WEIGHTS`: `free`/`low`
|
|
58
|
+
tiers weight cost, `mid`/`high` weight quality). Capability `requires.vision` and
|
|
59
|
+
`toolUse:none` filter the candidate pool.
|
|
60
|
+
3. Decide:
|
|
61
|
+
- **keep** — best candidate advantage is below the margin (default 8, `--margin`), or
|
|
62
|
+
the candidate data is low-confidence. *A free campaign ending is not by itself a
|
|
63
|
+
reason to switch*: the current model is re-scored with its post-event cost and only
|
|
64
|
+
loses on merit.
|
|
65
|
+
- **propose** — advantage ≥ margin, or a forced migration (`removed` / `deprecated` /
|
|
66
|
+
absent from a provider listing that was successfully probed).
|
|
67
|
+
- **insufficient-data** — no eligible candidate or the current model can't be scored.
|
|
68
|
+
- **skip** — backend unbound or placeholder. Fallback-only backends are reported as
|
|
69
|
+
`not-primary`, not evaluated.
|
|
70
|
+
|
|
71
|
+
## Information sources and fallback
|
|
72
|
+
|
|
73
|
+
| Source | How | Fallback when unavailable |
|
|
74
|
+
| --- | --- | --- |
|
|
75
|
+
| Model facts (price, context, scores) | `model-catalog.json`, maintained by hand from provider pricing pages / model docs / market snapshots | `scores: null` dims are excluded from scoring; a fully-unknown current model yields `insufficient-data`, never a speculative switch |
|
|
76
|
+
| Provider events | `--events` file written by the operator (or a future fetcher) | no events → availability + catalog status still catch `removed`/`deprecated` |
|
|
77
|
+
| Live availability | `--check-availability` runs `pi --list-models` (read-only) | probe failure → catalog status only; `infoStatus.availability` records which path ran. A provider missing from the listing means *unknown*, never *gone* |
|
|
78
|
+
|
|
79
|
+
## What a proposal contains
|
|
80
|
+
|
|
81
|
+
Each `changes[]` entry: `changeReason`, `affected` (backend, capabilities, agents),
|
|
82
|
+
`currentModel`, `proposedModel`, `expectedCostImpact`, `expectedQualityImpact`,
|
|
83
|
+
`scores` (current/proposed/delta/margin), `confidence`, and `rollback` — the previous
|
|
84
|
+
binding embedded plus the manual steps to restore it.
|
|
85
|
+
|
|
86
|
+
## Applying and rolling back
|
|
87
|
+
|
|
88
|
+
The task writes only `adapters/<x>/out/model-maintenance.proposal.json`. To apply:
|
|
89
|
+
edit `adapters/<x>/models.local.json` per `proposedModel`, re-run
|
|
90
|
+
`scripts/resolve-capabilities.mjs`, then merge `out/settings.proposal.json` into
|
|
91
|
+
`~/.pi/agent/settings.json` yourself. Rollback is the same flow using
|
|
92
|
+
`rollback.previousBinding`. It never writes `settings.json`, `models.json`,
|
|
93
|
+
`models.local.json`, `routing.json`, or anything under `~/.pi`.
|
|
94
|
+
|
|
95
|
+
## Phase 2 — execution tiers (monitor / evaluate / reconfigure)
|
|
96
|
+
|
|
97
|
+
The maintenance run itself has a cost hierarchy: the AI that watches and judges model
|
|
98
|
+
config is chosen under a **free -> cheapest-sufficient -> local fallback** policy, and
|
|
99
|
+
only escalates to a premium tier when the decision warrants it. The run is a dry-run
|
|
100
|
+
decision layer: `runMaintenancePlan` (`lib/maintenance-exec.mjs`) selects which model
|
|
101
|
+
each tier *would* run on and records why — no model is invoked, no config is written.
|
|
102
|
+
|
|
103
|
+
```
|
|
104
|
+
monitor cheapest watcher: diffs events/catalog/availability ->
|
|
105
|
+
{ changed, reasons, affectedModels, severity, escalationRequired }
|
|
106
|
+
| changed
|
|
107
|
+
evaluate cheapest-sufficient judgement: interprets evaluateMaintenance() ->
|
|
108
|
+
keep-inside-margin | proposal
|
|
109
|
+
| complex / low-confidence / large blast radius
|
|
110
|
+
reconfigure premium tier (a strong coding model at that point in time — never a
|
|
111
|
+
hardcoded name): multi-capability or structural re-optimisation
|
|
112
|
+
|
|
|
113
|
+
proposal still only out/model-maintenance.proposal.json
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
### cheapest-sufficient selection
|
|
117
|
+
|
|
118
|
+
Per tier, `selectTierModel` computes for every catalog model:
|
|
119
|
+
|
|
120
|
+
```
|
|
121
|
+
quality = tier-weighted 0-100 score (coding/reasoning/speed/context/toolUse)
|
|
122
|
+
eligible = usable status AND toolUse != none AND availability != 'unavailable'
|
|
123
|
+
AND quality >= requiredQuality[tier]
|
|
124
|
+
effectiveCostUsd/run = api($/1M blended) + electricity + speedPenalty
|
|
125
|
+
selected = free cloud eligible? -> cheapest of those
|
|
126
|
+
else cheapest eligible cloud
|
|
127
|
+
else cheapest eligible local (local fallback)
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
Free is preferred **only when it meets the quality bar** — a free model below
|
|
131
|
+
`requiredQuality` is rejected and selection proceeds to cheapest-sufficient.
|
|
132
|
+
Unknown cost components fall back to the cheapest *known* value in the pool, so an
|
|
133
|
+
unpriced model is never treated as free. `availability: unknown` (probe absent or
|
|
134
|
+
provider unseen) stays eligible; only `unavailable` (absent from a successful listing)
|
|
135
|
+
is excluded.
|
|
136
|
+
|
|
137
|
+
### Escalation conditions (evaluate -> reconfigure)
|
|
138
|
+
|
|
139
|
+
Any one of: proposed changes span >= `minCapabilities` (2) capabilities or >=
|
|
140
|
+
`minAgents` (3) agents; largest quality swing >= `minQualitySwing` (15 points); a
|
|
141
|
+
winning margin below `maxScoreDelta` (4 — a hard call); low-confidence or
|
|
142
|
+
insufficient-data decisions; a structural event (`removed`/`deprecated`); or monitor
|
|
143
|
+
severity `high`. The escalation is recorded on the run and on every proposed change as
|
|
144
|
+
`escalationReason`, `sourceTier`, `targetTier`, `affectedCapabilities`,
|
|
145
|
+
`estimatedDecisionCostUsd`.
|
|
146
|
+
|
|
147
|
+
### Local fallback and electricity cost
|
|
148
|
+
|
|
149
|
+
Catalog entries with `location: "local"` may carry `local: { powerWatts, taskMinutes }`.
|
|
150
|
+
Estimated run cost: `(powerWatts/1000) * (taskMinutes/60) * electricityPricePerKwh`
|
|
151
|
+
(`maintenance-policy.json`, default $0.30/kWh). Local entries join the same scoring —
|
|
152
|
+
if a cloud API run is cheaper than the electricity estimate, the cloud model wins
|
|
153
|
+
(Case E in `tests/maintenance-exec.test.mjs`).
|
|
154
|
+
|
|
155
|
+
### Run report
|
|
156
|
+
|
|
157
|
+
Every run writes `adapters/<x>/out/model-maintenance.run.json` (gitignored): per-tier
|
|
158
|
+
`selected` model, `selectionPath` (`free-cloud` / `cheapest-sufficient-cloud` /
|
|
159
|
+
`local-fallback`), `selectionReason`, full `candidates` list with `rejectedReasons`,
|
|
160
|
+
`estimatedDecisionCostUsd`, `fallbackOccurred`, the monitor output, the escalation
|
|
161
|
+
record, and the Phase 1 proposal when produced. No credentials are ever stored.
|
|
162
|
+
|
|
163
|
+
Policy defaults live in `lib/maintenance-exec.mjs` (`DEFAULT_POLICY`); the adapter
|
|
164
|
+
overrides them in `adapters/<x>/maintenance-policy.json` (`--policy` to point elsewhere).
|
|
165
|
+
|
|
166
|
+
## Phase 3 — real invocation (`--live`)
|
|
167
|
+
|
|
168
|
+
`node scripts/reevaluate-models.mjs --live` runs the same tiered pipeline but actually
|
|
169
|
+
calls the selected models through the pi adapter invoker (`adapters/pi/lib/invoke.mjs`:
|
|
170
|
+
`pi -p --model <provider/id[:thinking]> --no-tools --no-session --no-approve`). No new
|
|
171
|
+
execution substrate — model ids come from the same catalog/selection path.
|
|
172
|
+
|
|
173
|
+
### Deterministic authority boundary
|
|
174
|
+
|
|
175
|
+
The engine decides: eligible models, `requiredQuality`, cost, availability, escalation
|
|
176
|
+
conditions, and the keep/propose outcome. The LLM only interprets and annotates
|
|
177
|
+
(`reasoningSummary`, `proposalNotes`, `recommendedActions`). `recommendedModels`
|
|
178
|
+
outside the catalog are stripped and recorded as `rejectedRecommendations`. If a tier's
|
|
179
|
+
output fails schema validation twice (one retry), the run continues on the
|
|
180
|
+
deterministic result alone (`degradedToDeterministic`).
|
|
181
|
+
|
|
182
|
+
### Tier prompts and schemas
|
|
183
|
+
|
|
184
|
+
Each tier gets a fixed prompt (`buildTierPrompt`) embedding the deterministic facts and
|
|
185
|
+
a required JSON shape (`TIER_SCHEMAS`):
|
|
186
|
+
|
|
187
|
+
- **monitor** — organise diffs; `decision: changed|no-change`, `severity`,
|
|
188
|
+
`evaluateNeeded`. Cannot propose routing changes.
|
|
189
|
+
- **evaluate** — judge the deterministic result; `decision: keep|propose|insufficient-data`,
|
|
190
|
+
`confidence`, `proposalNotes`. Confidence below `escalation.minEvaluateConfidence`
|
|
191
|
+
(0.5) escalates.
|
|
192
|
+
- **reconfigure** — re-review the whole picture for structural/multi-capability
|
|
193
|
+
changes; adds `routingNotes`. Output is advisory only.
|
|
194
|
+
|
|
195
|
+
### Cost per run
|
|
196
|
+
|
|
197
|
+
`estimatedCostPerRun` normalises cloud and local to USD/run:
|
|
198
|
+
`api = (inTokens x $/1M-in + outTokens x $/1M-out)` from `taskProfiles[tier]`
|
|
199
|
+
(`estimatedInputTokens`/`estimatedOutputTokens`/`estimatedTaskMinutes`); local uses
|
|
200
|
+
`powerWatts x taskMinutes x electricityPricePerKwh`. Plus a small speed penalty.
|
|
201
|
+
|
|
202
|
+
### Failure and fallback
|
|
203
|
+
|
|
204
|
+
On invoke failure / quota / timeout / two bad outputs, the runner walks the
|
|
205
|
+
deterministic `ordered` candidate chain (free-cloud, then sufficient pool by cost).
|
|
206
|
+
Premium models (`premium: true` in the catalog) only enter the chain at the
|
|
207
|
+
reconfigure tier. Audit lands in `out/model-maintenance.run.json` under `invocations[]`:
|
|
208
|
+
model, ok/failureClass, schemaValid, latencyMs, tokens/actualCost when reported,
|
|
209
|
+
estimatedCostUsd, fallback chain. Prompts and credentials are never stored.
|
|
210
|
+
|
|
211
|
+
## Phase 4 — observation layer (`scripts/observe-models.mjs`)
|
|
212
|
+
|
|
213
|
+
External provider conditions are collected as **normalized observations**, stored,
|
|
214
|
+
deduped, diffed against the catalog, and turned into a *proposal* — never applied.
|
|
215
|
+
The layer is separate from the core engine (`lib/observe/`); the maintenance logic
|
|
216
|
+
stays free of provider/web specifics.
|
|
217
|
+
|
|
218
|
+
```
|
|
219
|
+
external sources (manual / pi --list-models / fixture announcements)
|
|
220
|
+
-> source adapters (lib/observe/sources.mjs)
|
|
221
|
+
-> normalized observations (validate + hash)
|
|
222
|
+
-> out/model-observations.jsonl (dedupe: same hash = duplicate; older = stale)
|
|
223
|
+
-> resolve conflicts (newer > trust > confidence; equal-trust major diff = conflict)
|
|
224
|
+
-> out/catalog-diff.json + out/model-catalog.proposal.json
|
|
225
|
+
-> --preview-maintenance: hypothetical catalog -> Phase 1-3 dry-run
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
### Observation schema
|
|
229
|
+
|
|
230
|
+
`{ provider, model, observedAt, source:{type:manual|api|web|cli, trust, url, label},
|
|
231
|
+
changes:{availability,status,free,freeUntil,inputPricePer1M,outputPricePer1M,
|
|
232
|
+
contextK,toolUse,vision}, confidence:0-1, evidence:[] }`. `null` = not observed —
|
|
233
|
+
distinct from `false`/`0`.
|
|
234
|
+
|
|
235
|
+
### Trust order
|
|
236
|
+
|
|
237
|
+
`manual_verified > provider_api > provider_web > pi_cli > third_party > unknown`.
|
|
238
|
+
Conflicts resolve by newer timestamp, then trust, then confidence; an equal-trust
|
|
239
|
+
disagreement on a major field (status/free/prices) is recorded as `conflict`, never
|
|
240
|
+
auto-resolved.
|
|
241
|
+
|
|
242
|
+
### Catalog proposal rules
|
|
243
|
+
|
|
244
|
+
`additions` (new model observed active), `updates` (field changes), `deprecations`
|
|
245
|
+
(status removed/deprecated — requires >=2 independent sources or one high-trust;
|
|
246
|
+
a listing absence alone is never removal), `conflicts`, `ignored`, `freshness`.
|
|
247
|
+
`applyProposalToCatalog` builds an in-memory hypothetical catalog for the preview.
|
|
248
|
+
|
|
249
|
+
### CLI
|
|
250
|
+
|
|
251
|
+
```powershell
|
|
252
|
+
node scripts/observe-models.mjs --source manual --input obs.json
|
|
253
|
+
node scripts/observe-models.mjs --source fixture --input announcements.json
|
|
254
|
+
node scripts/observe-models.mjs --check-pi
|
|
255
|
+
node scripts/observe-models.mjs --check-qoder # qoder-models-cache.json priceFactor -> free observations
|
|
256
|
+
node scripts/observe-models.mjs --check pi-cli,qoder-cache # generic observer list (same ids)
|
|
257
|
+
node scripts/observe-models.mjs --preview-maintenance
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
### Observer registry
|
|
261
|
+
|
|
262
|
+
Live probes are **observers** in `lib/observe/observers.mjs` — a common
|
|
263
|
+
`async run(context) -> { observations, probeFailed, metadata }` interface behind a
|
|
264
|
+
registry (`OBSERVERS`). `--check-pi`/`--check-qoder` map to `pi-cli`/`qoder-cache`;
|
|
265
|
+
`--check <csv>` accepts any ids. `resolveRequestedObservers` produces a stable,
|
|
266
|
+
deduped id list (unknown ids are an explicit error, never silently skipped);
|
|
267
|
+
`runObservers` executes each once, in order, isolating failures (one observer's
|
|
268
|
+
`probeFailed` never blocks the others; per-source results are audited on the run
|
|
269
|
+
record). Adding a provider = one registry entry — no CLI/job branching.
|
|
270
|
+
|
|
271
|
+
### Fixture / test boundary
|
|
272
|
+
|
|
273
|
+
Observations carry `environment: 'production' | 'test'` and `sourceFixture`. The
|
|
274
|
+
`fixture` source (`--source fixture`, `tests/fixtures/**` replays) tags its output
|
|
275
|
+
`test`. `productionObservations()` filters the store before `diffCatalog`/
|
|
276
|
+
`buildCatalogProposal` in both `observe-models.mjs` and `job.mjs`, so a fixture
|
|
277
|
+
replay is stored for audit but **never** drives a real catalog/routing proposal.
|
|
278
|
+
Anything without a marker counts as production (back-compat).
|
|
279
|
+
|
|
280
|
+
### Qoder provider-metadata observer (`--check-qoder`)
|
|
281
|
+
|
|
282
|
+
Reads `~/.pi/agent/qoder-models-cache.json` (schema v2: `{version, updatedAt,
|
|
283
|
+
models:[{id, priceFactor, ...}]}`) — the pi-maintained, provider-derived local cache —
|
|
284
|
+
**read-only**. `priceFactor` is the provider's subscription cost multiplier:
|
|
285
|
+
`0` = currently free, `>0` = paid. A missing file, unparseable JSON, absent entry,
|
|
286
|
+
or absent/invalid `priceFactor` yields *no observation* (unknown) — never a guessed
|
|
287
|
+
"not free". Trust tier `provider_local_cache`.
|
|
288
|
+
|
|
289
|
+
Transition detection compares against `out/qoder-observer-state.json` (gitignored
|
|
290
|
+
snapshot the job owns): `0 -> non-zero` emits `free:false` (campaign ended),
|
|
291
|
+
`non-zero -> 0` emits `free:true` (campaign started), same value emits nothing.
|
|
292
|
+
The emitted `free` field flows through the normal pipeline — catalog diff ->
|
|
293
|
+
proposal -> hypothetical catalog -> `free-campaign-ended|started` events ->
|
|
294
|
+
maintenance re-evaluation. A free end alone never forces a routing change;
|
|
295
|
+
`postCampaignCost: null` becomes `costUnknown` (not $0, not free) and is surfaced
|
|
296
|
+
in the proposal for human review. `--qoder-cache <path>` overrides the cache
|
|
297
|
+
location (tests/fixtures).
|
|
298
|
+
|
|
299
|
+
## Phase 5 — scheduled job + notifications (`scripts/model-maintenance-job.mjs`)
|
|
300
|
+
|
|
301
|
+
`model-maintenance-job.mjs` wraps observe -> dedupe -> diff -> meaningful-change gate
|
|
302
|
+
-> proposal -> preview -> optional `--live` tiers -> notification. Quiet by default:
|
|
303
|
+
only human-worthy changes notify. Exit code `0` quiet, `2` meaningful change pending
|
|
304
|
+
review, `1` error.
|
|
305
|
+
|
|
306
|
+
### Meaningful change & severity (deterministic, never LLM)
|
|
307
|
+
|
|
308
|
+
Quiet when: no observations / duplicates-only / stale-only / catalog unchanged /
|
|
309
|
+
no routing impact / probe-failed-only / unknown-only. Otherwise:
|
|
310
|
+
|
|
311
|
+
- **info** — new model, context/capability additions, no routing impact
|
|
312
|
+
- **action** — free-model end, cheapest-sufficient shift, preview binding change,
|
|
313
|
+
all-tier-candidates-failed
|
|
314
|
+
- **urgent** — current model removed/deprecated/unavailable, fallback to local,
|
|
315
|
+
no eligible candidate for an *unusable* backend, multi-capability impact
|
|
316
|
+
|
|
317
|
+
### Notification
|
|
318
|
+
|
|
319
|
+
Canonical payload (`severity,title,summary,changes,routingImpact,recommendedNextStep,
|
|
320
|
+
requiresApproval:true,proposalPath,previewPath,runId,observedAt`). Sinks: stdout,
|
|
321
|
+
`out/model-maintenance.notification.json`, and an opt-in `--notify-command` that
|
|
322
|
+
receives the JSON on stdin (audited; only runs when explicitly given). Dedupe:
|
|
323
|
+
`out/model-maintenance.state.json` stores `lastNotificationHash`/`lastSeverity`;
|
|
324
|
+
identical content is not resent, a severity rise re-notifies.
|
|
325
|
+
|
|
326
|
+
### Concurrency & budget
|
|
327
|
+
|
|
328
|
+
Single-run lock (`out/model-maintenance.lock`, stale after 30 min, auto-recovered),
|
|
329
|
+
atomic artifact writes (tmp+rename), `runId`/`startedAt`/`completedAt` in
|
|
330
|
+
`out/model-maintenance.lastrun.json`. Budget (`maintenance-policy.json` `budget`):
|
|
331
|
+
`maxEstimatedCostPerRunUsd`, `maxPremiumInvocationsPerRun`, `maxTotalInvocationsPerRun`
|
|
332
|
+
— over-budget skips invocations, continues deterministically, sets `budgetLimited`.
|
|
333
|
+
|
|
334
|
+
## Phase 6 — shadow mode + calibration (`scripts/report-model-maintenance.mjs`)
|
|
335
|
+
|
|
336
|
+
`--shadow` runs the normal pipeline but suppresses the external `--notify-command`
|
|
337
|
+
(stdout + file sinks still fire; `--shadow --shadow-notify` opts the command back in).
|
|
338
|
+
Every run appends a compact record to `out/telemetry-runs.jsonl` and folds into
|
|
339
|
+
`out/telemetry.json` via `lib/telemetry.mjs`.
|
|
340
|
+
|
|
341
|
+
### Telemetry
|
|
342
|
+
|
|
343
|
+
Aggregates: run/quiet/meaningful/notification/dedupe counts, severity distribution,
|
|
344
|
+
per-tier invocations, fallbacks (incl. local), premium invocations, budget-limited,
|
|
345
|
+
degraded-to-deterministic, observer failures, conflicts, duplicate/stale
|
|
346
|
+
observations, and cost totals (api / local-electricity / total, per-run, per
|
|
347
|
+
meaningful event, per notification). Per-decision records keep selected model,
|
|
348
|
+
tier, estimatedCostPerRun, quality vs requiredQuality margin, alternatives and
|
|
349
|
+
rejection reasons — no prompts or credentials.
|
|
350
|
+
|
|
351
|
+
### Report
|
|
352
|
+
|
|
353
|
+
`node scripts/report-model-maintenance.mjs [--days N] [--json] [--compact]` prints
|
|
354
|
+
Activity / Cost / Routing / Quality / Noise / Escalation sections, deterministic
|
|
355
|
+
warnings (too-noisy, too-expensive, too-many-premium-escalations,
|
|
356
|
+
too-many-fallbacks, quality-margin-too-small), counterfactual comparisons of the
|
|
357
|
+
selected model vs the runners-up it beat, and a policy calibration proposal.
|
|
358
|
+
|
|
359
|
+
### Calibration proposal + guards
|
|
360
|
+
|
|
361
|
+
`out/maintenance-policy.calibration.proposal.json` — each entry has
|
|
362
|
+
currentValue/proposedValue/evidence/expectedEffect/confidence. Emitted only when
|
|
363
|
+
`calibration.minRuns` (20) and `calibration.minMeaningfulEvents` (3) are met;
|
|
364
|
+
otherwise `insufficient-observation-data`. Never applied automatically.
|
|
365
|
+
|
|
366
|
+
### Retention
|
|
367
|
+
|
|
368
|
+
`--compact --days 30` folds raw run lines older than the window into a
|
|
369
|
+
`_compactedSummary` record; recent lines are kept, corrupt lines are never dropped,
|
|
370
|
+
and the rewrite is atomic (tmp+rename) so a failure preserves the original file.
|
|
371
|
+
|
|
372
|
+
## Scheduling
|
|
373
|
+
|
|
374
|
+
Run it when a provider announcement lands, or periodically:
|
|
375
|
+
|
|
376
|
+
Recommended cadence: every 6-12 hours (price/model conditions don't move per-minute;
|
|
377
|
+
avoid over-polling). One run at startup is reasonable. Overlap is prevented by the
|
|
378
|
+
lock file.
|
|
379
|
+
|
|
380
|
+
```powershell
|
|
381
|
+
node scripts/model-maintenance-job.mjs --check-pi --check-qoder --shadow # recommended: availability + Qoder priceFactor, observe-only
|
|
382
|
+
# tests/fixtures/** replays write test-tagged observations; they never reach the production diff
|
|
383
|
+
node scripts/model-maintenance-job.mjs --check-pi # availability only
|
|
384
|
+
node scripts/model-maintenance-job.mjs --check-qoder # Qoder priceFactor only
|
|
385
|
+
node scripts/model-maintenance-job.mjs --check-pi --live # + real tier invocations when needed
|
|
386
|
+
node scripts/model-maintenance-job.mjs --notify-command "node send.js" # explicit external sink
|
|
387
|
+
# Windows Task Scheduler example (NOT registered by the kit):
|
|
388
|
+
# schtasks /create /tn ludi-model-maintenance /sc hourly /mo 6 ^
|
|
389
|
+
# /tr "node <kit>\scripts\model-maintenance-job.mjs --check-pi"
|
|
390
|
+
# exit code 2 = a notification is waiting for human review.
|
|
391
|
+
|
|
392
|
+
# Recommended initial rollout (Phase 6): shadow mode, no external notify,
|
|
393
|
+
# review the calibration report after 7 days.
|
|
394
|
+
# schtasks /create /tn ludi-model-maintenance /sc hourly /mo 6 ^
|
|
395
|
+
# /tr "node <kit>\scripts\model-maintenance-job.mjs --check-pi --shadow"
|
|
396
|
+
# node <kit>\scripts\report-model-maintenance.mjs --days 7
|
|
397
|
+
```
|
|
398
|
+
|
|
399
|
+
The output file is gitignored (`adapters/pi/out/`); copy `changes[]` into an issue or
|
|
400
|
+
notes if you want history. Fixture for the Qoder/Qwen3.8-Flash free-campaign-end
|
|
401
|
+
scenario: `tests/fixtures/maintenance/`, exercised by `tests/maintenance.test.mjs`.
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
# Orchestrator (Phase 1)
|
|
2
|
+
|
|
3
|
+
One high-level request in, one integrated report out. The orchestrator plans, routes,
|
|
4
|
+
delegates, evaluates and decides; it never implements. Its main job is to cut down how
|
|
5
|
+
often the user gets asked something.
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
node scripts/orchestrate.mjs --dry-run "DOLL v2 Phase 2を進める" # plan + routing only
|
|
9
|
+
node scripts/orchestrate.mjs --repo <dir> "Fix the failing test" # run (agents read-only)
|
|
10
|
+
node scripts/orchestrate.mjs --repo <dir> --apply "..." # coder tasks via scout->coder pipeline (writes)
|
|
11
|
+
[--planner rules|model] [--policy <file>] [--out <dir>] [--json] [--trace]
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## Response language
|
|
15
|
+
|
|
16
|
+
All agents default to **Japanese** for user-facing output (summaries, reports, decisions,
|
|
17
|
+
status messages). The policy lives once in `lib/language-policy.mjs` and is appended to
|
|
18
|
+
every agent's system prompt by `loadAgents` — never duplicated into `agents/*.md`, never
|
|
19
|
+
per-provider. Schema keys/enums (`status`, `completed`, Context Pack `##` headings, JSON
|
|
20
|
+
field names) stay in English; only natural-language values are Japanese. An explicit user
|
|
21
|
+
language request ("英語で", "in English") overrides the default. `formatReport` already
|
|
22
|
+
renders 状態/完了/自動判断/未解決/ユーザー判断が必要 in Japanese.
|
|
23
|
+
|
|
24
|
+
## Modules (`lib/orchestrator/`)
|
|
25
|
+
|
|
26
|
+
| Module | Responsibility |
|
|
27
|
+
| --- | --- |
|
|
28
|
+
| `planner.mjs` | request -> task specs. `rules` (deterministic keyword template, default) or `model` (orchestrator agent returns JSON; invalid output falls back to rules). `validatePlan` checks ids, deps, cycles, agents, capabilities, `max_tasks`. |
|
|
29
|
+
| `task-store.mjs` | `createMemoryTaskStore` (Phase 1) plus `runnableTasks`, `strandedTasks`, `findCycle`. The persistent adapter is `store.mjs`. |
|
|
30
|
+
| `store.mjs` | SQLite run, task, decision, decision-memory, backend-health and trace store. |
|
|
31
|
+
| `health.mjs` | Classifies usage-limit and rate-limit failures and skips a backend until its TTL. |
|
|
32
|
+
| `router.mjs` | task -> agent -> capability through `agents/*.md` + `routing.json`; models via `lib/resolve.mjs`. Retries escalate along `routing.escalation.ladders`. |
|
|
33
|
+
| `runner.mjs` | AgentRunner adapter: `withEscalation` + adapter `invoke` (pi CLI), or `runPipeline` for `--apply`. Appends the structured-result contract; agent files are unchanged. |
|
|
34
|
+
| `evaluator.mjs` | Success only with status `completed`, a summary, required outputs present and every acceptance criterion `A<n>` met with evidence. |
|
|
35
|
+
| `escalation.mjs` | Escalation gate (below). |
|
|
36
|
+
| `policy.mjs` | Defaults <- `orchestration/decision-policy.json` <- `decision-policy.local.json` / `--policy`. |
|
|
37
|
+
| `orchestrator.mjs` | The loop, `dryRun`, `formatPlan`, `formatReport`. |
|
|
38
|
+
| `api.mjs` | Start, resume, list, show and answer. CLI and the pi extension both call this. |
|
|
39
|
+
|
|
40
|
+
## Loop
|
|
41
|
+
|
|
42
|
+
Each round: take runnable tasks (pending, all dependencies completed), run up to
|
|
43
|
+
`max_parallel_tasks` of them, then evaluate each result.
|
|
44
|
+
- **success**: the task completes, and any `newTasks` it reports are added (bounded by `max_tasks`).
|
|
45
|
+
If a review reports blocking issues, the work is sent back to the implementer and a
|
|
46
|
+
re-review is added (`max_rework_cycles`).
|
|
47
|
+
- **failure**: the retry strategy depends on the failure class (`failures.mjs`):
|
|
48
|
+
- **Protocol-quality failures** (`MALFORMED_RESULT`, `EMPTY_RESPONSE`, and turn-limit
|
|
49
|
+
`TIMEOUT` with no tool progress) mean the model could not honour the contract — the
|
|
50
|
+
SAME attempt advances to the next routing candidate instead of retrying that model.
|
|
51
|
+
When the current capability has no untried candidate left, the task escalates to the
|
|
52
|
+
next capability on the ladder (e.g. cheap-code -> strong-code).
|
|
53
|
+
- **Recoverable failures** (`TEST_FAILURE`, `TOOL_FAILURE`, validation `UNKNOWN`, a
|
|
54
|
+
turn-limit `TIMEOUT` that DID make tool progress) retry the same model with feedback.
|
|
55
|
+
- Two-layer tried history. `task.attemptedModels` is the CAPABILITY-LOCAL tried
|
|
56
|
+
list (reset on escalation so the new capability's candidates re-resolve).
|
|
57
|
+
`task.taskGlobalFailedModels` is the TASK-GLOBAL list: a model that hit a
|
|
58
|
+
protocol-quality failure (malformed / empty / no-progress turn-limit) is skipped
|
|
59
|
+
for the WHOLE task even after escalation — so cheap-code's failed Qwen is never
|
|
60
|
+
re-invoked on strong-code. Recoverable failures (tool/test/validation) do NOT
|
|
61
|
+
mark a model task-global.
|
|
62
|
+
- Attempt budget: `model_attempts_per_task` caps candidates per attempt;
|
|
63
|
+
`max_total_attempts_per_task` caps total REAL invocations. Only an actual
|
|
64
|
+
model/provider invocation counts — health-TTL skips, unavailable candidates,
|
|
65
|
+
already-tried skips and enumeration do NOT consume budget. Each result carries
|
|
66
|
+
`counters = { candidatesConsidered, candidatesSkipped, invocationsStarted }` so
|
|
67
|
+
the budget display shows real invocations (e.g. `3/4`), not candidates seen.
|
|
68
|
+
Once retries (or the model-attempt budget) run out the task is marked failed.
|
|
69
|
+
|
|
70
|
+
### Subagent execution budget
|
|
71
|
+
|
|
72
|
+
Turn limits are per-role and per-complexity, not a single global value
|
|
73
|
+
(`agent_runtime.turn_budgets`). `classifyTaskComplexity` rates a task
|
|
74
|
+
simple/normal/heavy/repo-history-heavy from its goal; `initialTurnBudget` resolves
|
|
75
|
+
the starting turn cap. When a subagent reaches that cap, a deterministic
|
|
76
|
+
`progressScore` (tool calls + unique files + commands − repeats) decides whether
|
|
77
|
+
to grant a bounded extension (`extension_turns` × `max_extensions`, capped by
|
|
78
|
+
`absolute_max_turns`). A no-progress agent stops immediately as
|
|
79
|
+
`NO_PROGRESS_TIMEOUT` (task-global model failure); a progressing agent that still
|
|
80
|
+
exceeds the absolute cap is `PROGRESS_TIMEOUT` — recoverable, NOT a model-quality
|
|
81
|
+
failure, so it is not added to `taskGlobalFailedModels`. Extension happens inside
|
|
82
|
+
one model invocation and never consumes `max_total_attempts_per_task`.
|
|
83
|
+
|
|
84
|
+
The planner also splits multi-concern investigations (e.g. commit-history +
|
|
85
|
+
UI-implementation + synthesis) into focused sub-tasks so no single scout is
|
|
86
|
+
overloaded — task sizing comes before turn budget.
|
|
87
|
+
- **blocked**: the agent needs a decision, so each decision goes through the escalation gate.
|
|
88
|
+
|
|
89
|
+
Protocol reliability is audited per provider/model in the `protocol_stats` table
|
|
90
|
+
(malformed / empty / turn_limit / structured_ok counts). This is telemetry only — it is
|
|
91
|
+
NOT fed back into routing scores.
|
|
92
|
+
|
|
93
|
+
The loop stops when no task is runnable or `max_rounds` is reached. Tasks whose
|
|
94
|
+
dependency failed or is blocked become `blocked`. The full event log is kept in
|
|
95
|
+
`result.trace`, and the CLI writes it to `<out>/orchestration-trace.json`.
|
|
96
|
+
|
|
97
|
+
## Escalation gate
|
|
98
|
+
|
|
99
|
+
1. **Hard gate.** A decision flagged `external_publish`, `destructive_action`, `production_risk`,
|
|
100
|
+
`high_cost`, `project_cancellation`, `major_direction_change` or `user_value_judgement` goes to
|
|
101
|
+
the user when that flag is enabled in policy. Flags come from the agent, from numbers
|
|
102
|
+
(`costUsd >= high_cost_threshold_usd`, `estimatedWeeks >= major_direction_change_weeks`), or
|
|
103
|
+
from policy keywords. This step is first, so a remembered answer cannot authorize it.
|
|
104
|
+
2. **Current run.** Reuse a decision already made in this run with the same key.
|
|
105
|
+
3. **Persistent decision memory.** Reuse an explicit user answer (or another saved rule) from an
|
|
106
|
+
earlier run when it is still in scope and not expired. Automatic choices are not saved.
|
|
107
|
+
4. **Single option.** Take the only viable option.
|
|
108
|
+
5. **Reversible.** Choose the best-scoring option (`reversible_decision: auto`).
|
|
109
|
+
6. **Low risk / low cost.** Choose one (`low_risk_decision: auto`).
|
|
110
|
+
7. **Project policy.** Prefer existing assets, maintainability and reversibility, but only
|
|
111
|
+
when this gives a unique winner.
|
|
112
|
+
8. **Small experiment.** If the estimate is within `poc.prefer_if_estimated_hours_lte` hours,
|
|
113
|
+
add an experiment task and resume the blocked task afterwards. Each decision gets at most one experiment.
|
|
114
|
+
9. **Otherwise** escalate. With a store, the run becomes `waiting_for_user` and the decision is
|
|
115
|
+
kept until `orchestrate.mjs --resume <run> --answer <decision> "<text>"`.
|
|
116
|
+
|
|
117
|
+
## Persistent runs (Phase 2)
|
|
118
|
+
|
|
119
|
+
State lives in SQLite (`node:sqlite`), default `.orchestration/state.db`, override with `--store`
|
|
120
|
+
or `LUDI_ORCHESTRATION_STORE`. SQL stays in `lib/orchestrator/store.mjs`. `createMemoryTaskStore`
|
|
121
|
+
is the Phase 1 store; `openStore().openTaskStore(runId)` is the persistent one. The in-memory
|
|
122
|
+
loop is unchanged when no session is passed.
|
|
123
|
+
|
|
124
|
+
```
|
|
125
|
+
node scripts/orchestrate.mjs --list
|
|
126
|
+
node scripts/orchestrate.mjs --decisions
|
|
127
|
+
node scripts/orchestrate.mjs --show <run-id>
|
|
128
|
+
node scripts/orchestrate.mjs --resume <run-id> --answer <decision-id> "use the existing approach"
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
Resume does not rerun `completed` tasks, does not reset attempts, rework cycles, rounds or the
|
|
132
|
+
task cap, and uses the policy snapshot stored with the run. A task left `running` by a crash is
|
|
133
|
+
put back to `pending`. A backend that returns `usage limit has been reached` is skipped for the
|
|
134
|
+
rest of the run and, until `backend_health.usage_exhausted_ttl_hours`, for later runs. Rate limits
|
|
135
|
+
stay on the run that saw them. There is no permanent blacklist.
|
|
136
|
+
|
|
137
|
+
pi loads `adapters/pi/orchestrator-ext` as `extensions/ludi-orchestrator` (`/orchestrate`, tool
|
|
138
|
+
`ludi_orchestrate`). The programmatic boundary is `lib/orchestrator/api.mjs`.
|
|
139
|
+
|
|
140
|
+
## Tool-capable children (Phase 3)
|
|
141
|
+
|
|
142
|
+
`execution: subagent` runs `pi --mode json -p` with `--tools` limited by `access` (filesystem, shell, git, network). Shell is `ludi_exec` in `adapters/pi/shell-gate`, which refuses push, publish, deploy, reset, and recursive delete before the process runs. `--apply` still uses the old pipeline. With no subagent launcher, `subagent` falls back to the one-shot invoker. A child id, tool count, and turn count are stored on the run trace. Runtime, tool-call, and turn caps are `agent_runtime` in the decision policy. Only `MODEL_FAILURE` and `BACKEND_LIMIT` move up the routing ladder; a failing test retries the same capability with the previous attempt in the contract.
|
|
143
|
+
|
|
144
|
+
Dry-run prints mode, workspace, tools, model, dependencies, and acceptance, and does not start pi.
|
|
145
|
+
|
|
146
|
+
## Phase 4 gaps (Asana -> persistent orchestrator -> tool-capable agents -> Asana)
|
|
147
|
+
|
|
148
|
+
- An `ExternalProjectStore` implementation for Asana that maps tasks, sections and comments, is
|
|
149
|
+
idempotent, and runs dry-run first. The run store stays separate from that project store.
|
|
150
|
+
- An intake adapter that turns an Asana task into a request plus acceptance criteria.
|
|
151
|
+
- A direct pi-subagents launcher. The current tool-capable child path uses `pi --mode json -p`;
|
|
152
|
+
`createPiInvoker` remains a synchronous fallback for one-shot and model-planning calls.
|
|
153
|
+
- Parallel execution for one-shot invocations. Tool-capable children use asynchronous `spawn`,
|
|
154
|
+
while the one-shot invoker still uses `spawnSync`.
|
|
155
|
+
- A reviewer severity schema and an acceptance rubric shared by all agents.
|