pi-crew 0.9.65 → 0.9.67
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +79 -0
- package/README.md +2 -3
- package/agents/executor.md +1 -1
- package/agents/test-engineer.md +1 -1
- package/agents/verifier.md +1 -1
- package/dist/index.mjs +4526 -4100
- package/package.json +7 -4
- package/skills/real-test-pi-crew/SKILL.md +10 -9
- package/src/config/config.ts +19 -3
- package/src/config/role-tools.ts +2 -2
- package/src/config/types.ts +2 -0
- package/src/extension/knowledge-injection.ts +17 -0
- package/src/extension/pi-api.ts +0 -16
- package/src/extension/team-tool/api/agent-control.ts +358 -0
- package/src/extension/team-tool/api/handler-context.ts +57 -0
- package/src/extension/team-tool/api/heartbeat.ts +75 -0
- package/src/extension/team-tool/api/mailbox.ts +242 -0
- package/src/extension/team-tool/api/plan-approval.ts +190 -0
- package/src/extension/team-tool/api/read.ts +443 -0
- package/src/extension/team-tool/api/task-claims.ts +207 -0
- package/src/extension/team-tool/api.ts +56 -1301
- package/src/extension/team-tool/cancel.ts +49 -4
- package/src/extension/team-tool/dispatch/manage.ts +7 -4
- package/src/extension/team-tool/explain.ts +3 -1
- package/src/extension/team-tool/goal-wrap.ts +2 -2
- package/src/extension/team-tool/goal.ts +4 -4
- package/src/extension/team-tool/lifecycle-actions.ts +9 -6
- package/src/extension/team-tool/parallel-dispatch.ts +2 -2
- package/src/extension/team-tool/respond.ts +11 -3
- package/src/extension/team-tool/run-intent.ts +357 -0
- package/src/extension/team-tool/run.ts +8 -285
- package/src/extension/team-tool/status.ts +56 -21
- package/src/extension/team-tool-types.ts +2 -0
- package/src/extension/team-tool.ts +10 -8
- package/src/prompt/scratchpad-lifecycle.ts +80 -5
- package/src/runtime/child-pi/child-pi-spawn.ts +13 -7
- package/src/runtime/child-pi/child-pi.ts +22 -144
- package/src/runtime/child-pi/mock-fixtures.ts +171 -0
- package/src/runtime/crew-agent-records.ts +18 -1
- package/src/runtime/output/output-validator.ts +34 -6
- package/src/runtime/scheduling/scheduler.ts +67 -19
- package/src/runtime/scratchpad/README.md +6 -0
- package/src/runtime/scratchpad/guest.ts +54 -5
- package/src/runtime/scratchpad/snapshot-hmac.ts +7 -1
- package/src/runtime/supervisor-contact.ts +0 -16
- package/src/runtime/task-runner/child-executor.ts +1 -0
- package/src/runtime/task-runner/state-helpers.ts +9 -1
- package/src/runtime/team-runner.ts +39 -2
- package/src/state/contracts.ts +3 -0
- package/src/state/event-log/event-log.ts +19 -27
- package/src/state/gitignore-manager.ts +61 -7
- package/src/utils/glob-match.ts +29 -0
- package/types/dwf.d.ts +1 -1
- package/src/types/new-api-types.ts +0 -35
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-crew",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.67",
|
|
4
4
|
"description": "Pi extension for coordinated AI teams, workflows, worktrees, and async task orchestration",
|
|
5
5
|
"author": "baphuongna",
|
|
6
6
|
"license": "MIT",
|
|
@@ -67,22 +67,25 @@
|
|
|
67
67
|
],
|
|
68
68
|
"scripts": {
|
|
69
69
|
"check": "npm run ci",
|
|
70
|
-
"ci": "npm run typecheck && npm run lint && npm run format:check && npm run check:conflict-markers && npm run check:decision-drift && npm run check:lazy-imports && npm run check:bundle-staleness && npm run build:bundle && npm run check:bundle-size && npm run test:bundle && npm test && npm pack --dry-run",
|
|
70
|
+
"ci": "npm run typecheck && npm run lint && npm run format:check && npm run check:conflict-markers && npm run check:decision-drift && npm run check:event-types && npm run check:lazy-imports && npm run check:bundle-staleness && npm run build:bundle && npm run check:bundle-size && npm run test:bundle && npm test && npm pack --dry-run",
|
|
71
71
|
"check:lazy-imports": "node scripts/check-lazy-imports.mjs",
|
|
72
72
|
"check:bundle-staleness": "node scripts/check-bundle-staleness.mjs",
|
|
73
73
|
"check:bundle-size": "node scripts/check-bundle-size.mjs",
|
|
74
74
|
"check:conflict-markers": "node scripts/check-conflict-markers.mjs",
|
|
75
75
|
"check:decision-drift": "node scripts/check-decision-drift.mjs",
|
|
76
|
+
"check:event-types": "node scripts/check-event-types-registry.mjs",
|
|
76
77
|
"typecheck": "tsc --noEmit && node --experimental-strip-types -e \"await import('./index.ts'); console.log('strip-types import ok')\"",
|
|
77
78
|
"lint": "biome check --linter-enabled=true --formatter-enabled=false --max-diagnostics=50 .",
|
|
78
79
|
"format:check": "biome format .",
|
|
79
80
|
"test": "npm run test:unit && npm run test:integration",
|
|
80
81
|
"test:unit": "node scripts/test-runner.mjs --test-concurrency=4 --test-timeout=180000 --test-force-exit 'test/unit/**/*.test.ts'",
|
|
81
82
|
"test:critical": "node scripts/test-runner.mjs --test-concurrency=4 --test-timeout=30000 --test-force-exit test/unit/runtime/broker/crew-broker-handshake.test.ts test/unit/runtime/broker/crew-broker-stale-socket.test.ts test/unit/runtime/broker/crew-broker-feature-flag.test.ts test/unit/runtime/broker/crew-broker-server-gate.test.ts test/unit/runtime/broker/crew-broker-client-fallback.test.ts test/unit/runtime/broker/crew-broker-mailbox-observer.test.ts test/unit/runtime/broker/crew-broker-close-during-reconnect.test.ts test/unit/runtime/broker/crew-broker-steer-dedup.test.ts test/unit/runtime/broker/crew-broker-symlink-steering.test.ts test/unit/ui/keybinding-map.parity.test.ts test/unit/ui/pi-tui-dispatch-probe.test.ts test/unit/utils/session-utils-extract.test.ts test/unit/config/config-schema-sync.test.ts test/unit/runtime/child-pi/child-pi-env-spread.test.ts",
|
|
82
|
-
"test:watch": "
|
|
83
|
+
"test:watch": "node scripts/test-runner.mjs --watch --test-concurrency=4 --test-timeout=30000 'test/unit/**/*.test.ts'",
|
|
83
84
|
"test:integration": "node scripts/test-runner.mjs --test-concurrency=1 --test-timeout=300000 test/integration/*.test.ts",
|
|
84
85
|
"test:smoke": "node scripts/test-runner.mjs --test-concurrency=1 --test-timeout=180000 test/smoke/*.smoke.ts",
|
|
85
86
|
"test:spike": "node scripts/test-runner.mjs --test-concurrency=2 --test-timeout=120000 --test-force-exit test/runtime/scratchpad/*.test.ts",
|
|
87
|
+
"test:functional": "node scripts/test-runner.mjs --test-concurrency=1 --test-timeout=120000 test/functional/pi-crew-functional.test.ts",
|
|
88
|
+
"test:platform": "node scripts/test-runner.mjs --test-concurrency=1 --test-timeout=60000 test/platform/*.test.ts",
|
|
86
89
|
"build:bundle": "node scripts/build-bundle.mjs",
|
|
87
90
|
"test:bundle": "node --experimental-strip-types --test --test-force-exit test/unit/bundle-load.test.ts",
|
|
88
91
|
"watch:bundle": "node scripts/watch-bundle.mjs",
|
|
@@ -93,7 +96,7 @@
|
|
|
93
96
|
"build:crew-font": "python scripts/build-crew-vibes-font.py",
|
|
94
97
|
"bench": "node scripts/run-bench.mjs",
|
|
95
98
|
"bench:check": "node scripts/bench-check.mjs",
|
|
96
|
-
"test:new": "
|
|
99
|
+
"test:new": "node scripts/test-runner.mjs --test-concurrency=4 --test-timeout=30000 'test/unit/**/*.test.ts'",
|
|
97
100
|
"test:changed": "node scripts/test-changed.mjs",
|
|
98
101
|
"bench:capture": "node scripts/run-bench.mjs && node -e \"require('node:fs').copyFileSync('test/bench/results.json','test/bench/baseline.json')\"",
|
|
99
102
|
"profile:startup": "node scripts/profile-startup.mjs",
|
|
@@ -110,7 +110,7 @@ To add Tier 1 to CI as a fast-feedback gate (under 30s):
|
|
|
110
110
|
|
|
111
111
|
---
|
|
112
112
|
|
|
113
|
-
## Tier 1 — Critical unit tests (~
|
|
113
|
+
## Tier 1 — Critical unit tests (~25s, 101 tests, the only suite you need for broker/UI changes)
|
|
114
114
|
|
|
115
115
|
**What**: run the curated 14-file fast subset.
|
|
116
116
|
|
|
@@ -122,7 +122,7 @@ To add Tier 1 to CI as a fast-feedback gate (under 30s):
|
|
|
122
122
|
time npm run test:critical
|
|
123
123
|
```
|
|
124
124
|
|
|
125
|
-
Expected output: `# tests
|
|
125
|
+
Expected output: `# tests 101 # pass 101 # fail 0 # duration_ms ~26000`. (Count was 97 at v0.9.46; 101 since the model-routing merge — verify with the actual run; the skill's hard-coded numbers drift between releases.)
|
|
126
126
|
|
|
127
127
|
**References**:
|
|
128
128
|
|
|
@@ -162,7 +162,7 @@ PI_CREW_BROKER=0 npm run test:critical
|
|
|
162
162
|
PI_CREW_BROKER=1 npm run test:critical
|
|
163
163
|
```
|
|
164
164
|
|
|
165
|
-
All three must show `# pass
|
|
165
|
+
All three must show `# pass 101 # fail 0`. Measured times in this session (2026-08-11): ~26s for default, ~26s for `PI_CREW_BROKER=0`, ~26s for `PI_CREW_BROKER=1` (varies ±1-2s run-to-run).
|
|
166
166
|
|
|
167
167
|
**References**:
|
|
168
168
|
|
|
@@ -202,7 +202,7 @@ Compare the printed md5 against what the user's Pi session loaded. If they diffe
|
|
|
202
202
|
| Bundle builder | `scripts/build-bundle.mjs` (esbuild-based, bundles `index.bundle.ts` → `dist/index.mjs`) |
|
|
203
203
|
| Bundle resolution rule | `index.ts:5-22` (entrypoint docstring); also `scripts/build-bundle.mjs:14-20` (entrypoint preference); **symlink is live for source files but the bundled `dist/index.mjs` is loaded** |
|
|
204
204
|
| Postinstall hook | `scripts/postinstall.mjs:43` — best-effort bundle rebuild; falls back to strip-types if esbuild missing |
|
|
205
|
-
| Bundle md5 after Phase-4 commit | `1cc4d55e18add7b9a036c569143320b6` (~2.78 MB at the time;
|
|
205
|
+
| Bundle md5 after Phase-4 commit | `1cc4d55e18add7b9a036c569143320b6` (~2.78 MB at the time; **check current**: `md5sum dist/index.mjs`. As of v0.9.66 I-batch 2026-08-11: `16e29d053bd370e24f40df147dadcb79` ~2.81 MB) |
|
|
206
206
|
|
|
207
207
|
---
|
|
208
208
|
|
|
@@ -454,7 +454,7 @@ If the two md5s match → session is on the latest code. If not → user must `/
|
|
|
454
454
|
|
|
455
455
|
**Real measured outcome** (this session, after the v0.9.57 schema fix): 9a (15 team actions) + 9b (4 subagent tools / 3 run paths) exercised; all green; the two silent-failure modes that motivated this tier (`Unknown type` from `Type.Unsafe` without Kind, and `Validation failed for tool team` from empty-string-strict schema) were caught ONLY by this battery — Tier 1-8 all passed while the team tool was broken live. The session also surfaced the unauthorized-agent-edit anti-pattern (a chain-run agent edited `chain-runner.ts` mid-smoke) — see Anti-patterns.
|
|
456
456
|
|
|
457
|
-
**Not covered by the cheap battery above** — the actions below need extra setup, cost, or user confirmation. Run them only when the change touches their code path, and prefer a throwaway cwd / config so you don't mutate the user's real state. Organised by cost/safety
|
|
457
|
+
**Not covered by the cheap battery above** — the actions below need extra setup, cost, or user confirmation. Run them only when the change touches their code path, and prefer a throwaway cwd / config so you don't mutate the user's real state. Organised by cost/safety. **As of 2026-08-11 (extended battery, run report `real-test-2026-08-11-scratchpad-I-batch.md`), 9c/9e/9f have been exercised live once each — they are no longer unproven, but still require explicit scope+confirmation to re-run.**
|
|
458
458
|
|
|
459
459
|
**9c. Lifecycle / recovery** (needs a *running* run — start an async run, then exercise these against its runId):
|
|
460
460
|
- `team action='wait' runId='...'` — block until completion
|
|
@@ -508,9 +508,10 @@ If the two md5s match → session is on the latest code. If not → user must `/
|
|
|
508
508
|
| `makeFakeCtx({ flagOn: false })` without `brokerEnv: "0"` | `makeFakeCtx` deletes `PI_CREW_BROKER` env if `brokerEnv` is undefined | `612e18b` (test fix) | `test/unit/crew-broker-server-gate.test.ts:78` — pass `brokerEnv: "0"` to preserve env |
|
|
509
509
|
| Trust green CI on one OS | macOS/Windows regressions slip through | n/a (permanent) | `.crew/knowledge.md` — "CI runs 3 OSes ... A flake on one OS IS a real bug" |
|
|
510
510
|
| Trusting a team-run agent not to edit the repo under test | Agents spawned by `team`/`Agent`/`crew_agent` inherit the session cwd and have `edit`/`write` tools — a proactive LLM (observed with deepseek) will make **unauthorized source edits** to pi-crew during a trivial smoke run (e.g. "improving" `chain-runner.ts` while parsing a chain string). The edit can be correct + green-tested yet still be unintended scope creep that silently lands in your commit. | n/a (permanent) | After EVERY team/subagent run: `git status` and verify each changed file was authored by you. Diff + review any surprise change before staging. Consider `workspaceMode: 'worktree'` for parallel/risky runs to isolate mutations. |
|
|
511
|
+
| **Armed-role tool-surface bug (found live 2026-08-11)**: an opt-in tool (e.g. `scratchpad`) is armed via `ROLE_TOOL_CONFIGS[role].scratchpad=true` AND env `PI_CREW_SCRATCHPAD=1`, but NEVER appears in the worker surface. Root cause: `resolveToolPolicy` (`src/agents/agent-config.ts:165`) falls back `roleConfig.tools ?? agent.tools` when the role has no `tools` allowlist, and the builtin `agents/{executor,verifier,test-engineer}.md` frontmatter `tools:` did not list `scratchpad` → pi got `--tools read,grep,find,ls,bash,edit,write` and **hard-filtered** scratchpad. Env was correct; the tool was silently dropped by the `--tools` allowlist. Reproduce: `pi -p --tools read,grep,find,ls,bash,edit,write "list tools"` → no scratchpad; with scratchpad added → present. | `f753be30` | **Fix**: keep armed-role `agents/*.md` frontmatter `tools:` lists in sync with `ROLE_TOOL_CONFIGS` (QW17 pins the pinned roles; add the new tool to BOTH frontmatter AND role config for pinned roles, frontmatter-only for vacuous roles). A smoke run that claims the tool is "not available" in the worker is a REAL signal — verify the worker's actual `--tools` allowlist, not just env vars. |
|
|
511
512
|
| `Type.Unsafe({ anyOf/type })` schema field **without** `[TypeBox.Kind]` symbol | `Value.Check` throws `Unknown type` the first time a model emits that field (e.g. `skill`, `config`) — every team action returns `isError:true` text `"Unknown type"`. Tier 1-8 stay green because unit tests never send the offending field. | v0.9.57 | `src/schema/team-tool-schema.ts` — `SkillOverride`/`FreeformConfig` switched from `Type.Unsafe` to TypeBox-native `Type.Union`/`Type.Record`. See Tier 9. |
|
|
512
513
|
| Schema too strict for model-emitted empty strings (`runId:""`, `workspaceMode:""`, `budgetTotal:0`) | pi-ai `validateToolArguments` runs BEFORE the pi-crew handler and rejects `""` against Literal unions / patterns → `Validation failed for tool team` → model loops. | v0.9.57 | `src/schema/team-tool-schema.ts` — added `Literal("")` to unions, `^$|` pattern for runId, `""` to action enum, `0`/Boolean allowances. Handler-side `normalizeTeamParams` drops the empties. |
|
|
513
|
-
| Claiming "all 9 tiers pass" while 9c–9f were never run | Overclaim — once reported "9 tiers pass" when only 9a (8/10) + 9b (4/5) had actually run; 9c–9f were skipped. Past runs then become unverifiable ("did it really pass 9 tiers?"). | n/a (process) | Fill `REPORT-TEMPLATE.md` per-tier DURING the run. "Tier 9 pass" = 9a AND 9b AND the applicable 9c–9f, each with evidence. Round-up-to-pass is the anti-pattern this row exists to prevent. |
|
|
514
|
+
| Claiming "all 9 tiers pass" while 9c–9f were never run | Overclaim — once reported "9 tiers pass" when only 9a (8/10) + 9b (4/5) had actually run; 9c–9f were skipped. Past runs then become unverifiable ("did it really pass 9 tiers?"). **2026-08-11 repeat**: an initial report said "9c–9f skipped" yet the summary read as full coverage until the gap was called out. | n/a (process) | Fill `REPORT-TEMPLATE.md` per-tier DURING the run. "Tier 9 pass" = 9a AND 9b AND the applicable 9c–9f, each with evidence. Round-up-to-pass is the anti-pattern this row exists to prevent. If 9c–9f are skipped, SAY SO in the verdict and do not phrase it as "all pass". |
|
|
514
515
|
| chain run with `workflow:"chain"` forwarded to steps | Every chain step fails in ~58ms with an EMPTY error string — looks like a parse failure but isn't. `chain-dispatch` forwards `params.workflow` ("chain") into executor overrides; each step then runs the "chain" workflow via the normal `executeTeamRun` path and fails fast + silently. | Open (issue #44) | Omit `workflow` when invoking `action:'run' chain=...` — chain then runs 2/2 success (~308s). See `docs/bugs/chain-workflow-forward-quirk.md`. |
|
|
515
516
|
|
|
516
517
|
---
|
|
@@ -648,7 +649,7 @@ The skill does NOT need to be updated for every commit — only when the cited l
|
|
|
648
649
|
## Quick reference — exact commands
|
|
649
650
|
|
|
650
651
|
```bash
|
|
651
|
-
# Tier 1 (critical unit, ~
|
|
652
|
+
# Tier 1 (critical unit, ~25s, 101 tests)
|
|
652
653
|
npm run test:critical
|
|
653
654
|
# Tier 2 (3-path proof, broker changes only)
|
|
654
655
|
PI_CREW_BROKER=0 npm run test:critical
|
|
@@ -687,14 +688,14 @@ md5sum "$(npm root -g)"/pi-crew/dist/index.mjs 2>/dev/null \
|
|
|
687
688
|
|
|
688
689
|
Before claiming "tested":
|
|
689
690
|
|
|
690
|
-
- [ ] Tier 1: `test:critical` fresh-run, all pass (<25s). Count varies by release — was 97 at v0.9.46, 101
|
|
691
|
+
- [ ] Tier 1: `test:critical` fresh-run, all pass (<25s). Count varies by release — was 97 at v0.9.46, **101 since the model-routing merge (v0.9.66)**; record the actual count in the report.
|
|
691
692
|
- [ ] Tier 2: 3-path proof all pass — **required if you touched `src/config/defaults.ts` or `src/extension/registration/lifecycle-handlers.ts`**
|
|
692
693
|
- [ ] Tier 3: `npm run typecheck` exit 0, `npm run build:bundle` exit 0
|
|
693
694
|
- [ ] Tier 4: bundle md5 matches what the session loaded (or user has `/quit`-ed + reopened)
|
|
694
695
|
- [ ] Tier 5/6: live TUI smoke for any `src/ui/` change — keystroke reached `handleInput`
|
|
695
696
|
- [ ] Tier 7: smoke team run for any `src/runtime/plan-templates.ts` or `workflows/*.workflow.md` change — completed, no hang, verifier output under 60s
|
|
696
697
|
- [ ] Tier 8: final md5 sync check passed
|
|
697
|
-
- [ ] Tier 9: feature battery — **required if you touched `src/schema/team-tool-schema.ts`, `src/extension/registration/team-tool.ts`,
|
|
698
|
+
- [ ] Tier 9: feature battery — **required if you touched `src/schema/team-tool-schema.ts`, `src/extension/registration/team-tool.ts`, any `Type.Unsafe({...})` schema, or any armed-role tool list (`agents/*.md` / `src/config/role-tools.ts`)**. 9a read-only batch all return clean; one probe per 9b spawn path (sync / async / chain / `Agent` / `crew_agent`+`get_subagent_result`) completes with `consistency=1`. Run 9c–9f only when the change touches their code path; **at least one full 9c/9e/9f sweep per release is recommended so the battery stays proven** (see `real-test-2026-08-11-scratchpad-I-batch.md`); 9d (destructive) requires explicit user confirmation. **After every run: `git status` to catch unauthorized agent edits.**
|
|
698
699
|
- [ ] **Output report**: save `docs/real-test/reports/real-test-<YYYY-MM-DD>-<slug>.md` from `skills/real-test-pi-crew/REPORT-TEMPLATE.md`, filled DURING the run with per-tier evidence (counts/md5/runId) — not reconstructed from memory afterward. This is what makes past runs verifiable instead of trust-the-summary.
|
|
699
700
|
|
|
700
701
|
**"All 9 tiers pass" is a claim that needs per-row evidence.** Tier 9 means 9a **and** 9b **and** whichever of 9c–9f applies to the change — not "9a passed, therefore 9 passed". If any required item above is unchecked or lacks concrete evidence (a number, an md5, a runId), the answer to "is it tested?" is **no** — say so explicitly instead of rounding up to "pass".
|
package/src/config/config.ts
CHANGED
|
@@ -1249,12 +1249,20 @@ export function updateConfig(patch: PiTeamsConfig, options: UpdateConfigOptions
|
|
|
1249
1249
|
for (const unset of options.unsetPaths) unsetPath(raw, unset);
|
|
1250
1250
|
merged = parseConfig(raw);
|
|
1251
1251
|
}
|
|
1252
|
+
// Skip-if-unchanged: an empty/identical patch must not rewrite the file
|
|
1253
|
+
// (e.g. `team action='config'` with an empty patch — read-only path).
|
|
1254
|
+
// Both sides are parseConfig-normalized, so JSON.stringify key order is
|
|
1255
|
+
// deterministic (same construction path); no key sorting needed.
|
|
1256
|
+
const normalizedCurrent = parseConfig(current);
|
|
1257
|
+
if (JSON.stringify(merged) === JSON.stringify(normalizedCurrent)) {
|
|
1258
|
+
return { path: filePath, config: merged, written: false }; // unchanged — skip write
|
|
1259
|
+
}
|
|
1252
1260
|
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
1253
1261
|
atomicWriteFile(filePath, `${JSON.stringify(merged, null, 2)}\n`);
|
|
1254
1262
|
// (F16) Invalidate the loadConfig cache after a write — the next
|
|
1255
1263
|
// caller must see the new value, not a 0-2s stale snapshot.
|
|
1256
1264
|
invalidateConfigCache();
|
|
1257
|
-
return { path: filePath, config: merged };
|
|
1265
|
+
return { path: filePath, config: merged, written: true };
|
|
1258
1266
|
});
|
|
1259
1267
|
}
|
|
1260
1268
|
|
|
@@ -1273,10 +1281,18 @@ export function updateAutonomousConfig(patch: PiTeamsAutonomousConfig): SavedPiT
|
|
|
1273
1281
|
current.autonomous && typeof current.autonomous === "object" && !Array.isArray(current.autonomous)
|
|
1274
1282
|
? (current.autonomous as Record<string, unknown>)
|
|
1275
1283
|
: {};
|
|
1276
|
-
|
|
1284
|
+
// Skip-if-unchanged (raw shape): a no-op autonomous patch must not
|
|
1285
|
+
// rewrite the file. NOTE: compare the RAW on-disk record, NOT the
|
|
1286
|
+
// parseConfig-normalized shape — normalizing would add default keys and
|
|
1287
|
+
// false-positive the equality check.
|
|
1288
|
+
const next = { ...current, autonomous: { ...currentAutonomous, ...patch } };
|
|
1289
|
+
if (JSON.stringify(next) === JSON.stringify(current)) {
|
|
1290
|
+
return { path: filePath, config: parseConfig(current), written: false }; // unchanged — skip write
|
|
1291
|
+
}
|
|
1292
|
+
current.autonomous = next.autonomous;
|
|
1277
1293
|
atomicWriteFile(filePath, `${JSON.stringify(current, null, 2)}\n`);
|
|
1278
1294
|
// (F16) Invalidate the loadConfig cache after a write — see updateConfig.
|
|
1279
1295
|
invalidateConfigCache();
|
|
1280
|
-
return { path: filePath, config: parseConfig(current) };
|
|
1296
|
+
return { path: filePath, config: parseConfig(current), written: true };
|
|
1281
1297
|
});
|
|
1282
1298
|
}
|
package/src/config/role-tools.ts
CHANGED
|
@@ -84,7 +84,7 @@ export const ROLE_TOOL_CONFIGS: Record<string, RoleToolConfig> = {
|
|
|
84
84
|
// agents/verifier.md). Tool-set keeps bash but excludes edit/write so source
|
|
85
85
|
// integrity is preserved during verification. Mirrors cold-verifier behavior.
|
|
86
86
|
verifier: {
|
|
87
|
-
tools: ["read", "grep", "find", "ls", "bash"],
|
|
87
|
+
tools: ["read", "grep", "find", "ls", "bash", "scratchpad"],
|
|
88
88
|
excludeTools: ["edit", "write", "web"],
|
|
89
89
|
// Phase 1 scratchpad: multi-cell test/verify flows reuse parsed state.
|
|
90
90
|
scratchpad: true,
|
|
@@ -92,7 +92,7 @@ export const ROLE_TOOL_CONFIGS: Record<string, RoleToolConfig> = {
|
|
|
92
92
|
|
|
93
93
|
// Test Engineer - Can write tests (F1: hyphenated key)
|
|
94
94
|
"test-engineer": {
|
|
95
|
-
tools: ["read", "edit", "write", "bash", "ls"],
|
|
95
|
+
tools: ["read", "edit", "write", "bash", "ls", "scratchpad"],
|
|
96
96
|
excludeTools: ["web"],
|
|
97
97
|
// Phase 1 scratchpad: build/run test suites with state across cells.
|
|
98
98
|
scratchpad: true,
|
package/src/config/types.ts
CHANGED
|
@@ -312,6 +312,8 @@ export interface ConfigValidationResult {
|
|
|
312
312
|
export interface SavedPiTeamsConfig {
|
|
313
313
|
config: PiTeamsConfig;
|
|
314
314
|
path: string;
|
|
315
|
+
/** Whether the file was actually rewritten. `false` when a no-op patch hit the skip-write guard. */
|
|
316
|
+
written: boolean;
|
|
315
317
|
}
|
|
316
318
|
|
|
317
319
|
export interface UpdateConfigOptions {
|
|
@@ -301,6 +301,7 @@ export function readKnowledge(cwd: string, query?: KnowledgeQuery): string {
|
|
|
301
301
|
content = `${content.slice(0, MAX_KNOWLEDGE_HEAD_BYTES)}\n\n<!-- knowledge.md truncated at ${MAX_KNOWLEDGE_HEAD_BYTES} bytes (head shown). Full file: ${p} — use the \`read\` tool if you need sections beyond the head. -->`;
|
|
302
302
|
}
|
|
303
303
|
knowledgeCache.set(p, { key: cacheKey, content });
|
|
304
|
+
enforceKnowledgeCacheCap(knowledgeCache);
|
|
304
305
|
return content;
|
|
305
306
|
}
|
|
306
307
|
|
|
@@ -325,6 +326,7 @@ export function readKnowledge(cwd: string, query?: KnowledgeQuery): string {
|
|
|
325
326
|
conventions: parsed.conventions,
|
|
326
327
|
sessionLog: parsed.sessionLog,
|
|
327
328
|
});
|
|
329
|
+
enforceKnowledgeCacheCap(sectionCache);
|
|
328
330
|
}
|
|
329
331
|
|
|
330
332
|
const queryText = [query.goal, query.taskText].filter(Boolean).join(" \n ");
|
|
@@ -382,6 +384,21 @@ interface CachedSections {
|
|
|
382
384
|
}
|
|
383
385
|
const sectionCache = new Map<string, CachedSections>();
|
|
384
386
|
|
|
387
|
+
// H6 (2026-08-10): FIFO cap — mirrors the discover-* caches
|
|
388
|
+
// (TEAM_DISCOVERY_MAX_ENTRIES = 32, WORKFLOW_DISCOVERY_MAX_ENTRIES = 32).
|
|
389
|
+
// Without this, the caches grow with every distinct project root seen by the
|
|
390
|
+
// process; each entry can be KB-scale (full knowledge.md content + parsed
|
|
391
|
+
// sections). Eviction is safe: a cache miss falls through to a fresh
|
|
392
|
+
// readFileSync + parse, same as the first call for that path.
|
|
393
|
+
const KNOWLEDGE_CACHE_MAX_ENTRIES = 64;
|
|
394
|
+
|
|
395
|
+
function enforceKnowledgeCacheCap(cache: Map<string, unknown>): void {
|
|
396
|
+
if (cache.size > KNOWLEDGE_CACHE_MAX_ENTRIES) {
|
|
397
|
+
const oldest = cache.keys().next().value;
|
|
398
|
+
if (oldest !== undefined) cache.delete(oldest);
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
385
402
|
/** Head cap for the no-query (legacy / main-session) path. */
|
|
386
403
|
const MAX_KNOWLEDGE_HEAD_BYTES = 2_000;
|
|
387
404
|
|
package/src/extension/pi-api.ts
CHANGED
|
@@ -38,19 +38,3 @@ export type {
|
|
|
38
38
|
} from "@earendil-works/pi-coding-agent";
|
|
39
39
|
|
|
40
40
|
export { createBashTool, defineTool } from "@earendil-works/pi-coding-agent";
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
* @deprecated Drift detector removed in Phase 5 follow-up to H1.
|
|
44
|
-
*
|
|
45
|
-
* History: this constant was meant to surface version-drift between
|
|
46
|
-
* dev-time type-check and runtime peer-dep version. pi-crew declares
|
|
47
|
-
* peer deps as `*` so the runtime version is set by whatever host pi
|
|
48
|
-
* install the user has — pinning this constant to a specific devDep
|
|
49
|
-
* range was a false invariant that did not reflect how extensions
|
|
50
|
-
* actually consume peer packages.
|
|
51
|
-
*
|
|
52
|
-
* Kept exported (now `"0.0.0-unset"`) only for any downstream consumers
|
|
53
|
-
* that may import it via re-exports. No diagnostic uses it anymore.
|
|
54
|
-
* Will be removed in a future major version.
|
|
55
|
-
*/
|
|
56
|
-
export const BUILT_AGAINST_PI_VERSION = "0.0.0-unset";
|
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Extracted `api` operation handlers — live-agent control group (H3 phase 2).
|
|
3
|
+
*
|
|
4
|
+
* Extracted from `src/extension/team-tool/api.ts` on 2026-08-10. Behaviour is
|
|
5
|
+
* byte-identical to the inline `if` blocks they replace.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { readCrewAgents } from "../../../runtime/crew-agent-records.ts";
|
|
9
|
+
import { appendLiveAgentControlRequest } from "../../../runtime/live-session/live-agent-control.ts";
|
|
10
|
+
import {
|
|
11
|
+
followUpLiveAgent,
|
|
12
|
+
getLiveAgent,
|
|
13
|
+
listActiveLiveAgents,
|
|
14
|
+
resumeLiveAgent,
|
|
15
|
+
steerLiveAgent,
|
|
16
|
+
stopLiveAgent,
|
|
17
|
+
} from "../../../runtime/live-session/live-agent-manager.ts";
|
|
18
|
+
import { liveControlRealtimeMessage, publishLiveControlRealtime } from "../../../runtime/live-session/live-control-realtime.ts";
|
|
19
|
+
import {
|
|
20
|
+
appendFollowUpMessage,
|
|
21
|
+
appendFollowUpMessageAsync,
|
|
22
|
+
appendSteeringMessage,
|
|
23
|
+
appendSteeringMessageAsync,
|
|
24
|
+
} from "../../../state/coordination/mailbox.ts";
|
|
25
|
+
import { appendEvent } from "../../../state/event-log/event-log.ts";
|
|
26
|
+
import type { ApiOperationHandler } from "./handler-context.ts";
|
|
27
|
+
|
|
28
|
+
export const handleNudgeAgent: ApiOperationHandler = (hctx) => {
|
|
29
|
+
const { cfg, loaded, result, paramRequired, ctx } = hctx;
|
|
30
|
+
const agentId = typeof cfg.agentId === "string" ? cfg.agentId : undefined;
|
|
31
|
+
const agent = readCrewAgents(loaded.manifest).find((item) => item.id === agentId || item.taskId === agentId);
|
|
32
|
+
if (!agent)
|
|
33
|
+
return result(
|
|
34
|
+
paramRequired(
|
|
35
|
+
"API nudge-agent",
|
|
36
|
+
"config.agentId matching an agent id or task id",
|
|
37
|
+
"{ action: 'api', runId: 'team_...', config: { operation: 'nudge-agent', agentId: 'agent-1' } }",
|
|
38
|
+
),
|
|
39
|
+
{
|
|
40
|
+
action: "api",
|
|
41
|
+
status: "error",
|
|
42
|
+
runId: loaded.manifest.runId,
|
|
43
|
+
},
|
|
44
|
+
true,
|
|
45
|
+
);
|
|
46
|
+
const messageText =
|
|
47
|
+
typeof cfg.message === "string" && cfg.message.trim()
|
|
48
|
+
? cfg.message.trim()
|
|
49
|
+
: "Please report your current status, blocker, or smallest next step.";
|
|
50
|
+
const message = appendSteeringMessage(loaded.manifest, {
|
|
51
|
+
taskId: agent.taskId,
|
|
52
|
+
to: agent.taskId,
|
|
53
|
+
body: messageText,
|
|
54
|
+
priority: "normal",
|
|
55
|
+
data: { source: "nudge-agent" },
|
|
56
|
+
});
|
|
57
|
+
appendEvent(loaded.manifest.eventsPath, {
|
|
58
|
+
type: "agent.nudged",
|
|
59
|
+
runId: loaded.manifest.runId,
|
|
60
|
+
taskId: agent.taskId,
|
|
61
|
+
message: messageText,
|
|
62
|
+
data: { agentId: agent.id, mailboxMessageId: message.id },
|
|
63
|
+
});
|
|
64
|
+
ctx.events?.emit?.("crew.mailbox.message", {
|
|
65
|
+
runId: loaded.manifest.runId,
|
|
66
|
+
id: message.id,
|
|
67
|
+
direction: message.direction,
|
|
68
|
+
from: message.from,
|
|
69
|
+
to: message.to,
|
|
70
|
+
taskId: message.taskId,
|
|
71
|
+
source: "nudge-agent",
|
|
72
|
+
});
|
|
73
|
+
return result(JSON.stringify({ agentId: agent.id, mailboxMessage: message }, null, 2), {
|
|
74
|
+
action: "api",
|
|
75
|
+
status: "ok",
|
|
76
|
+
runId: loaded.manifest.runId,
|
|
77
|
+
artifactsRoot: loaded.manifest.artifactsRoot,
|
|
78
|
+
});
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
export const handleListLiveAgents: ApiOperationHandler = (hctx) => {
|
|
82
|
+
const { loaded, result } = hctx;
|
|
83
|
+
return result(
|
|
84
|
+
JSON.stringify(
|
|
85
|
+
listActiveLiveAgents().filter((agent) => agent.runId === loaded.manifest.runId),
|
|
86
|
+
null,
|
|
87
|
+
2,
|
|
88
|
+
),
|
|
89
|
+
{
|
|
90
|
+
action: "api",
|
|
91
|
+
status: "ok",
|
|
92
|
+
runId: loaded.manifest.runId,
|
|
93
|
+
artifactsRoot: loaded.manifest.artifactsRoot,
|
|
94
|
+
},
|
|
95
|
+
);
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Handles steer-agent / follow-up-agent / stop-agent / resume-agent /
|
|
100
|
+
* interrupt-agent — the live-agent control cluster (shared body in the
|
|
101
|
+
* original handleApi; interrupt falls through to stop).
|
|
102
|
+
*/
|
|
103
|
+
export const handleLiveAgentControl: ApiOperationHandler = async (hctx) => {
|
|
104
|
+
const { cfg, loaded, result, paramRequired, ctx } = hctx;
|
|
105
|
+
const operation = typeof cfg.operation === "string" ? cfg.operation : "";
|
|
106
|
+
const agentId = typeof cfg.agentId === "string" ? cfg.agentId : undefined;
|
|
107
|
+
if (!agentId)
|
|
108
|
+
return result(
|
|
109
|
+
paramRequired(
|
|
110
|
+
`API ${operation}`,
|
|
111
|
+
"config.agentId",
|
|
112
|
+
`{ action: 'api', runId: 'team_...', config: { operation: '${operation}', agentId: 'agent-1' } }`,
|
|
113
|
+
),
|
|
114
|
+
{
|
|
115
|
+
action: "api",
|
|
116
|
+
status: "error",
|
|
117
|
+
runId: loaded.manifest.runId,
|
|
118
|
+
},
|
|
119
|
+
true,
|
|
120
|
+
);
|
|
121
|
+
const message = typeof cfg.message === "string" && cfg.message.trim() ? cfg.message.trim() : undefined;
|
|
122
|
+
const prompt = typeof cfg.prompt === "string" && cfg.prompt.trim() ? cfg.prompt.trim() : message;
|
|
123
|
+
try {
|
|
124
|
+
const live = getLiveAgent(agentId);
|
|
125
|
+
if (live && live.runId !== loaded.manifest.runId)
|
|
126
|
+
return result(
|
|
127
|
+
`Live agent '${agentId}' does not belong to run ${loaded.manifest.runId}.`,
|
|
128
|
+
{
|
|
129
|
+
action: "api",
|
|
130
|
+
status: "error",
|
|
131
|
+
runId: loaded.manifest.runId,
|
|
132
|
+
},
|
|
133
|
+
true,
|
|
134
|
+
);
|
|
135
|
+
if (live && live.workspaceId !== loaded.manifest.cwd)
|
|
136
|
+
return result(
|
|
137
|
+
`Live agent '${agentId}' does not belong to workspace ${loaded.manifest.cwd}.`,
|
|
138
|
+
{
|
|
139
|
+
action: "api",
|
|
140
|
+
status: "error",
|
|
141
|
+
runId: loaded.manifest.runId,
|
|
142
|
+
},
|
|
143
|
+
true,
|
|
144
|
+
);
|
|
145
|
+
if (!live && (operation === "steer-agent" || operation === "follow-up-agent"))
|
|
146
|
+
throw new Error(`Live agent '${agentId}' not found.`);
|
|
147
|
+
const liveTaskId = live?.taskId;
|
|
148
|
+
if ((operation === "steer-agent" || operation === "follow-up-agent") && !liveTaskId)
|
|
149
|
+
throw new Error(`Live agent '${agentId}' not found.`);
|
|
150
|
+
const targetTaskId = liveTaskId ?? agentId;
|
|
151
|
+
if (operation === "steer-agent") {
|
|
152
|
+
const text = message ?? "Please report current status and wrap up if possible.";
|
|
153
|
+
const realtime = await steerLiveAgent(agentId, text);
|
|
154
|
+
const mailboxMessage = await appendSteeringMessageAsync(loaded.manifest, {
|
|
155
|
+
taskId: targetTaskId,
|
|
156
|
+
body: text,
|
|
157
|
+
status: "delivered",
|
|
158
|
+
data: { source: "steer-agent", realtime: true },
|
|
159
|
+
});
|
|
160
|
+
return result(JSON.stringify({ realtime, mailboxMessage }, null, 2), {
|
|
161
|
+
action: "api",
|
|
162
|
+
status: "ok",
|
|
163
|
+
runId: loaded.manifest.runId,
|
|
164
|
+
artifactsRoot: loaded.manifest.artifactsRoot,
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
if (operation === "follow-up-agent") {
|
|
168
|
+
if (!prompt)
|
|
169
|
+
return result(
|
|
170
|
+
paramRequired(
|
|
171
|
+
"API follow-up-agent",
|
|
172
|
+
"config.prompt or config.message",
|
|
173
|
+
"{ action: 'api', runId: 'team_...', config: { operation: 'follow-up-agent', agentId: 'agent-1', prompt: '<next step>' } }",
|
|
174
|
+
),
|
|
175
|
+
{
|
|
176
|
+
action: "api",
|
|
177
|
+
status: "error",
|
|
178
|
+
runId: loaded.manifest.runId,
|
|
179
|
+
},
|
|
180
|
+
true,
|
|
181
|
+
);
|
|
182
|
+
const realtime = await followUpLiveAgent(agentId, prompt);
|
|
183
|
+
const mailboxMessage = await appendFollowUpMessageAsync(loaded.manifest, {
|
|
184
|
+
taskId: targetTaskId,
|
|
185
|
+
body: prompt,
|
|
186
|
+
status: "delivered",
|
|
187
|
+
data: { source: "follow-up-agent", realtime: true },
|
|
188
|
+
});
|
|
189
|
+
return result(JSON.stringify({ realtime, mailboxMessage }, null, 2), {
|
|
190
|
+
action: "api",
|
|
191
|
+
status: "ok",
|
|
192
|
+
runId: loaded.manifest.runId,
|
|
193
|
+
artifactsRoot: loaded.manifest.artifactsRoot,
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
if (operation === "resume-agent") {
|
|
197
|
+
if (!prompt)
|
|
198
|
+
return result(
|
|
199
|
+
paramRequired(
|
|
200
|
+
"API resume-agent",
|
|
201
|
+
"config.prompt or config.message",
|
|
202
|
+
"{ action: 'api', runId: 'team_...', config: { operation: 'resume-agent', agentId: 'agent-1', prompt: '<resume instruction>' } }",
|
|
203
|
+
),
|
|
204
|
+
{
|
|
205
|
+
action: "api",
|
|
206
|
+
status: "error",
|
|
207
|
+
runId: loaded.manifest.runId,
|
|
208
|
+
},
|
|
209
|
+
true,
|
|
210
|
+
);
|
|
211
|
+
return result(JSON.stringify(await resumeLiveAgent(agentId, prompt), null, 2), {
|
|
212
|
+
action: "api",
|
|
213
|
+
status: "ok",
|
|
214
|
+
runId: loaded.manifest.runId,
|
|
215
|
+
artifactsRoot: loaded.manifest.artifactsRoot,
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
return result(JSON.stringify(await stopLiveAgent(agentId), null, 2), {
|
|
219
|
+
action: "api",
|
|
220
|
+
status: "ok",
|
|
221
|
+
runId: loaded.manifest.runId,
|
|
222
|
+
artifactsRoot: loaded.manifest.artifactsRoot,
|
|
223
|
+
});
|
|
224
|
+
} catch (error) {
|
|
225
|
+
const agent = readCrewAgents(loaded.manifest).find((item) => item.id === agentId || item.taskId === agentId);
|
|
226
|
+
if (!agent) {
|
|
227
|
+
const err = error instanceof Error ? error.message : String(error);
|
|
228
|
+
return result(
|
|
229
|
+
err,
|
|
230
|
+
{
|
|
231
|
+
action: "api",
|
|
232
|
+
status: "error",
|
|
233
|
+
runId: loaded.manifest.runId,
|
|
234
|
+
},
|
|
235
|
+
true,
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
const task = loaded.tasks.find((item) => item.id === agent.taskId);
|
|
239
|
+
if (!task)
|
|
240
|
+
return result(
|
|
241
|
+
`API ${operation} agent '${agentId}' does not match a run task.`,
|
|
242
|
+
{
|
|
243
|
+
action: "api",
|
|
244
|
+
status: "error",
|
|
245
|
+
runId: loaded.manifest.runId,
|
|
246
|
+
},
|
|
247
|
+
true,
|
|
248
|
+
);
|
|
249
|
+
if (operation === "resume-agent" && !prompt)
|
|
250
|
+
return result(
|
|
251
|
+
paramRequired(
|
|
252
|
+
"API resume-agent",
|
|
253
|
+
"config.prompt or config.message",
|
|
254
|
+
"{ action: 'api', runId: 'team_...', config: { operation: 'resume-agent', agentId: 'agent-1', prompt: '<resume instruction>' } }",
|
|
255
|
+
),
|
|
256
|
+
{
|
|
257
|
+
action: "api",
|
|
258
|
+
status: "error",
|
|
259
|
+
runId: loaded.manifest.runId,
|
|
260
|
+
},
|
|
261
|
+
true,
|
|
262
|
+
);
|
|
263
|
+
if (operation === "follow-up-agent" && !prompt)
|
|
264
|
+
return result(
|
|
265
|
+
paramRequired(
|
|
266
|
+
"API follow-up-agent",
|
|
267
|
+
"config.prompt or config.message",
|
|
268
|
+
"{ action: 'api', runId: 'team_...', config: { operation: 'follow-up-agent', agentId: 'agent-1', prompt: '<next step>' } }",
|
|
269
|
+
),
|
|
270
|
+
{
|
|
271
|
+
action: "api",
|
|
272
|
+
status: "error",
|
|
273
|
+
runId: loaded.manifest.runId,
|
|
274
|
+
},
|
|
275
|
+
true,
|
|
276
|
+
);
|
|
277
|
+
try {
|
|
278
|
+
const request = appendLiveAgentControlRequest(loaded.manifest, {
|
|
279
|
+
taskId: task.id,
|
|
280
|
+
agentId: agent.id,
|
|
281
|
+
operation:
|
|
282
|
+
operation === "resume-agent"
|
|
283
|
+
? "resume"
|
|
284
|
+
: operation === "follow-up-agent"
|
|
285
|
+
? "follow-up"
|
|
286
|
+
: operation === "steer-agent"
|
|
287
|
+
? "steer"
|
|
288
|
+
: "stop",
|
|
289
|
+
message: operation === "resume-agent" || operation === "follow-up-agent" ? prompt : message,
|
|
290
|
+
});
|
|
291
|
+
const mailboxMessage =
|
|
292
|
+
operation === "steer-agent"
|
|
293
|
+
? appendSteeringMessage(loaded.manifest, {
|
|
294
|
+
taskId: task.id,
|
|
295
|
+
to: agent.id,
|
|
296
|
+
body: message ?? "Please report current status and wrap up if possible.",
|
|
297
|
+
status: "delivered",
|
|
298
|
+
data: {
|
|
299
|
+
source: "steer-agent",
|
|
300
|
+
liveControlRequestId: request.id,
|
|
301
|
+
},
|
|
302
|
+
})
|
|
303
|
+
: operation === "follow-up-agent" && prompt
|
|
304
|
+
? appendFollowUpMessage(loaded.manifest, {
|
|
305
|
+
taskId: task.id,
|
|
306
|
+
to: agent.id,
|
|
307
|
+
body: prompt,
|
|
308
|
+
status: "delivered",
|
|
309
|
+
data: {
|
|
310
|
+
source: "follow-up-agent",
|
|
311
|
+
liveControlRequestId: request.id,
|
|
312
|
+
},
|
|
313
|
+
})
|
|
314
|
+
: undefined;
|
|
315
|
+
publishLiveControlRealtime(request);
|
|
316
|
+
ctx.events?.emit?.("pi-crew:live-control", liveControlRealtimeMessage(request));
|
|
317
|
+
appendEvent(loaded.manifest.eventsPath, {
|
|
318
|
+
type: "agent.control.queued",
|
|
319
|
+
runId: loaded.manifest.runId,
|
|
320
|
+
taskId: agent.taskId,
|
|
321
|
+
message: `Queued ${request.operation} control request for live agent.`,
|
|
322
|
+
data: {
|
|
323
|
+
request,
|
|
324
|
+
mailboxMessageId: mailboxMessage?.id,
|
|
325
|
+
realtime: true,
|
|
326
|
+
},
|
|
327
|
+
});
|
|
328
|
+
return result(JSON.stringify({ queued: true, request, mailboxMessage }, null, 2), {
|
|
329
|
+
action: "api",
|
|
330
|
+
status: "ok",
|
|
331
|
+
runId: loaded.manifest.runId,
|
|
332
|
+
artifactsRoot: loaded.manifest.artifactsRoot,
|
|
333
|
+
});
|
|
334
|
+
} catch (queueError) {
|
|
335
|
+
const message = queueError instanceof Error ? queueError.message : String(queueError);
|
|
336
|
+
return result(
|
|
337
|
+
message,
|
|
338
|
+
{
|
|
339
|
+
action: "api",
|
|
340
|
+
status: "error",
|
|
341
|
+
runId: loaded.manifest.runId,
|
|
342
|
+
},
|
|
343
|
+
true,
|
|
344
|
+
);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
};
|
|
348
|
+
|
|
349
|
+
/** Dispatcher map for the live-agent control group. Consumed by handleApi. */
|
|
350
|
+
export const AGENT_CONTROL_OPERATIONS: Record<string, ApiOperationHandler> = {
|
|
351
|
+
"nudge-agent": handleNudgeAgent,
|
|
352
|
+
"list-live-agents": handleListLiveAgents,
|
|
353
|
+
"steer-agent": handleLiveAgentControl,
|
|
354
|
+
"follow-up-agent": handleLiveAgentControl,
|
|
355
|
+
"stop-agent": handleLiveAgentControl,
|
|
356
|
+
"resume-agent": handleLiveAgentControl,
|
|
357
|
+
"interrupt-agent": handleLiveAgentControl,
|
|
358
|
+
};
|