@pi-unipi/unipi 3.0.0-alpha.2 → 3.0.0-alpha.21
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 +292 -0
- package/README.md +8 -11
- package/docs/prefix-cache-architecture.md +14 -0
- package/package.json +22 -24
- package/packages/ask-user/README.md +55 -107
- package/packages/ask-user/ask-ui.ts +306 -732
- package/packages/ask-user/clipboard.ts +50 -0
- package/packages/ask-user/config.ts +74 -78
- package/packages/ask-user/package.json +2 -2
- package/packages/ask-user/questions.ts +151 -0
- package/packages/ask-user/skills/ask-user/SKILL.md +59 -133
- package/packages/ask-user/tools.ts +198 -432
- package/packages/autocomplete/package.json +1 -1
- package/packages/autocomplete/src/constants.ts +38 -43
- package/packages/autocomplete/src/sorting.ts +8 -0
- package/packages/background-tasks/package.json +2 -2
- package/packages/background-tasks/src/config.ts +0 -73
- package/packages/background-tasks/src/index.ts +20 -16
- package/packages/background-tasks/src/registry.ts +1 -171
- package/packages/background-tasks/src/types.ts +0 -32
- package/packages/btw/README.md +13 -56
- package/packages/btw/extensions/btw.ts +403 -1796
- package/packages/btw/package.json +5 -2
- package/packages/btw/skills/btw/SKILL.md +7 -143
- package/packages/compactor/README.md +56 -287
- package/packages/compactor/package.json +5 -9
- package/packages/compactor/skills/compactor/SKILL.md +13 -20
- package/packages/compactor/src/card.ts +90 -0
- package/packages/compactor/src/commands/index.ts +73 -130
- package/packages/compactor/src/compaction/compact-args.ts +5 -3
- package/packages/compactor/src/compaction/filter-noise.ts +1 -1
- package/packages/compactor/src/compaction/hooks.ts +487 -311
- package/packages/compactor/src/compaction/source.ts +178 -0
- package/packages/compactor/src/compaction/summarize.ts +586 -52
- package/packages/compactor/src/config/manager.ts +130 -127
- package/packages/compactor/src/config/schema.ts +32 -48
- package/packages/compactor/src/index.ts +40 -431
- package/packages/compactor/src/info-screen.ts +44 -190
- package/packages/compactor/src/session/recall-blocks.ts +1 -1
- package/packages/compactor/src/stats.ts +76 -0
- package/packages/compactor/src/tools/context-budget.ts +17 -34
- package/packages/compactor/src/tools/ctx-doctor.ts +45 -36
- package/packages/compactor/src/tools/register.ts +29 -269
- package/packages/compactor/src/types.ts +47 -186
- package/packages/core/README.md +25 -0
- package/packages/core/child-agent.ts +1 -0
- package/packages/core/command-echo.ts +63 -0
- package/packages/core/compaction-context.ts +41 -0
- package/packages/core/constants.ts +8 -38
- package/packages/core/events.ts +0 -53
- package/packages/core/fusion-status.ts +4 -0
- package/packages/core/global.d.ts +10 -0
- package/packages/core/harness-messages.ts +392 -0
- package/packages/core/index.ts +14 -0
- package/packages/core/kanboard-status.ts +49 -0
- package/packages/core/long-horizon-owner-status.ts +46 -0
- package/packages/core/model-cache.ts +81 -7
- package/packages/core/package.json +6 -3
- package/packages/core/src/attach/detect.ts +163 -0
- package/packages/core/src/child-agent/child-args.ts +31 -0
- package/packages/core/src/child-agent/delivery.ts +41 -0
- package/packages/core/src/child-agent/index.ts +5 -0
- package/packages/core/src/child-agent/readers.ts +203 -0
- package/packages/core/src/child-agent/runtime.ts +584 -0
- package/packages/core/src/evidence.ts +78 -0
- package/packages/core/src/hints/browser.ts +278 -0
- package/packages/core/src/hints/crab-data.ts +19 -0
- package/packages/core/src/hints/index.ts +865 -0
- package/packages/core/src/hints/lines.ts +607 -0
- package/packages/core/src/hints/store.ts +120 -0
- package/packages/core/src/jev/chatter.ts +24 -0
- package/packages/core/src/jev/settings.ts +177 -14
- package/packages/core/src/package-colors.ts +2 -0
- package/packages/core/src/settings/catalog.ts +39 -15
- package/packages/core/src/settings/engine.ts +48 -2
- package/packages/core/src/settings/hub.ts +83 -20
- package/packages/core/src/settings/open.ts +49 -0
- package/packages/core/src/settings/schema.ts +27 -7
- package/packages/core/src/tui/hub-kit.ts +18 -5
- package/packages/core/src/tui/kit.ts +198 -0
- package/packages/core/src/tui/progress.ts +73 -0
- package/packages/core/src/turn/arbiter.ts +290 -0
- package/packages/footer/README.md +4 -6
- package/packages/footer/package.json +3 -3
- package/packages/footer/src/glance-editor.ts +19 -11
- package/packages/footer/src/help.ts +1 -1
- package/packages/footer/src/index.ts +36 -3
- package/packages/footer/src/presets.ts +3 -4
- package/packages/footer/src/rendering/icons.ts +0 -20
- package/packages/footer/src/segments/compactor.ts +52 -142
- package/packages/footer/src/segments/status-ext.ts +0 -1
- package/packages/fusion/README.md +14 -4
- package/packages/fusion/package.json +4 -3
- package/packages/fusion/src/child-args.ts +1 -0
- package/packages/fusion/src/index.ts +161 -8
- package/packages/fusion/src/pi-settings.ts +57 -0
- package/packages/fusion/src/picker.ts +186 -51
- package/packages/fusion/src/preset.ts +56 -2
- package/packages/fusion/src/prompts.ts +1 -1
- package/packages/fusion/src/sidekick-runtime.ts +1 -420
- package/packages/fusion/src/tools.ts +107 -128
- package/packages/fusion/src/transcript.ts +27 -85
- package/packages/info-screen/package.json +2 -2
- package/packages/input-shortcuts/README.md +18 -30
- package/packages/input-shortcuts/package.json +2 -2
- package/packages/input-shortcuts/src/burst.ts +88 -0
- package/packages/input-shortcuts/src/chord-overlay.ts +20 -70
- package/packages/input-shortcuts/src/cleared-input.ts +97 -0
- package/packages/input-shortcuts/src/index.ts +99 -193
- package/packages/input-shortcuts/src/last-response.ts +40 -0
- package/packages/input-shortcuts/src/registers.ts +8 -23
- package/packages/input-shortcuts/src/types.ts +4 -16
- package/packages/input-shortcuts/src/undo-redo.ts +59 -47
- package/packages/kanboard/README.md +51 -47
- package/packages/kanboard/index.ts +293 -55
- package/packages/kanboard/package.json +8 -8
- package/packages/kanboard/skills/kanboard/SKILL.md +61 -27
- package/packages/kanboard/src/badges.ts +93 -0
- package/packages/kanboard/src/bin.ts +4 -2
- package/packages/kanboard/src/commands.ts +218 -84
- package/packages/kanboard/src/debug.ts +20 -0
- package/packages/kanboard/src/guard.ts +267 -109
- package/packages/kanboard/src/monitor.ts +330 -0
- package/packages/kanboard/src/notice-buffer.ts +74 -0
- package/packages/kanboard/src/progress.ts +27 -0
- package/packages/kanboard/src/reminders.ts +234 -0
- package/packages/kanboard/src/settings.ts +69 -15
- package/packages/kanboard/src/shapes.ts +5 -0
- package/packages/long-horizon/index.ts +190 -13
- package/packages/long-horizon/package.json +3 -3
- package/packages/long-horizon/src/commands.ts +103 -7
- package/packages/long-horizon/src/compaction-brief.ts +35 -0
- package/packages/long-horizon/src/engine/continuation.ts +84 -15
- package/packages/long-horizon/src/engine/goal-state.ts +64 -13
- package/packages/long-horizon/src/engine/nudge-stash.ts +71 -0
- package/packages/long-horizon/src/engine/ralph.ts +3 -2
- package/packages/long-horizon/src/engine/runaway.ts +68 -11
- package/packages/long-horizon/src/engine/verifier.ts +19 -0
- package/packages/long-horizon/src/gate.ts +37 -12
- package/packages/long-horizon/src/judge/resolve.ts +1 -0
- package/packages/long-horizon/src/owner.ts +35 -2
- package/packages/long-horizon/src/progress.ts +87 -0
- package/packages/long-horizon/src/prompts/goal.ts +1 -1
- package/packages/long-horizon/src/runtime.ts +172 -18
- package/packages/long-horizon/src/settings.ts +54 -39
- package/packages/long-horizon/src/tools/goal.ts +6 -0
- package/packages/long-horizon/src/tools/graph.ts +1 -1
- package/packages/long-horizon/src/tools/swarm.ts +3 -3
- package/packages/mcp/package.json +2 -2
- package/packages/memory/commands.ts +7 -2
- package/packages/memory/index.ts +210 -32
- package/packages/memory/package.json +3 -3
- package/packages/memory/save-session.ts +264 -0
- package/packages/memory/settings.ts +17 -0
- package/packages/memory/tools.ts +253 -203
- package/packages/notify/package.json +2 -2
- package/packages/skill-registry/README.md +53 -16
- package/packages/skill-registry/index.ts +230 -0
- package/packages/skill-registry/package.json +19 -8
- package/packages/skill-registry/src/editor.ts +206 -0
- package/packages/skill-registry/src/judge.ts +289 -0
- package/packages/skill-registry/src/registry.ts +87 -0
- package/packages/skill-registry/src/settings.ts +259 -0
- package/packages/skill-registry/src/vault.ts +80 -0
- package/packages/subagents/README.md +39 -119
- package/packages/subagents/package.json +6 -13
- package/packages/subagents/skills/subagents/SKILL.md +18 -42
- package/packages/subagents/src/agents.ts +213 -0
- package/packages/subagents/src/cards.ts +182 -0
- package/packages/subagents/src/index.ts +609 -1084
- package/packages/subagents/src/manager.ts +429 -0
- package/packages/subagents/src/profiles.ts +130 -0
- package/packages/subagents/src/transcript.ts +154 -0
- package/packages/subagents/src/ui.ts +389 -0
- package/packages/unipi/bundled.js +39756 -45569
- package/packages/unipi/index.ts +55 -10
- package/packages/updater/package.json +2 -2
- package/packages/utility/README.md +76 -69
- package/packages/utility/package.json +2 -2
- package/packages/utility/skills/image/SKILL.md +37 -0
- package/packages/utility/skills/utility/SKILL.md +17 -60
- package/packages/utility/src/answer/extract.ts +128 -0
- package/packages/utility/src/answer/index.ts +213 -0
- package/packages/utility/src/answer/reply.ts +133 -0
- package/packages/utility/src/answer/web.ts +167 -0
- package/packages/utility/src/attach/index.ts +205 -0
- package/packages/utility/src/commands.ts +38 -142
- package/packages/utility/src/diagnostics/engine.ts +50 -5
- package/packages/utility/src/image/index.ts +33 -0
- package/packages/utility/src/image/models.ts +221 -0
- package/packages/{image/src → utility/src/image}/recognize.ts +2 -2
- package/packages/utility/src/image/settings.ts +178 -0
- package/packages/{image/src/image-source.ts → utility/src/image/source.ts} +1 -1
- package/packages/utility/src/image/tools.ts +168 -0
- package/packages/utility/src/image/transports.ts +186 -0
- package/packages/utility/src/index.ts +68 -389
- package/packages/utility/src/lifecycle/cleanup.ts +116 -311
- package/packages/utility/src/rename/gate.ts +102 -0
- package/packages/utility/src/rename/index.ts +191 -0
- package/packages/utility/src/rename/session.ts +170 -0
- package/packages/utility/src/render/delegated.ts +285 -0
- package/packages/utility/src/render/harness.ts +683 -0
- package/packages/utility/src/render/parse.ts +156 -0
- package/packages/utility/src/render/pi-settings.ts +38 -0
- package/packages/utility/src/render/reply-bg.ts +307 -0
- package/packages/utility/src/render/simple.ts +724 -0
- package/packages/utility/src/render/spacing.ts +235 -0
- package/packages/utility/src/render/styled.ts +251 -0
- package/packages/utility/src/render/tools.ts +353 -0
- package/packages/utility/src/settings.ts +166 -233
- package/packages/utility/src/types.ts +1 -222
- package/packages/watchdog/index.ts +17 -6
- package/packages/watchdog/package.json +3 -3
- package/packages/watchdog/src/config.ts +3 -2
- package/packages/web-api/package.json +2 -2
- package/packages/workflow/README.md +3 -0
- package/packages/workflow/index.ts +0 -2
- package/packages/workflow/package.json +2 -2
- package/packages/workflow/src/permission/decide.ts +6 -4
- package/packages/workflow/src/permission/index.ts +17 -7
- package/packages/workflow/src/permission/settings.ts +39 -4
- package/packages/workflow/src/plan/index.ts +19 -4
- package/packages/background-tasks/extensions/delegate-child.ts +0 -1
- package/packages/background-tasks/src/context-parent-snapshot.ts +0 -142
- package/packages/background-tasks/src/context-token-budget.ts +0 -903
- package/packages/background-tasks/src/context-visible-conversation-v2.ts +0 -551
- package/packages/background-tasks/src/delegate/artifacts.ts +0 -487
- package/packages/background-tasks/src/delegate/budget.ts +0 -415
- package/packages/background-tasks/src/delegate/hook-contract-evidence.json +0 -18
- package/packages/background-tasks/src/delegate/hook-contract.ts +0 -154
- package/packages/background-tasks/src/delegate/launch.ts +0 -484
- package/packages/background-tasks/src/delegate/result-package.ts +0 -459
- package/packages/background-tasks/src/delegate/runner.ts +0 -430
- package/packages/background-tasks/src/delegate/seed.ts +0 -423
- package/packages/background-tasks/src/delegate/types.ts +0 -323
- package/packages/background-tasks/src/delegate-child-extension.ts +0 -978
- package/packages/background-tasks/src/delegate-extension.ts +0 -654
- package/packages/background-tasks/src/fixtures/delegate-context-incident.json +0 -17
- package/packages/compactor/skills/compactor-detail/SKILL.md +0 -116
- package/packages/compactor/skills/compactor-doctor/SKILL.md +0 -71
- package/packages/compactor/skills/compactor-stats/SKILL.md +0 -51
- package/packages/compactor/src/compaction/build-sections.ts +0 -76
- package/packages/compactor/src/compaction/extract/goals.ts +0 -36
- package/packages/compactor/src/compaction/extract/preferences.ts +0 -40
- package/packages/compactor/src/compaction/format.ts +0 -53
- package/packages/compactor/src/compaction/merge.ts +0 -143
- package/packages/compactor/src/config/presets.ts +0 -160
- package/packages/compactor/src/executor/executor.ts +0 -233
- package/packages/compactor/src/executor/runtime.ts +0 -115
- package/packages/compactor/src/security/evaluator.ts +0 -89
- package/packages/compactor/src/security/policy.ts +0 -96
- package/packages/compactor/src/security/scanner.ts +0 -65
- package/packages/compactor/src/session/auto-inject.ts +0 -60
- package/packages/compactor/src/session/db.ts +0 -325
- package/packages/compactor/src/session/extract.ts +0 -144
- package/packages/compactor/src/session/resume-inject.ts +0 -64
- package/packages/compactor/src/session/snapshot.ts +0 -327
- package/packages/compactor/src/tools/ctx-batch-execute.ts +0 -41
- package/packages/compactor/src/tools/ctx-execute-file.ts +0 -23
- package/packages/compactor/src/tools/ctx-execute.ts +0 -23
- package/packages/compactor/src/tools/ctx-stats.ts +0 -96
- package/packages/image/README.md +0 -99
- package/packages/image/package.json +0 -61
- package/packages/image/skills/image/SKILL.md +0 -76
- package/packages/image/src/generate.ts +0 -256
- package/packages/image/src/index.ts +0 -135
- package/packages/image/src/models.ts +0 -519
- package/packages/image/src/openai-images-api.ts +0 -282
- package/packages/image/src/register-providers.ts +0 -209
- package/packages/image/src/settings.ts +0 -173
- package/packages/image/src/tools.ts +0 -326
- package/packages/input-shortcuts/src/clipboard.ts +0 -119
- package/packages/kanboard/src/runner.ts +0 -817
- package/packages/skill-registry/skills/auto/SKILL.md +0 -465
- package/packages/skill-registry/skills/brainstorm/SKILL.md +0 -219
- package/packages/skill-registry/skills/chore-create/SKILL.md +0 -315
- package/packages/skill-registry/skills/chore-execute/SKILL.md +0 -314
- package/packages/skill-registry/skills/consolidate/SKILL.md +0 -142
- package/packages/skill-registry/skills/consultant/SKILL.md +0 -97
- package/packages/skill-registry/skills/debug/SKILL.md +0 -228
- package/packages/skill-registry/skills/document/SKILL.md +0 -120
- package/packages/skill-registry/skills/fix/SKILL.md +0 -212
- package/packages/skill-registry/skills/gather-context/SKILL.md +0 -145
- package/packages/skill-registry/skills/plan/SKILL.md +0 -231
- package/packages/skill-registry/skills/quick-fix/SKILL.md +0 -135
- package/packages/skill-registry/skills/quick-work/SKILL.md +0 -110
- package/packages/skill-registry/skills/research/SKILL.md +0 -251
- package/packages/skill-registry/skills/review-work/SKILL.md +0 -187
- package/packages/skill-registry/skills/scan-issues/SKILL.md +0 -195
- package/packages/skill-registry/skills/work/SKILL.md +0 -218
- package/packages/skill-registry/skills/worktree-create/SKILL.md +0 -69
- package/packages/skill-registry/skills/worktree-list/SKILL.md +0 -78
- package/packages/skill-registry/skills/worktree-merge/SKILL.md +0 -89
- package/packages/subagents/agents/delegate.md +0 -14
- package/packages/subagents/agents/oracle.md +0 -78
- package/packages/subagents/agents/researcher.md +0 -52
- package/packages/subagents/agents/reviewer.md +0 -80
- package/packages/subagents/agents/scout.md +0 -50
- package/packages/subagents/agents/worker.md +0 -59
- package/packages/subagents/prompts/council.md +0 -48
- package/packages/subagents/prompts/gather-context-and-clarify.md +0 -13
- package/packages/subagents/prompts/parallel-cleanup.md +0 -59
- package/packages/subagents/prompts/parallel-research.md +0 -50
- package/packages/subagents/prompts/parallel-review.md +0 -54
- package/packages/subagents/prompts/review-loop.md +0 -43
- package/packages/subagents/skills/council-mode/SKILL.md +0 -230
- package/packages/subagents/skills/subagents/references/constraints-and-recipes.md +0 -259
- package/packages/subagents/skills/subagents/references/execution-controls.md +0 -454
- package/packages/subagents/skills/subagents/references/management-authoring-rpc.md +0 -161
- package/packages/subagents/skills/subagents/references/multi-lane-orchestration.md +0 -39
- package/packages/subagents/skills/subagents/references/prompting-and-roles.md +0 -267
- package/packages/subagents/src/acceptance.ts +0 -331
- package/packages/subagents/src/agent-manager.ts +0 -405
- package/packages/subagents/src/agent-memory.ts +0 -300
- package/packages/subagents/src/agent-overrides.ts +0 -183
- package/packages/subagents/src/agent-runner.ts +0 -271
- package/packages/subagents/src/async-runner.ts +0 -422
- package/packages/subagents/src/authority-policy.ts +0 -47
- package/packages/subagents/src/budgets.ts +0 -285
- package/packages/subagents/src/child-safety.ts +0 -140
- package/packages/subagents/src/config.ts +0 -249
- package/packages/subagents/src/conversation-viewer.ts +0 -281
- package/packages/subagents/src/core-compat.ts +0 -94
- package/packages/subagents/src/custom-agents.ts +0 -343
- package/packages/subagents/src/file-system-retry.ts +0 -50
- package/packages/subagents/src/fleet-data.ts +0 -105
- package/packages/subagents/src/fleet-view.ts +0 -322
- package/packages/subagents/src/foreground-detach.ts +0 -59
- package/packages/subagents/src/fork-context.ts +0 -285
- package/packages/subagents/src/global.d.ts +0 -7
- package/packages/subagents/src/guide.ts +0 -129
- package/packages/subagents/src/mission-state.ts +0 -133
- package/packages/subagents/src/mission-store.ts +0 -436
- package/packages/subagents/src/output-limits.ts +0 -142
- package/packages/subagents/src/parity-types.ts +0 -315
- package/packages/subagents/src/pi-args.ts +0 -167
- package/packages/subagents/src/result-files.ts +0 -232
- package/packages/subagents/src/result-watcher.ts +0 -221
- package/packages/subagents/src/retained-children.ts +0 -176
- package/packages/subagents/src/run-fanout-budget.ts +0 -356
- package/packages/subagents/src/scheduled-runs.ts +0 -349
- package/packages/subagents/src/schemas.ts +0 -263
- package/packages/subagents/src/slash-commands.ts +0 -80
- package/packages/subagents/src/supervisor-channel.ts +0 -249
- package/packages/subagents/src/tool-handler.ts +0 -1144
- package/packages/subagents/src/types.ts +0 -238
- package/packages/subagents/src/widget.ts +0 -457
- package/packages/subagents/src/workflow-script.ts +0 -737
- package/packages/subagents/src/workflow-worker.ts +0 -384
- package/packages/subagents/src/worktree.ts +0 -614
- package/packages/utility/src/analytics/collector.ts +0 -293
- package/packages/utility/src/info-screen.ts +0 -80
- package/packages/utility/src/lifecycle/process.ts +0 -162
- package/packages/utility/src/prefix-cache.ts +0 -263
- package/packages/utility/src/skill-discovery.ts +0 -542
- package/packages/utility/src/tools/env.ts +0 -113
- package/packages/utility/src/tui/name-badge-state.ts +0 -315
- package/packages/utility/src/tui/name-badge.ts +0 -109
- /package/packages/{subagents/src → core/src/child-agent}/pi-spawn.ts +0 -0
|
@@ -1,1153 +1,678 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @pi-unipi/subagents —
|
|
2
|
+
* @pi-unipi/subagents — Devin-model subagents for unipi.
|
|
3
3
|
*
|
|
4
|
-
* Tools:
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* Tools: `run_subagent` (foreground wait / background notify / resume) and a
|
|
5
|
+
* reader registration for the shared core `read_subagent` tool.
|
|
6
|
+
* Profiles: built-ins `subagent_explore` / `subagent_general` plus custom
|
|
7
|
+
* markdown agents from ~/.unipi/config/agents and <workspace>/.unipi/config/agents.
|
|
8
|
+
* Config `subagents`: enabled, defaultModel, defaultThinking, maxConcurrent —
|
|
9
|
+
* re-read every turn, so settings and agent files apply without a restart.
|
|
10
|
+
*
|
|
11
|
+
* TUI (Devin parity): spawn card with a live tail, `Subagent "…" completed`
|
|
12
|
+
* lines, the `N subagents (k running) · ↓ select` strip, the dock (↵ view,
|
|
13
|
+
* f foreground, x cancel), Ctrl+B to background a foreground subagent.
|
|
7
14
|
*/
|
|
8
15
|
|
|
9
|
-
import {
|
|
10
|
-
import { Text } from "@earendil-works/pi-tui";
|
|
16
|
+
import { type ExtensionAPI, type ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
11
17
|
import { Type } from "typebox";
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
import { homedir } from "node:os";
|
|
15
|
-
import { MODULES, UNIPI_EVENTS, emitEvent, type UnipiBadgeGenerateRequestEvent } from "@pi-unipi/core";
|
|
16
|
-
import { boundHelperOutput, withHerdrBlocked } from "./core-compat.js";
|
|
17
|
-
import { AgentManager } from "./agent-manager.js";
|
|
18
|
-
import { initConfig, loadRawGlobalConfig, loadRawWorkspaceConfig } from "./config.js";
|
|
19
|
-
import { type AgentActivity, type NotificationDetails, BUILTIN_TYPES } from "./types.js";
|
|
20
|
-
import { loadBuiltinFileAgents } from "./custom-agents.js";
|
|
21
|
-
import { ConversationViewer } from "./conversation-viewer.js";
|
|
22
|
-
import { AgentWidget, SPINNER, TOOL_DISPLAY, formatMs, formatTurns, describeActivity } from "./widget.js";
|
|
23
|
-
import { handleSpawnHelper, type HandlerDeps } from "./tool-handler.js";
|
|
24
|
-
import { SpawnHelperParams, GetHelperResultParams } from "./schemas.js";
|
|
25
|
-
import { runAsyncSubagent, createAsyncRunDir, writeStatus, readStatus } from "./async-runner.js";
|
|
26
|
-
import { createResultWatcher, cleanupAsyncRetention } from "./result-watcher.js";
|
|
27
|
-
import { writeAsyncResultFile, readAsyncResultFile } from "./result-files.js";
|
|
28
|
-
import { writePendingSubscription } from "./result-watcher.js";
|
|
29
|
-
import { RESULTS_DIR, ASYNC_DIR, ensureDirs } from "./parity-types.js";
|
|
30
|
-
import { createForkContextResolver } from "./fork-context.js";
|
|
31
|
-
import { createWorktrees, cleanupWorktrees, diffWorktrees, type WorktreeSetup } from "./worktree.js";
|
|
32
|
-
import { FleetView } from "./fleet-view.js";
|
|
33
|
-
import { registerSlashCommands } from "./slash-commands.js";
|
|
18
|
+
import { Key, matchesKey, truncateToWidth, type TUI } from "@earendil-works/pi-tui";
|
|
19
|
+
import { renderDelegatedStep, type DelegatedStep } from "@pi-unipi/utility/src/render/delegated.js";
|
|
34
20
|
import {
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
21
|
+
UNIPI_EVENTS, emitEvent, getSettings, isChildProcess, registerSettings, registerWaitSource, getSharedFusionStatus, SPINNER_MS,
|
|
22
|
+
} from "@pi-unipi/core";
|
|
23
|
+
import {
|
|
24
|
+
ensureReadSubagentTool, registerSubagentReader, setReadSubagentDemand,
|
|
25
|
+
createCompletionDelivery, type HandoffReport, type SidekickStep,
|
|
26
|
+
} from "@pi-unipi/core/child-agent.js";
|
|
27
|
+
import {
|
|
28
|
+
SubagentManager, backgroundRunningReason, canSpawn, getSharedSubagents, subscribeSubagents, recordStatusFor,
|
|
29
|
+
MAX_CONCURRENT, type SubagentRecord, type SubagentRun, type SubagentStatus,
|
|
30
|
+
} from "./manager.js";
|
|
31
|
+
import { loadProfiles, type AgentProfile } from "./profiles.js";
|
|
32
|
+
import { buildTranscript, itemsFromEvents } from "./transcript.js";
|
|
33
|
+
import {
|
|
34
|
+
SubagentDock, SubagentStrip, elapsed, plural, profileLabel, statusColor, statusGlyph, statLine, tailLines, STATUS_LABEL, type ThemeLike,
|
|
35
|
+
} from "./ui.js";
|
|
36
|
+
import { AGENTS_COMMAND, registerAgentsCommand } from "./agents.js";
|
|
37
|
+
import {
|
|
38
|
+
cardOutcome, renderCompletion, renderRunCall, renderRunResult, type CardContext, type CardDetails, type RunArgs,
|
|
39
|
+
} from "./cards.js";
|
|
40
|
+
|
|
41
|
+
export interface SubagentsConfig {
|
|
42
|
+
enabled: boolean;
|
|
43
|
+
defaultModel?: string;
|
|
44
|
+
defaultThinking?: string;
|
|
45
|
+
maxConcurrent?: number;
|
|
45
46
|
}
|
|
46
47
|
|
|
47
|
-
|
|
48
|
+
const DEFAULT_CONFIG: SubagentsConfig = { enabled: true, defaultThinking: "inherit", maxConcurrent: MAX_CONCURRENT };
|
|
49
|
+
const AUTO_MODEL = "auto (Fusion sidekick → your model)";
|
|
48
50
|
|
|
51
|
+
registerSettings({
|
|
52
|
+
namespace: "subagents",
|
|
53
|
+
label: "Subagents",
|
|
54
|
+
defaults: DEFAULT_CONFIG as unknown as Record<string, unknown>,
|
|
55
|
+
schema: [
|
|
56
|
+
{
|
|
57
|
+
title: "Subagents",
|
|
58
|
+
fields: [
|
|
59
|
+
{ key: "enabled", type: "boolean", label: "Enabled", description: "Offer run_subagent / read_subagent to the agent. Applies from the next turn." },
|
|
60
|
+
{
|
|
61
|
+
key: "defaultModel", type: "model", label: "Default subagent model", capability: "text",
|
|
62
|
+
description: "Model for subagent_explore and custom agents without a model: line. subagent_general always runs on your model.",
|
|
63
|
+
emptyLabel: AUTO_MODEL, emptyOption: AUTO_MODEL,
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
key: "defaultThinking", type: "enum", label: "Default thinking level",
|
|
67
|
+
description: "For subagent_explore and custom agents without thinking:. subagent_general uses yours.",
|
|
68
|
+
options: [{ value: "inherit", label: "inherit (your level)" }, "off", "minimal", "low", "medium", "high", "xhigh"],
|
|
69
|
+
},
|
|
70
|
+
{ key: "maxConcurrent", type: "number", label: "Max running at once", min: 1, max: 16, description: "Further run_subagent calls are refused until one finishes." },
|
|
71
|
+
],
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
title: "Agents",
|
|
75
|
+
fields: [
|
|
76
|
+
{ key: "manageAgents", type: "action", label: "Manage agents…", description: "List, create, edit, copy or delete custom agents (global or this project).", command: AGENTS_COMMAND },
|
|
77
|
+
],
|
|
78
|
+
},
|
|
79
|
+
],
|
|
80
|
+
});
|
|
49
81
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
function formatTokens(count: number): string {
|
|
53
|
-
if (count >= 1_000_000) return `${(count / 1_000_000).toFixed(1)}M token`;
|
|
54
|
-
if (count >= 1_000) return `${(count / 1_000).toFixed(1)}k token`;
|
|
55
|
-
return `${count} token`;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
/** Format tokens safely from session. */
|
|
59
|
-
function safeFormatTokens(session: any): string {
|
|
60
|
-
if (!session) return "";
|
|
61
|
-
try {
|
|
62
|
-
const stats = session.getSessionStats();
|
|
63
|
-
const total = stats.tokens?.total ?? 0;
|
|
64
|
-
return formatTokens(total);
|
|
65
|
-
} catch {
|
|
66
|
-
return "";
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
/** Get raw token count from session. */
|
|
71
|
-
function safeTokenCount(session: any): number {
|
|
72
|
-
if (!session) return 0;
|
|
82
|
+
export function loadConfig(cwd: string): SubagentsConfig {
|
|
73
83
|
try {
|
|
74
|
-
|
|
84
|
+
const m = getSettings("subagents", cwd) as unknown as SubagentsConfig;
|
|
85
|
+
const n = Number(m.maxConcurrent);
|
|
86
|
+
return {
|
|
87
|
+
enabled: m.enabled !== false,
|
|
88
|
+
defaultModel: typeof m.defaultModel === "string" && m.defaultModel.trim() ? m.defaultModel.trim() : undefined,
|
|
89
|
+
defaultThinking: typeof m.defaultThinking === "string" && m.defaultThinking !== "inherit" && m.defaultThinking ? m.defaultThinking : undefined,
|
|
90
|
+
maxConcurrent: Number.isFinite(n) && n >= 1 ? Math.floor(n) : MAX_CONCURRENT,
|
|
91
|
+
};
|
|
75
92
|
} catch {
|
|
76
|
-
return
|
|
93
|
+
return { enabled: true, maxConcurrent: MAX_CONCURRENT };
|
|
77
94
|
}
|
|
78
95
|
}
|
|
79
96
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
}
|
|
97
|
+
const RunSubagentParams = Type.Object({
|
|
98
|
+
title: Type.String({ description: "Short 3-8 word title shown to the user" }),
|
|
99
|
+
task: Type.String({
|
|
100
|
+
description: "Complete, self-contained instructions. The subagent does not see this conversation: include the goal, relevant paths, constraints, and what to report back.",
|
|
101
|
+
}),
|
|
102
|
+
profile: Type.String({ description: "" }), // rebuilt at load: profile list
|
|
103
|
+
is_background: Type.Optional(Type.Boolean({ description: "Run without waiting; report arrives via notification (default false)" })),
|
|
104
|
+
resume: Type.Optional(Type.String({ description: "agent_id of an earlier subagent to continue (keeps its context); always runs in the foreground" })),
|
|
105
|
+
});
|
|
89
106
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
switch (status) {
|
|
93
|
-
case "error": return `Error: ${error ?? "unknown"}`;
|
|
94
|
-
case "aborted": return "Aborted (max turns exceeded)";
|
|
95
|
-
case "stopped": return "Stopped";
|
|
96
|
-
default: return "Done";
|
|
97
|
-
}
|
|
98
|
-
}
|
|
107
|
+
const RUN_DESCRIPTION =
|
|
108
|
+
"Launch an independent subagent for a self-contained task. It has its own context and does not see this conversation, so put everything it needs in `task`. Foreground (default) waits and returns the subagent's report. With is_background:true it returns immediately and you receive a <subagent_completion_notification> when it finishes; use read_subagent to wait for it — never poll in a loop. Background subagents cannot ask for approval: tool calls that would need approval are denied. Use resume:<agent_id> to continue an earlier subagent with a follow-up task.";
|
|
99
109
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
if (!config.enabled) return;
|
|
110
|
+
const STRIP_KEY = "subagents-strip";
|
|
111
|
+
const FG_KEY = "subagents-foreground";
|
|
112
|
+
const WORKING = "Subagent running · Ctrl+B to run in background";
|
|
104
113
|
|
|
105
|
-
// Compute paths at factory time
|
|
106
|
-
const homeDir = homedir();
|
|
107
|
-
const cwd = process.cwd();
|
|
108
|
-
const globalAgentsDir = join(homeDir, ".unipi", "config", "agents");
|
|
109
|
-
const workspaceAgentsDir = join(cwd, ".unipi", "config", "agents");
|
|
110
114
|
|
|
111
|
-
|
|
112
|
-
const
|
|
115
|
+
function leadSessionId(ctx: ExtensionContext): string {
|
|
116
|
+
const m = ctx.sessionManager as { getSessionId?: () => string | undefined } | undefined;
|
|
117
|
+
return m?.getSessionId?.() ?? "default";
|
|
118
|
+
}
|
|
113
119
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
* "new" / "fork" / "resume"), and pi re-invokes the extension factory for
|
|
131
|
-
* the replacement session. A fresh closure therefore starts with
|
|
132
|
-
* `sessionEnded = false`, so the guard can never latch permanently.
|
|
133
|
-
* Verified: `/new` emits `shutdown reason=new` then re-runs the factory.
|
|
134
|
-
*
|
|
135
|
-
* `pi.events` is NOT gated, so cross-module events still fire.
|
|
136
|
-
*/
|
|
137
|
-
let sessionEnded = false;
|
|
120
|
+
/** Model order: profile.model → (general: parent) → config.defaultModel →
|
|
121
|
+
* Fusion sidekickKey → parent. Exported for tests. */
|
|
122
|
+
export function resolveSubagentModel(
|
|
123
|
+
profile: AgentProfile,
|
|
124
|
+
ctx: Pick<ExtensionContext, "model">,
|
|
125
|
+
config: SubagentsConfig,
|
|
126
|
+
): string {
|
|
127
|
+
if (profile.model !== undefined) return profile.model;
|
|
128
|
+
const cur = ctx.model as { provider?: string; id?: string } | undefined;
|
|
129
|
+
const parent = cur ? `${String(cur.provider)}/${String(cur.id)}` : "";
|
|
130
|
+
if (profile.id === "subagent_general") return parent;
|
|
131
|
+
if (config.defaultModel !== undefined) return config.defaultModel;
|
|
132
|
+
const sidekickKey = getSharedFusionStatus()?.sidekickKey;
|
|
133
|
+
if (sidekickKey !== undefined) return sidekickKey;
|
|
134
|
+
return parent;
|
|
135
|
+
}
|
|
138
136
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
137
|
+
/** subagent_general rides the parent's level; defaultThinking covers
|
|
138
|
+
* explore/custom agents without their own `thinking`. Exported for tests. */
|
|
139
|
+
export function resolveSubagentThinking(
|
|
140
|
+
profile: AgentProfile,
|
|
141
|
+
ctx: Pick<ExtensionContext, "thinkingLevel">,
|
|
142
|
+
config: SubagentsConfig,
|
|
143
|
+
): string {
|
|
144
|
+
const own = profile.id === "subagent_general" ? undefined : config.defaultThinking;
|
|
145
|
+
return profile.thinking ?? own ?? ctx.thinkingLevel ?? "medium";
|
|
146
|
+
}
|
|
143
147
|
|
|
144
|
-
|
|
145
|
-
// is deliverable, and touching `pi` would throw.
|
|
146
|
-
if (sessionEnded) return;
|
|
148
|
+
export { cardOutcome };
|
|
147
149
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
+
/**
|
|
151
|
+
* UI-only completed-step entry: payload + guarded append, identical for
|
|
152
|
+
* foreground and background runs. The final report is intentionally never
|
|
153
|
+
* streamed here — the lead reports it itself. Exported for tests.
|
|
154
|
+
*/
|
|
155
|
+
export function appendSubagentStep(
|
|
156
|
+
appendEntry: (type: string, data: Record<string, unknown>) => void,
|
|
157
|
+
record: Pick<SubagentRecord, "id" | "title" | "profile" | "startedAt">,
|
|
158
|
+
step: SidekickStep,
|
|
159
|
+
): void {
|
|
160
|
+
try {
|
|
161
|
+
appendEntry("subagent-step", {
|
|
162
|
+
...step,
|
|
163
|
+
agentId: record.id,
|
|
164
|
+
id: record.id,
|
|
165
|
+
title: record.title,
|
|
166
|
+
profile: record.profile,
|
|
167
|
+
group: `subagent:${record.id}:${record.startedAt}`,
|
|
168
|
+
label: `${profileLabel(record.profile)} subagent · ${record.title}`,
|
|
169
|
+
});
|
|
170
|
+
} catch {
|
|
171
|
+
/* entry rendering never affects the run */
|
|
172
|
+
}
|
|
173
|
+
}
|
|
150
174
|
|
|
151
|
-
|
|
152
|
-
|
|
175
|
+
export default function subagents(pi: ExtensionAPI, deps?: { manager?: SubagentManager }): void {
|
|
176
|
+
const manager = deps?.manager ?? new SubagentManager();
|
|
177
|
+
const delivery = createCompletionDelivery<HandoffReport>((report) => deliverCompletion(report));
|
|
153
178
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
record.resultConsumed = true;
|
|
165
|
-
}
|
|
179
|
+
// Turn-arbiter wait source (lead only): a background subagent is still
|
|
180
|
+
// working while the lead settles — its completion will arrive as an event
|
|
181
|
+
// turn, so a nudge now would race it.
|
|
182
|
+
if (!isChildProcess()) {
|
|
183
|
+
try {
|
|
184
|
+
registerWaitSource("subagents", () => backgroundRunningReason(getSharedSubagents()));
|
|
185
|
+
} catch {
|
|
186
|
+
// Registration must never block module load.
|
|
187
|
+
}
|
|
188
|
+
}
|
|
166
189
|
|
|
190
|
+
let profiles: AgentProfile[] = [];
|
|
191
|
+
let config: SubagentsConfig = { ...DEFAULT_CONFIG };
|
|
192
|
+
let enabled = false;
|
|
193
|
+
let uiCtx: ExtensionContext | undefined;
|
|
194
|
+
const warned = new Set<string>();
|
|
195
|
+
|
|
196
|
+
// UI state
|
|
197
|
+
let stripTui: TUI | undefined;
|
|
198
|
+
let stripInstalled = false;
|
|
199
|
+
let fgInstalled = false;
|
|
200
|
+
let dockOpen = false;
|
|
201
|
+
let workingShown = false;
|
|
202
|
+
let unsubInput: (() => void) | undefined;
|
|
203
|
+
/** Foreground waiters (run_subagent / read_subagent block) → move to bg. */
|
|
204
|
+
const fgWaits = new Map<string, () => void>();
|
|
205
|
+
/** Background agents the user foregrounded from the dock (f). */
|
|
206
|
+
const watched = new Set<string>();
|
|
207
|
+
|
|
208
|
+
function deliverCompletion(report: HandoffReport): void {
|
|
209
|
+
const rec = manager.record(report.id);
|
|
210
|
+
if (rec?.cancelledBy === "session") return; // the session is gone
|
|
211
|
+
const status = rec?.status ?? recordStatusFor(report);
|
|
212
|
+
const head = status === "cancelled"
|
|
213
|
+
? rec?.cancelledBy === "user" ? "Cancelled by the user." : "Cancelled."
|
|
214
|
+
: status === "failed" ? `Failed${report.error ? `: ${report.error}` : "."}` : "";
|
|
215
|
+
const body = [head, report.text].filter((s) => s && s.trim()).join("\n\n");
|
|
216
|
+
try {
|
|
217
|
+
pi.sendMessage(
|
|
218
|
+
{
|
|
219
|
+
customType: "subagent-completion",
|
|
220
|
+
content: `<subagent_completion_notification agent_id="${report.id}" status="${status}">\n${body}\n</subagent_completion_notification>`,
|
|
221
|
+
display: true,
|
|
222
|
+
details: {
|
|
223
|
+
owner: "subagents", id: report.id, title: rec?.title ?? report.id, profile: rec?.profile, model: rec?.model, status,
|
|
224
|
+
cancelledBy: rec?.cancelledBy, toolCalls: report.toolCalls, durationMs: report.durationMs, usage: report.usage, report: report.text.slice(0, 20_000),
|
|
225
|
+
},
|
|
226
|
+
} as never,
|
|
227
|
+
{ deliverAs: "followUp", triggerTurn: true } as never,
|
|
228
|
+
);
|
|
229
|
+
} catch {
|
|
230
|
+
/* session replaced while it finished */
|
|
231
|
+
}
|
|
232
|
+
}
|
|
167
233
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
234
|
+
/** Re-read config + agent files; returns the load warnings. Safe at load. */
|
|
235
|
+
function reload(cwd: string): string[] {
|
|
236
|
+
config = loadConfig(cwd);
|
|
237
|
+
enabled = config.enabled;
|
|
238
|
+
const loaded = loadProfiles(cwd);
|
|
239
|
+
profiles = loaded.profiles;
|
|
240
|
+
return loaded.warnings;
|
|
241
|
+
}
|
|
176
242
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
243
|
+
/** reload + tool sync + warnings (runtime only — not during loading). */
|
|
244
|
+
function refresh(cwd: string, ctx?: ExtensionContext): void {
|
|
245
|
+
// Warnings surface once per distinct message per process.
|
|
246
|
+
for (const w of reload(cwd)) {
|
|
247
|
+
if (warned.has(w)) continue;
|
|
248
|
+
warned.add(w);
|
|
249
|
+
(ctx ?? uiCtx)?.ui?.notify?.(`subagents: ${w}`, "warning");
|
|
250
|
+
}
|
|
251
|
+
setReadSubagentDemand(pi, "subagents", enabled);
|
|
252
|
+
syncRunSubagent();
|
|
253
|
+
}
|
|
186
254
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
{
|
|
194
|
-
customType: "subagent-notification",
|
|
195
|
-
content: notificationXml,
|
|
196
|
-
display: true,
|
|
197
|
-
details,
|
|
198
|
-
},
|
|
199
|
-
{ deliverAs: "followUp", triggerTurn: true },
|
|
200
|
-
);
|
|
201
|
-
} catch {
|
|
202
|
-
// Runtime went stale between the guard and here — nothing to notify.
|
|
203
|
-
}
|
|
204
|
-
}
|
|
255
|
+
function syncRunSubagent(): void {
|
|
256
|
+
const want = enabled && canSpawn();
|
|
257
|
+
const current = pi.getActiveTools();
|
|
258
|
+
const has = current.includes("run_subagent");
|
|
259
|
+
if (has !== want) pi.setActiveTools(want ? [...current, "run_subagent"] : current.filter((t) => t !== "run_subagent"));
|
|
260
|
+
}
|
|
205
261
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
description: record.description,
|
|
210
|
-
status: record.status,
|
|
211
|
-
result: record.result,
|
|
212
|
-
error: record.error,
|
|
213
|
-
});
|
|
214
|
-
},
|
|
215
|
-
config.maxConcurrent,
|
|
216
|
-
(record) => {
|
|
217
|
-
pi.events.emit("subagents:started", {
|
|
218
|
-
id: record.id,
|
|
219
|
-
type: record.type,
|
|
220
|
-
description: record.description,
|
|
221
|
-
});
|
|
222
|
-
},
|
|
223
|
-
config.types,
|
|
224
|
-
process.cwd(),
|
|
225
|
-
{
|
|
226
|
-
user: loadRawGlobalConfig()?.subagents,
|
|
227
|
-
project: loadRawWorkspaceConfig(process.cwd())?.subagents,
|
|
228
|
-
},
|
|
229
|
-
);
|
|
262
|
+
// Profiles load before registration so the tool schema's profile list is
|
|
263
|
+
// complete; later additions reach the model through the prompt section.
|
|
264
|
+
reload(process.cwd());
|
|
230
265
|
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
const
|
|
234
|
-
const asyncSessionId = `unipi-${process.pid}`;
|
|
266
|
+
const transcriptOf = (rec: SubagentRecord) =>
|
|
267
|
+
buildTranscript({ sessionFile: rec.sessionFile, task: rec.task, running: rec.status === "running", events: manager.events(rec.id) });
|
|
268
|
+
const liveItems = (id: string) => itemsFromEvents(manager.events(id) ?? []);
|
|
235
269
|
|
|
236
|
-
|
|
237
|
-
const agent = manager.getAgentConfig(manager.resolveAlias(launch.agentName));
|
|
238
|
-
if (!agent) throw new Error(`Unknown agent "${launch.agentName}".`);
|
|
239
|
-
if (launch.resumeSessionFile && !existsSync(launch.resumeSessionFile)) {
|
|
240
|
-
throw new Error(`Resume session file is missing: ${launch.resumeSessionFile}`);
|
|
241
|
-
}
|
|
270
|
+
// ── UI sync ───────────────────────────────────────────────────────────────
|
|
242
271
|
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
worktreeSetup = createWorktrees(process.cwd(), `async-${Date.now().toString(36)}`, 1);
|
|
250
|
-
childCwd = worktreeSetup.worktrees[0]!.agentCwd;
|
|
251
|
-
} catch (worktreeError) {
|
|
252
|
-
throw new Error(
|
|
253
|
-
`Worktree isolation failed: ${worktreeError instanceof Error ? worktreeError.message : String(worktreeError)}`,
|
|
254
|
-
);
|
|
272
|
+
function syncUi(): void {
|
|
273
|
+
const ctx = uiCtx;
|
|
274
|
+
if (!ctx?.hasUI) return;
|
|
275
|
+
try {
|
|
276
|
+
for (const id of watched) {
|
|
277
|
+
if (manager.run(id) === undefined) watched.delete(id);
|
|
255
278
|
}
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
sessionManager?: {
|
|
270
|
-
getSessionFile(): string | undefined;
|
|
271
|
-
getLeafId(): string | null;
|
|
272
|
-
openSession?: Parameters<typeof createForkContextResolver>[0]["openSession"];
|
|
273
|
-
};
|
|
274
|
-
})?.sessionManager;
|
|
275
|
-
if (!sessionManager) {
|
|
276
|
-
throw new Error("Forked context requires a persisted parent session (session manager unavailable).");
|
|
277
|
-
}
|
|
278
|
-
const resolver = createForkContextResolver(sessionManager, "fork");
|
|
279
|
-
forkSessionFile = resolver.sessionFileForIndex(0);
|
|
280
|
-
forceThinkingOff = resolver.thinkingOverrideForIndex(0) === "off";
|
|
281
|
-
if (!forkSessionFile) {
|
|
282
|
-
throw new Error("Forked context failed to produce a branched session file.");
|
|
283
|
-
}
|
|
284
|
-
writeStatus(runDir, { context: "fork", forkSessionFile });
|
|
285
|
-
} catch (forkError) {
|
|
286
|
-
// Reference rule: explicit fork never silently downgrades.
|
|
287
|
-
activeAsyncRuns.delete(runId);
|
|
288
|
-
throw forkError;
|
|
279
|
+
const wantStrip = getSharedSubagents().length > 0;
|
|
280
|
+
if (wantStrip && !stripInstalled) {
|
|
281
|
+
stripInstalled = true;
|
|
282
|
+
ctx.ui.setWidget(STRIP_KEY, (tui, theme) => {
|
|
283
|
+
stripTui = tui;
|
|
284
|
+
return new SubagentStrip(tui, theme, getSharedSubagents, (rec) => ({ toolCalls: manager.toolCalls(rec.id), usage: manager.usage(rec.id) }));
|
|
285
|
+
}, { placement: "belowEditor" });
|
|
286
|
+
} else if (!wantStrip && stripInstalled) {
|
|
287
|
+
stripInstalled = false;
|
|
288
|
+
stripTui = undefined;
|
|
289
|
+
ctx.ui.setWidget(STRIP_KEY, undefined);
|
|
290
|
+
} else {
|
|
291
|
+
stripTui?.requestRender();
|
|
289
292
|
}
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
model: launch.model,
|
|
302
|
-
thinking: launch.thinking,
|
|
303
|
-
tools: agent.builtinToolNames,
|
|
304
|
-
extensions: childExtensionsArg(agent.extensions),
|
|
305
|
-
timeoutMs: launch.timeoutMs,
|
|
306
|
-
parentSessionId: asyncSessionId,
|
|
307
|
-
config,
|
|
308
|
-
...(launch.resumeSessionFile ? { sessionFile: launch.resumeSessionFile } : {}),
|
|
309
|
-
...(forkSessionFile ? { forkSessionFile } : {}),
|
|
310
|
-
...(forceThinkingOff ? { forceThinkingOff } : {}),
|
|
311
|
-
},
|
|
312
|
-
runDir,
|
|
313
|
-
controller.signal,
|
|
314
|
-
);
|
|
315
|
-
writeAsyncResultFile(RESULTS_DIR, {
|
|
316
|
-
runId,
|
|
317
|
-
sessionId: asyncSessionId,
|
|
318
|
-
...(result.output !== undefined ? { output: result.output } : {}),
|
|
319
|
-
...(result.error ? { error: result.error } : {}),
|
|
320
|
-
success: result.status === "completed",
|
|
321
|
-
state: result.status,
|
|
322
|
-
timedOut: result.status === "timedOut",
|
|
323
|
-
durationMs: result.durationMs,
|
|
324
|
-
}, { asyncDir: runDir });
|
|
325
|
-
} catch (error) {
|
|
326
|
-
writeStatus(runDir, {
|
|
327
|
-
status: "failed",
|
|
328
|
-
error: error instanceof Error ? error.message : String(error),
|
|
329
|
-
});
|
|
330
|
-
writeAsyncResultFile(RESULTS_DIR, {
|
|
331
|
-
runId,
|
|
332
|
-
sessionId: asyncSessionId,
|
|
333
|
-
error: error instanceof Error ? error.message : String(error),
|
|
334
|
-
success: false,
|
|
335
|
-
state: "failed",
|
|
336
|
-
}, { asyncDir: runDir });
|
|
337
|
-
} finally {
|
|
338
|
-
activeAsyncRuns.delete(runId);
|
|
339
|
-
if (worktreeSetup) {
|
|
340
|
-
try {
|
|
341
|
-
const diffs = diffWorktrees(worktreeSetup, [launch.agentName], runDir);
|
|
342
|
-
writeFileSync(
|
|
343
|
-
join(runDir, "handoff.json"),
|
|
344
|
-
JSON.stringify({ patches: diffs.map((d) => d.patchPath) }),
|
|
345
|
-
{ mode: 0o600 },
|
|
346
|
-
);
|
|
347
|
-
cleanupWorktrees(worktreeSetup, {
|
|
348
|
-
kind: "preserve",
|
|
349
|
-
capturedDiffs: diffs,
|
|
350
|
-
handoffManifestPath: join(runDir, "handoff.json"),
|
|
351
|
-
});
|
|
352
|
-
} catch {
|
|
353
|
-
// Worktree cleanup is best-effort; preserved trees surface in the report.
|
|
354
|
-
}
|
|
355
|
-
}
|
|
293
|
+
const wantFg = watched.size > 0;
|
|
294
|
+
if (wantFg && !fgInstalled) {
|
|
295
|
+
fgInstalled = true;
|
|
296
|
+
ctx.ui.setWidget(FG_KEY, (tui, theme) => {
|
|
297
|
+
const timer = setInterval(() => tui.requestRender(), SPINNER_MS);
|
|
298
|
+
timer.unref?.();
|
|
299
|
+
return { invalidate() {}, render: (w: number) => renderWatched(w, theme), dispose: () => clearInterval(timer) };
|
|
300
|
+
}, { placement: "aboveEditor" });
|
|
301
|
+
} else if (!wantFg && fgInstalled) {
|
|
302
|
+
fgInstalled = false;
|
|
303
|
+
ctx.ui.setWidget(FG_KEY, undefined);
|
|
356
304
|
}
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
// Result watcher: deliver async completions as follow-up notifications.
|
|
363
|
-
const watcher = createResultWatcher({
|
|
364
|
-
resultsDir: RESULTS_DIR,
|
|
365
|
-
sessionId: asyncSessionId,
|
|
366
|
-
resultScanLogging: config.resultScanLogging ?? "activity",
|
|
367
|
-
notifier: (notification) => {
|
|
368
|
-
if (sessionEnded) return;
|
|
369
|
-
try {
|
|
370
|
-
pi.sendMessage(
|
|
371
|
-
{
|
|
372
|
-
customType: "unipi-response",
|
|
373
|
-
content:
|
|
374
|
-
`<task-notification>\n` +
|
|
375
|
-
`<task-id>${notification.runId}</task-id>\n` +
|
|
376
|
-
`<status>${notification.success ? "completed" : notification.state ?? "failed"}</status>\n` +
|
|
377
|
-
`<summary>Background agent "${notification.agent ?? "agent"}" ${notification.state ?? (notification.success ? "completed" : "failed")}</summary>\n` +
|
|
378
|
-
(notification.error
|
|
379
|
-
? `<error>${notification.error}</error>\n`
|
|
380
|
-
: `<result>${(notification.output ?? "").slice(0, 2000)}</result>\n`) +
|
|
381
|
-
`</task-notification>`,
|
|
382
|
-
display: false,
|
|
383
|
-
},
|
|
384
|
-
{ deliverAs: "followUp", triggerTurn: true },
|
|
385
|
-
);
|
|
386
|
-
} catch {
|
|
387
|
-
// Runtime went stale — nothing to notify.
|
|
305
|
+
const wantWorking = fgWaits.size > 0 || watched.size > 0;
|
|
306
|
+
if (wantWorking !== workingShown) {
|
|
307
|
+
workingShown = wantWorking;
|
|
308
|
+
ctx.ui.setWorkingMessage(wantWorking ? WORKING : undefined);
|
|
388
309
|
}
|
|
389
|
-
}
|
|
390
|
-
|
|
310
|
+
} catch {
|
|
311
|
+
/* UI is best-effort; the runs are unaffected */
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
const unsubRegistry = subscribeSubagents(syncUi);
|
|
315
|
+
|
|
316
|
+
function renderWatched(width: number, theme: ThemeLike): string[] {
|
|
317
|
+
const out: string[] = [];
|
|
318
|
+
for (const id of watched) {
|
|
319
|
+
const rec = manager.record(id);
|
|
320
|
+
if (rec === undefined) continue;
|
|
321
|
+
// Same Devin card as a foreground run_subagent.
|
|
322
|
+
const head = `${statusGlyph(rec.status, theme)} ${theme.bold(`${profileLabel(rec.profile)} subagent`)} ${rec.title}`;
|
|
323
|
+
const foot = theme.fg("dim", `└ ${statLine(rec, manager.toolCalls(id), manager.usage(id))} · ctrl+b background`);
|
|
324
|
+
out.push(truncateToWidth(head, width), ...tailLines(liveItems(id), width, theme, 4), truncateToWidth(` ${foot}`, width));
|
|
325
|
+
}
|
|
326
|
+
return out;
|
|
327
|
+
}
|
|
391
328
|
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
329
|
+
function foregroundAgent(id: string): string | undefined {
|
|
330
|
+
const run = manager.run(id);
|
|
331
|
+
if (run === undefined) return "Already finished — ask the agent to resume it.";
|
|
332
|
+
if (fgWaits.has(id) || watched.has(id)) return undefined;
|
|
333
|
+
if (uiCtx) run.runtime.attachUi(uiCtx.ui as never);
|
|
334
|
+
manager.setBackground(id, false);
|
|
335
|
+
watched.add(id);
|
|
336
|
+
syncUi();
|
|
337
|
+
return undefined;
|
|
338
|
+
}
|
|
397
339
|
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
return result.runId;
|
|
414
|
-
},
|
|
415
|
-
});
|
|
416
|
-
manager.startPolling();
|
|
417
|
-
schedulePoller = manager;
|
|
418
|
-
}).catch(() => {});
|
|
340
|
+
/** Ctrl+B: every foreground subagent goes to the background. */
|
|
341
|
+
function backgroundAll(): boolean {
|
|
342
|
+
let any = false;
|
|
343
|
+
for (const move of [...fgWaits.values()]) {
|
|
344
|
+
move();
|
|
345
|
+
any = true;
|
|
346
|
+
}
|
|
347
|
+
for (const id of watched) {
|
|
348
|
+
manager.run(id)?.runtime.detachUi();
|
|
349
|
+
manager.setBackground(id, true);
|
|
350
|
+
any = true;
|
|
351
|
+
}
|
|
352
|
+
watched.clear();
|
|
353
|
+
syncUi();
|
|
354
|
+
return any;
|
|
419
355
|
}
|
|
420
|
-
|
|
421
|
-
|
|
356
|
+
|
|
357
|
+
function cancelAgent(id: string): string | undefined {
|
|
358
|
+
return manager.cancel(id, "user") ? undefined : "Not running.";
|
|
422
359
|
}
|
|
423
360
|
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
361
|
+
async function openDock(ctx: ExtensionContext, initialId?: string): Promise<void> {
|
|
362
|
+
if (dockOpen || !ctx.hasUI) return;
|
|
363
|
+
dockOpen = true;
|
|
364
|
+
try {
|
|
365
|
+
await ctx.ui.custom<void>((tui, theme, _kb, done) => new SubagentDock(tui, theme, {
|
|
366
|
+
records: getSharedSubagents,
|
|
367
|
+
transcript: transcriptOf,
|
|
368
|
+
toolCalls: (rec) => manager.toolCalls(rec.id),
|
|
369
|
+
usage: (rec) => manager.usage(rec.id),
|
|
370
|
+
subscribe: subscribeSubagents,
|
|
371
|
+
foreground: foregroundAgent,
|
|
372
|
+
cancel: cancelAgent,
|
|
373
|
+
}, () => done(), initialId));
|
|
374
|
+
} finally {
|
|
375
|
+
dockOpen = false;
|
|
376
|
+
}
|
|
377
|
+
}
|
|
430
378
|
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
consume: (count) => { sessionSpawnsUsed += count; },
|
|
439
|
-
},
|
|
440
|
-
runAsync: runAsyncDep,
|
|
441
|
-
spawnBackground: (spawnCtx, agentName, childPrompt, options) => {
|
|
442
|
-
const { state: bgState, callbacks: bgCallbacks } = createActivityTracker(options.maxTurns);
|
|
443
|
-
const origOnSession = bgCallbacks.onSessionCreated;
|
|
444
|
-
bgCallbacks.onSessionCreated = (session: any) => {
|
|
445
|
-
origOnSession(session);
|
|
446
|
-
bgState.tokens = safeFormatTokens(session);
|
|
447
|
-
widget.update();
|
|
448
|
-
};
|
|
449
|
-
const id = manager.spawn(pi, spawnCtx, agentName, childPrompt, {
|
|
450
|
-
description: options.description ?? `${agentName} task`,
|
|
451
|
-
maxTurns: options.maxTurns,
|
|
452
|
-
modelInput: options.modelInput,
|
|
453
|
-
modelRegistry: spawnCtx.modelRegistry,
|
|
454
|
-
thinkingLevel: coerceThinkingLevel(options.thinkingLevel as never),
|
|
455
|
-
isBackground: true,
|
|
456
|
-
...bgCallbacks,
|
|
457
|
-
});
|
|
458
|
-
agentActivity.set(id, bgState);
|
|
459
|
-
widget.ensureTimer();
|
|
460
|
-
widget.update();
|
|
461
|
-
return id;
|
|
462
|
-
},
|
|
463
|
-
spawnForeground: async (spawnCtx, agentName, childPrompt, options) => {
|
|
464
|
-
// Stream progress via the widget — reuse the activity tracker.
|
|
465
|
-
let spinnerFrame = 0;
|
|
466
|
-
const startedAt = Date.now();
|
|
467
|
-
let fgId: string | undefined;
|
|
468
|
-
const { state: fgState, callbacks: fgCallbacks } = createActivityTracker(options.maxTurns);
|
|
379
|
+
/** The editor is focused, empty and not autocompleting (↓ may open the dock). */
|
|
380
|
+
function editorIdle(): boolean {
|
|
381
|
+
const tui = stripTui as { getFocusedComponent?: () => unknown } | undefined;
|
|
382
|
+
const f = tui?.getFocusedComponent?.() as { getText?: () => string; isShowingAutocomplete?: () => boolean } | null | undefined;
|
|
383
|
+
if (!f || typeof f.getText !== "function" || typeof f.isShowingAutocomplete !== "function") return false;
|
|
384
|
+
return f.getText() === "" && !f.isShowingAutocomplete();
|
|
385
|
+
}
|
|
469
386
|
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
activity: describeActivity(fgState.activeTools, fgState.responseText),
|
|
479
|
-
spinnerFrame: spinnerFrame % SPINNER.length,
|
|
480
|
-
});
|
|
481
|
-
};
|
|
482
|
-
const origOnSession = fgCallbacks.onSessionCreated;
|
|
483
|
-
fgCallbacks.onSessionCreated = (session: any) => {
|
|
484
|
-
origOnSession(session);
|
|
485
|
-
fgState.tokens = safeFormatTokens(session);
|
|
486
|
-
for (const a of manager.listAgents()) {
|
|
487
|
-
if (a.session === session) {
|
|
488
|
-
fgId = a.id;
|
|
489
|
-
agentActivity.set(a.id, fgState);
|
|
490
|
-
widget.ensureTimer();
|
|
491
|
-
break;
|
|
492
|
-
}
|
|
493
|
-
}
|
|
494
|
-
};
|
|
495
|
-
const spinnerInterval = setInterval(() => { spinnerFrame++; streamUpdate(); }, 80);
|
|
496
|
-
// Detach: stop waiting without killing the child. The completion
|
|
497
|
-
// notification arrives later via the normal onComplete path.
|
|
498
|
-
activeForegroundDetach = () => {
|
|
499
|
-
if (!fgId) return false;
|
|
500
|
-
const record = manager.getRecord(fgId);
|
|
501
|
-
if (!record || record.status !== "running") return false;
|
|
502
|
-
record.resultConsumed = true; // suppress the duplicate notification
|
|
503
|
-
return true;
|
|
504
|
-
};
|
|
505
|
-
try {
|
|
506
|
-
const record = await manager.spawnAndWait(pi, spawnCtx, agentName, childPrompt, {
|
|
507
|
-
description: options.description ?? `${agentName} task`,
|
|
508
|
-
maxTurns: options.maxTurns,
|
|
509
|
-
modelInput: options.modelInput,
|
|
510
|
-
modelRegistry: spawnCtx.modelRegistry,
|
|
511
|
-
thinkingLevel: coerceThinkingLevel(options.thinkingLevel as never),
|
|
512
|
-
...fgCallbacks,
|
|
513
|
-
});
|
|
514
|
-
const durationMs = (record.completedAt ?? Date.now()) - record.startedAt;
|
|
515
|
-
const tokenText = safeFormatTokens(fgState.session);
|
|
516
|
-
if (record.status === "error") {
|
|
517
|
-
return { ok: false, output: record.error ?? "failed", error: record.error, toolUses: record.toolUses, durationMs };
|
|
518
|
-
}
|
|
519
|
-
const output = boundHelperOutput(record.result?.trim() || "No output.");
|
|
520
|
-
record.resultArtifactPath = output.artifactPath;
|
|
521
|
-
return { ok: true, output: output.text, toolUses: record.toolUses, durationMs };
|
|
522
|
-
} finally {
|
|
523
|
-
clearInterval(spinnerInterval);
|
|
524
|
-
activeForegroundDetach = undefined;
|
|
525
|
-
if (fgId) {
|
|
526
|
-
agentActivity.delete(fgId);
|
|
527
|
-
widget.markFinished(fgId);
|
|
528
|
-
widget.update();
|
|
529
|
-
}
|
|
530
|
-
}
|
|
531
|
-
},
|
|
532
|
-
};
|
|
533
|
-
// onUpdate stream for the current foreground execution (set per execute() call)
|
|
534
|
-
let onUpdateForForeground: (details: Record<string, unknown>) => void = () => {};
|
|
387
|
+
function onTerminalInput(data: string): { consume?: boolean } | undefined {
|
|
388
|
+
if (matchesKey(data, "ctrl+b")) return backgroundAll() ? { consume: true } : undefined;
|
|
389
|
+
if (matchesKey(data, Key.down) && !dockOpen && stripInstalled && uiCtx && editorIdle()) {
|
|
390
|
+
void openDock(uiCtx);
|
|
391
|
+
return { consume: true };
|
|
392
|
+
}
|
|
393
|
+
return undefined;
|
|
394
|
+
}
|
|
535
395
|
|
|
536
|
-
//
|
|
537
|
-
|
|
538
|
-
|
|
396
|
+
// ── run_subagent ──────────────────────────────────────────────────────────
|
|
397
|
+
|
|
398
|
+
if (canSpawn()) {
|
|
399
|
+
const profileParam = profiles.map((p) => `${p.id} — ${p.description}`).join("; ");
|
|
400
|
+
pi.registerTool({
|
|
401
|
+
name: "run_subagent",
|
|
402
|
+
label: "Run Subagent",
|
|
403
|
+
description: RUN_DESCRIPTION,
|
|
404
|
+
parameters: Type.Object({
|
|
405
|
+
...RunSubagentParams.properties,
|
|
406
|
+
profile: Type.String({ description: `Profile name. Available: ${profileParam}. Custom agents added later are listed in the Subagents section of the system prompt.` }),
|
|
407
|
+
}),
|
|
408
|
+
renderShell: "self",
|
|
409
|
+
renderCall: (args: RunArgs, theme, context) => renderRunCall(args, theme, context as unknown as CardContext),
|
|
410
|
+
renderResult: (result, opts, theme, context) =>
|
|
411
|
+
renderRunResult(result as never, opts, theme, context as unknown as CardContext, (context as { args?: RunArgs }).args ?? {}, {
|
|
412
|
+
tail: (id, w) => tailLines(liveItems(id), w, theme, 4),
|
|
413
|
+
toolCalls: (id) => manager.toolCalls(id),
|
|
414
|
+
startedAt: (id) => manager.record(id)?.startedAt,
|
|
415
|
+
usage: (id) => manager.usage(id),
|
|
416
|
+
record: (id) => manager.record(id),
|
|
417
|
+
}),
|
|
418
|
+
execute: runSubagent as never,
|
|
419
|
+
// Simple render style: live stats in the row meta, `└ Completed …` under it.
|
|
420
|
+
...( {
|
|
421
|
+
simpleMeta: (details: CardDetails | undefined) => {
|
|
422
|
+
const rec = details?.id !== undefined ? manager.record(details.id) : undefined;
|
|
423
|
+
return rec === undefined ? undefined : ` · ${statLine(rec, manager.toolCalls(rec.id), manager.usage(rec.id))}`;
|
|
424
|
+
},
|
|
425
|
+
simpleResult: (result: { details?: CardDetails }, theme: ThemeLike) => [` ${cardOutcome(result.details, theme)}`],
|
|
426
|
+
} as object),
|
|
427
|
+
});
|
|
428
|
+
}
|
|
539
429
|
|
|
430
|
+
function textResult(text: string, details: CardDetails, isError = false) {
|
|
431
|
+
return { content: [{ type: "text" as const, text }], details, isError };
|
|
432
|
+
}
|
|
540
433
|
|
|
541
|
-
|
|
542
|
-
function buildNotificationDetails(record: any, activity?: AgentActivity): NotificationDetails {
|
|
434
|
+
function doneDetails(rec: SubagentRecord, report: HandoffReport): CardDetails {
|
|
543
435
|
return {
|
|
544
|
-
id:
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
toolUses: record.toolUses,
|
|
548
|
-
turnCount: activity?.turnCount ?? 0,
|
|
549
|
-
maxTurns: activity?.maxTurns,
|
|
550
|
-
totalTokens: safeTokenCount(record.session),
|
|
551
|
-
durationMs: record.completedAt ? record.completedAt - record.startedAt : 0,
|
|
552
|
-
error: record.error,
|
|
553
|
-
resultPreview: record.result
|
|
554
|
-
? record.result.length > 200
|
|
555
|
-
? record.result.slice(0, 200) + "…"
|
|
556
|
-
: record.result
|
|
557
|
-
: "No output.",
|
|
436
|
+
owner: "subagents", id: rec.id, title: rec.title, profile: rec.profile, model: rec.model, phase: "done",
|
|
437
|
+
status: rec.status === "running" ? recordStatusFor(report, rec.cancelledBy) : rec.status,
|
|
438
|
+
toolCalls: report.toolCalls, durationMs: report.durationMs, usage: report.usage, error: report.error, cancelledBy: rec.cancelledBy,
|
|
558
439
|
};
|
|
559
440
|
}
|
|
560
441
|
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
(message, { expanded }, theme) => {
|
|
565
|
-
const d = message.details;
|
|
566
|
-
if (!d) return undefined;
|
|
567
|
-
|
|
568
|
-
function renderOne(d: NotificationDetails): string {
|
|
569
|
-
const isError = d.status === "error" || d.status === "stopped" || d.status === "aborted";
|
|
570
|
-
const icon = isError ? theme.fg("error", "✗") : theme.fg("success", "✓");
|
|
571
|
-
const statusText = isError
|
|
572
|
-
? d.status
|
|
573
|
-
: d.status === "steered"
|
|
574
|
-
? "completed (steered)"
|
|
575
|
-
: "completed";
|
|
576
|
-
|
|
577
|
-
// Line 1: icon + agent description + status
|
|
578
|
-
let line = `${icon} ${theme.bold(d.description)} ${theme.fg("dim", statusText)}`;
|
|
579
|
-
|
|
580
|
-
// Line 2: stats
|
|
581
|
-
const parts: string[] = [];
|
|
582
|
-
if (d.turnCount > 0) parts.push(formatTurns(d.turnCount, d.maxTurns));
|
|
583
|
-
if (d.toolUses > 0) parts.push(`${d.toolUses} tool use${d.toolUses === 1 ? "" : "s"}`);
|
|
584
|
-
if (d.totalTokens > 0) parts.push(formatTokens(d.totalTokens));
|
|
585
|
-
if (d.durationMs > 0) parts.push(formatMs(d.durationMs));
|
|
586
|
-
if (parts.length) {
|
|
587
|
-
line += "\n " + parts.map((p) => theme.fg("dim", p)).join(" " + theme.fg("dim", "·") + " ");
|
|
588
|
-
}
|
|
589
|
-
|
|
590
|
-
// Line 3: result preview (collapsed) or full (expanded)
|
|
591
|
-
if (expanded) {
|
|
592
|
-
const lines = d.resultPreview.split("\n").slice(0, 30);
|
|
593
|
-
for (const l of lines) line += "\n" + theme.fg("dim", ` ${l}`);
|
|
594
|
-
} else {
|
|
595
|
-
const preview = d.resultPreview.split("\n")[0]?.slice(0, 80) ?? "";
|
|
596
|
-
line += "\n " + theme.fg("dim", `⎿ ${preview}`);
|
|
597
|
-
}
|
|
598
|
-
|
|
599
|
-
return line;
|
|
600
|
-
}
|
|
601
|
-
|
|
602
|
-
const all = [d, ...(d.others ?? [])];
|
|
603
|
-
return new Text(all.map(renderOne).join("\n"), 0, 0);
|
|
604
|
-
},
|
|
605
|
-
);
|
|
606
|
-
|
|
607
|
-
// Create widget
|
|
608
|
-
const widget = new AgentWidget(manager, agentActivity);
|
|
609
|
-
|
|
610
|
-
// ---- FleetView (persistent fleet panel; our AgentWidget slot system) ----
|
|
611
|
-
const fleetView = new FleetView(manager, agentActivity, ASYNC_DIR, {
|
|
612
|
-
placement: config.fleetView === false ? undefined : (config.fleetViewPlacement ?? "belowEditor"),
|
|
613
|
-
sessionId: asyncSessionId,
|
|
614
|
-
openInspector: async (entry) => {
|
|
615
|
-
if (!sessionCtx?.ui) return;
|
|
616
|
-
if (entry.source === "inprocess") {
|
|
617
|
-
const record = manager.getRecord(entry.key.replace("inprocess:", ""));
|
|
618
|
-
if (record?.session) {
|
|
619
|
-
await withHerdrBlocked(
|
|
620
|
-
pi,
|
|
621
|
-
"fleet inspector",
|
|
622
|
-
() => sessionCtx!.ui.custom<undefined>(
|
|
623
|
-
(tui, theme, _keybindings, done) =>
|
|
624
|
-
new ConversationViewer(tui, record.session!, {
|
|
625
|
-
type: record.type,
|
|
626
|
-
description: record.description,
|
|
627
|
-
status: record.status,
|
|
628
|
-
toolUses: record.toolUses,
|
|
629
|
-
startedAt: record.startedAt,
|
|
630
|
-
completedAt: record.completedAt,
|
|
631
|
-
}, agentActivity.get(record.id), theme, done),
|
|
632
|
-
{ overlay: true, overlayOptions: { anchor: "center", width: "90%" } },
|
|
633
|
-
),
|
|
634
|
-
);
|
|
635
|
-
}
|
|
636
|
-
return;
|
|
637
|
-
}
|
|
638
|
-
// Async run: show the transcript tail from the result payload / output.txt.
|
|
639
|
-
const runId = entry.key.replace("async:", "");
|
|
640
|
-
const payload = readAsyncResultFile(RESULTS_DIR, runId);
|
|
641
|
-
const text = payload
|
|
642
|
-
? `${payload.success ? "completed" : payload.state ?? "failed"}\n\n${payload.output ?? payload.error ?? "(no output)"}`
|
|
643
|
-
: "(run still active — transcript available after completion)";
|
|
644
|
-
await withHerdrBlocked(
|
|
645
|
-
pi,
|
|
646
|
-
"fleet inspector",
|
|
647
|
-
() => sessionCtx!.ui.custom<undefined>(
|
|
648
|
-
(_tui, theme, _keybindings, done) => ({
|
|
649
|
-
render: (width: number): string[] => [
|
|
650
|
-
...text.split("\n").slice(-30).map((line) => ` ${line}`),
|
|
651
|
-
"",
|
|
652
|
-
` ${theme.fg("dim", "esc/q close")}`,
|
|
653
|
-
],
|
|
654
|
-
handleInput: (data: string): void => {
|
|
655
|
-
if (data === "\x1b" || data === "q") done(undefined);
|
|
656
|
-
},
|
|
657
|
-
invalidate: (): void => {},
|
|
658
|
-
}),
|
|
659
|
-
{ overlay: true, overlayOptions: { anchor: "center", width: "90%" } },
|
|
660
|
-
),
|
|
661
|
-
);
|
|
662
|
-
},
|
|
663
|
-
});
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
// ---- Slash commands (/unipi:subagents-*) ----
|
|
667
|
-
registerSlashCommands(pi, () => sessionCtx ?? undefined, {
|
|
668
|
-
manager,
|
|
669
|
-
config,
|
|
670
|
-
asyncDirRoot: ASYNC_DIR,
|
|
671
|
-
sessionId: asyncSessionId,
|
|
672
|
-
});
|
|
673
|
-
|
|
674
|
-
// Register info group at factory time (not session_start)
|
|
675
|
-
const registry = getInfoRegistry();
|
|
676
|
-
if (registry) {
|
|
677
|
-
registry.registerGroup({
|
|
678
|
-
id: "subagents",
|
|
679
|
-
name: "Subagents",
|
|
680
|
-
icon: "🤖",
|
|
681
|
-
priority: 80,
|
|
682
|
-
config: {
|
|
683
|
-
showByDefault: true,
|
|
684
|
-
stats: [
|
|
685
|
-
{ id: "maxConcurrent", label: "Max Concurrent", show: true },
|
|
686
|
-
{ id: "activeCount", label: "Active Agents", show: true },
|
|
687
|
-
{ id: "enabled", label: "Enabled", show: true },
|
|
688
|
-
{ id: "types", label: "Available Types", show: true },
|
|
689
|
-
],
|
|
690
|
-
},
|
|
691
|
-
dataProvider: async () => {
|
|
692
|
-
const types = config.types || {};
|
|
693
|
-
const codeBuiltins: string[] = [...BUILTIN_TYPES];
|
|
694
|
-
const fileBuiltins = [...loadBuiltinFileAgents().keys()];
|
|
695
|
-
|
|
696
|
-
const customTypes: string[] = [];
|
|
697
|
-
for (const dir of [globalAgentsDir, workspaceAgentsDir]) {
|
|
698
|
-
try {
|
|
699
|
-
if (existsSync(dir)) {
|
|
700
|
-
for (const file of readdirSync(dir)) {
|
|
701
|
-
if (file.endsWith(".md") && !customTypes.includes(file.replace(".md", ""))) {
|
|
702
|
-
customTypes.push(file.replace(".md", ""));
|
|
703
|
-
}
|
|
704
|
-
}
|
|
705
|
-
}
|
|
706
|
-
} catch { /* ignore */ }
|
|
707
|
-
}
|
|
708
|
-
|
|
709
|
-
const allTypes = manager.getKnownTypes();
|
|
710
|
-
const typeList = allTypes.map((t) => {
|
|
711
|
-
const isEnabled = manager.isTypeEnabled(t);
|
|
712
|
-
const scope = codeBuiltins.includes(t) || fileBuiltins.includes(t)
|
|
713
|
-
? "builtin"
|
|
714
|
-
: customTypes.includes(t)
|
|
715
|
-
? "custom"
|
|
716
|
-
: "config";
|
|
717
|
-
return `${t}(${scope})${isEnabled ? "" : " [disabled]"}`;
|
|
718
|
-
}).join(", ");
|
|
719
|
-
|
|
720
|
-
const activeAgents = manager.listAgents().filter((a) => a.status === "running").length;
|
|
721
|
-
|
|
722
|
-
return {
|
|
723
|
-
maxConcurrent: { value: String(manager.getMaxConcurrent()) },
|
|
724
|
-
activeCount: { value: String(activeAgents) },
|
|
725
|
-
enabled: { value: config.enabled ? "yes" : "no" },
|
|
726
|
-
types: {
|
|
727
|
-
value: allTypes.length > 0 ? allTypes[0] : "none",
|
|
728
|
-
detail: allTypes.length > 1 ? typeList : undefined,
|
|
729
|
-
},
|
|
730
|
-
};
|
|
731
|
-
},
|
|
732
|
-
});
|
|
442
|
+
function reportText(rec: SubagentRecord, report: HandoffReport, status: SubagentStatus): string {
|
|
443
|
+
const head = status === "cancelled" ? `Subagent ${rec.id} was cancelled${rec.cancelledBy === "user" ? " by the user" : ""}. Partial output:\n\n` : "";
|
|
444
|
+
return `${head}${report.text}\n\n--- subagent ${rec.id} · ${status} · ${plural(report.toolCalls, "tool call")} · ${(report.durationMs / 1000).toFixed(1)}s`;
|
|
733
445
|
}
|
|
734
446
|
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
447
|
+
/**
|
|
448
|
+
* Wait for a run while the lead's UI can answer its approvals. Ends on the
|
|
449
|
+
* report, Esc (aborted), Ctrl+B (moved), a pending user message
|
|
450
|
+
* (interrupted) or the timeout.
|
|
451
|
+
*/
|
|
452
|
+
async function waitForeground(
|
|
453
|
+
run: SubagentRun,
|
|
454
|
+
signal: AbortSignal | undefined,
|
|
455
|
+
ctx: ExtensionContext,
|
|
456
|
+
onUpdate: ((u: unknown) => void) | undefined,
|
|
457
|
+
timeoutMs: number,
|
|
458
|
+
): Promise<{ report?: HandoffReport; interrupted?: boolean; aborted?: boolean; moved?: boolean; error?: string }> {
|
|
459
|
+
const id = run.record.id;
|
|
460
|
+
let move!: () => void;
|
|
461
|
+
const moved = new Promise<"moved">((r) => {
|
|
462
|
+
move = () => r("moved");
|
|
463
|
+
});
|
|
464
|
+
run.runtime.attachUi(ctx.ui as never);
|
|
465
|
+
delivery.attach(id);
|
|
466
|
+
fgWaits.set(id, move);
|
|
467
|
+
manager.setBackground(id, false);
|
|
468
|
+
syncUi();
|
|
469
|
+
const started = Date.now();
|
|
742
470
|
try {
|
|
743
|
-
|
|
744
|
-
(
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
}
|
|
756
|
-
return fleetView.handleKey(data, () => {
|
|
757
|
-
try {
|
|
758
|
-
return ctx.ui.getEditorText() !== "";
|
|
759
|
-
} catch {
|
|
760
|
-
return false;
|
|
761
|
-
}
|
|
762
|
-
});
|
|
471
|
+
while (true) {
|
|
472
|
+
if (signal?.aborted) return { aborted: true };
|
|
473
|
+
if (ctx.hasPendingMessages?.()) return { interrupted: true };
|
|
474
|
+
const remaining = timeoutMs - (Date.now() - started);
|
|
475
|
+
if (remaining <= 0) return {};
|
|
476
|
+
const tick = new Promise<undefined>((r) => setTimeout(() => r(undefined), Math.min(400, remaining)));
|
|
477
|
+
const outcome = await Promise.race([run.done.then((r) => ({ report: r }), (e) => ({ error: String(e) })), moved, tick]);
|
|
478
|
+
if (outcome === "moved") return { moved: true };
|
|
479
|
+
if (outcome !== undefined) return outcome;
|
|
480
|
+
onUpdate?.({
|
|
481
|
+
content: [{ type: "text" as const, text: `Subagent "${run.record.title}" working · ${plural(manager.toolCalls(id), "tool call")}` }],
|
|
482
|
+
details: { owner: "subagents", id, title: run.record.title, profile: run.record.profile, model: run.record.model, status: "running", startedAt: run.record.startedAt } satisfies CardDetails,
|
|
763
483
|
});
|
|
764
484
|
}
|
|
765
|
-
}
|
|
766
|
-
|
|
485
|
+
} finally {
|
|
486
|
+
fgWaits.delete(id);
|
|
487
|
+
run.runtime.detachUi();
|
|
488
|
+
syncUi();
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
async function runSubagent(
|
|
492
|
+
_toolCallId: string,
|
|
493
|
+
params: { title: string; task: string; profile: string; is_background?: boolean; resume?: string },
|
|
494
|
+
signal: AbortSignal | undefined,
|
|
495
|
+
onUpdate: ((u: unknown) => void) | undefined,
|
|
496
|
+
ctx: ExtensionContext,
|
|
497
|
+
) {
|
|
498
|
+
const cwd = ctx.cwd ?? process.cwd();
|
|
499
|
+
const resumeId = params.resume?.trim() || undefined;
|
|
500
|
+
const profileName = resumeId !== undefined ? (manager.record(resumeId)?.profile ?? params.profile) : params.profile;
|
|
501
|
+
const profile = profiles.find((p) => p.id === profileName);
|
|
502
|
+
if (!profile) {
|
|
503
|
+
return textResult(`Unknown profile "${params.profile}". Available: ${profiles.map((p) => p.id).join(", ")}`, { owner: "subagents", status: "failed", title: params.title }, true);
|
|
767
504
|
}
|
|
768
|
-
fleetView.setUICtx(ctx.ui);
|
|
769
|
-
emitEvent(pi, UNIPI_EVENTS.MODULE_READY, {
|
|
770
|
-
name: MODULES.SUBAGENTS || "subagents",
|
|
771
|
-
version: "0.2.0",
|
|
772
|
-
commands: [],
|
|
773
|
-
tools: ["spawn_helper", "get_helper_result"],
|
|
774
|
-
});
|
|
775
|
-
});
|
|
776
505
|
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
506
|
+
const start = manager.start({
|
|
507
|
+
title: params.title,
|
|
508
|
+
task: params.task,
|
|
509
|
+
profile,
|
|
510
|
+
model: resolveSubagentModel(profile, ctx, config),
|
|
511
|
+
thinking: resolveSubagentThinking(profile, ctx, config),
|
|
512
|
+
cwd,
|
|
513
|
+
leadSessionId: leadSessionId(ctx),
|
|
514
|
+
background: resumeId === undefined && params.is_background === true,
|
|
515
|
+
resume: resumeId,
|
|
516
|
+
maxConcurrent: config.maxConcurrent,
|
|
517
|
+
onStep: (record, step) => appendSubagentStep((type, data) => pi.appendEntry(type, data), record, step),
|
|
518
|
+
});
|
|
519
|
+
if ("error" in start) return textResult(start.error, { owner: "subagents", status: "failed", title: params.title, error: start.error }, true);
|
|
520
|
+
const { run } = start;
|
|
521
|
+
const rec = run.record;
|
|
522
|
+
const id = rec.id;
|
|
523
|
+
const base: CardDetails = { owner: "subagents", id, title: rec.title, profile: rec.profile, model: rec.model };
|
|
524
|
+
|
|
525
|
+
if (rec.background) {
|
|
526
|
+
delivery.detach(id, run.done);
|
|
527
|
+
return textResult(
|
|
528
|
+
`Subagent ${id} started in the background. You will receive a <subagent_completion_notification agent_id="${id}"> when it finishes; use read_subagent to wait.`,
|
|
529
|
+
{ ...base, status: "running", phase: "started" },
|
|
530
|
+
);
|
|
531
|
+
}
|
|
781
532
|
|
|
782
|
-
const
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
533
|
+
const waited = await waitForeground(run, signal, ctx, onUpdate, 2_700_000);
|
|
534
|
+
if (waited.report) {
|
|
535
|
+
delivery.consume(id);
|
|
536
|
+
const details = doneDetails(rec, waited.report);
|
|
537
|
+
return textResult(reportText(rec, waited.report, details.status!), details, details.status !== "completed");
|
|
538
|
+
}
|
|
539
|
+
if (waited.aborted) {
|
|
540
|
+
// Esc stopped the turn: the tool result reports the cancel; a completion
|
|
541
|
+
// notice would wake the lead right after the user stopped it.
|
|
542
|
+
delivery.consume(id);
|
|
543
|
+
manager.cancel(id, "user");
|
|
544
|
+
return textResult(`The user cancelled subagent "${rec.title}" (${id}). Don't restart it unless asked; it can be resumed with resume:${id}.`, { ...base, status: "cancelled", phase: "done", cancelledBy: "user", toolCalls: manager.toolCalls(id), durationMs: Date.now() - rec.startedAt }, true);
|
|
545
|
+
}
|
|
546
|
+
delivery.detach(id, run.done);
|
|
547
|
+
manager.setBackground(id, true);
|
|
548
|
+
if (waited.moved) {
|
|
549
|
+
return textResult(
|
|
550
|
+
`The user moved subagent "${rec.title}" (${id}) to the background so you don't wait for it. It keeps working; you will receive a <subagent_completion_notification agent_id="${id}"> when it finishes. Do not wait for it with read_subagent — continue with other work, or end your turn and let the notification wake you.`,
|
|
551
|
+
{ ...base, status: "running", phase: "moved" },
|
|
552
|
+
);
|
|
553
|
+
}
|
|
554
|
+
if (waited.interrupted) {
|
|
555
|
+
return textResult(
|
|
556
|
+
`A user message arrived while subagent "${rec.title}" (${id}) was working. It continues in the background. Act on the user's message first; its completion notification will arrive when it finishes (or call read_subagent({agent_id:"${id}", block:true})).`,
|
|
557
|
+
{ ...base, status: "running", phase: "moved" },
|
|
558
|
+
);
|
|
559
|
+
}
|
|
560
|
+
if (waited.error) return textResult(`Subagent ${id} failed: ${waited.error}`, { ...base, status: "failed", phase: "done", error: waited.error }, true);
|
|
561
|
+
return textResult(`Subagent ${id} is still running in the background; its completion notification will arrive when it finishes.`, { ...base, status: "running", phase: "moved" });
|
|
562
|
+
}
|
|
786
563
|
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
const
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
modelInput = parsed.generationModel;
|
|
797
|
-
}
|
|
564
|
+
// ── Reader registration for the shared read_subagent tool ────────────────
|
|
565
|
+
registerSubagentReader("subagents", {
|
|
566
|
+
owns: (id) => manager.record(id) !== undefined || manager.run(id) !== undefined,
|
|
567
|
+
latest: () => manager.latest(),
|
|
568
|
+
read: async (params, signal, onUpdate, ctx) => {
|
|
569
|
+
const id = params.agent_id ?? manager.latest()?.id;
|
|
570
|
+
const record = id !== undefined ? manager.record(id) : undefined;
|
|
571
|
+
if (id === undefined || record === undefined) {
|
|
572
|
+
return textResult(params.agent_id !== undefined ? `No subagent found for ${params.agent_id}.` : "No subagent has run yet.", { owner: "subagents" }, true);
|
|
798
573
|
}
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
574
|
+
const base: CardDetails = { owner: "subagents", id, title: record.title, profile: record.profile, model: record.model };
|
|
575
|
+
const run = manager.run(id);
|
|
576
|
+
if (record.status !== "running" || run === undefined) {
|
|
577
|
+
const text = record.report !== undefined
|
|
578
|
+
? `${record.status === "cancelled" ? `Cancelled${record.cancelledBy === "user" ? " by the user" : ""}. Partial output:\n\n` : ""}${record.report}\n\n--- subagent ${id} · ${record.status} · ${plural(record.toolCalls, "tool call")}`
|
|
579
|
+
: `Subagent ${id} ${record.status}${record.error !== undefined ? `: ${record.error}` : ""}`;
|
|
580
|
+
return textResult(text, { ...base, status: record.status, phase: "done", toolCalls: record.toolCalls, cancelledBy: record.cancelledBy }, record.status !== "completed");
|
|
581
|
+
}
|
|
582
|
+
if (params.block !== true) {
|
|
583
|
+
return textResult(`Subagent "${record.title}" (${id}) is running — ${plural(manager.toolCalls(id), "tool call")} so far.`, { ...base, status: "running" });
|
|
808
584
|
}
|
|
809
|
-
|
|
585
|
+
const wasBackground = record.background;
|
|
586
|
+
const waited = await waitForeground(run, signal, ctx, onUpdate, Math.min(600, params.timeout ?? 30) * 1000);
|
|
587
|
+
if (waited.report) {
|
|
588
|
+
delivery.consume(id);
|
|
589
|
+
const details = doneDetails(record, waited.report);
|
|
590
|
+
return textResult(reportText(record, waited.report, details.status!), details, details.status !== "completed");
|
|
591
|
+
}
|
|
592
|
+
delivery.detach(id, run.done);
|
|
593
|
+
if (wasBackground) manager.setBackground(id, true);
|
|
594
|
+
if (waited.error) return textResult(`Subagent ${id} failed: ${waited.error}`, { ...base, status: "failed", error: waited.error }, true);
|
|
595
|
+
if (waited.aborted) return textResult(`Stopped waiting for subagent ${id}; it keeps running in the background.`, { ...base, status: "running" });
|
|
596
|
+
if (waited.moved) return textResult(`The user moved subagent ${id} to the background; stop waiting and continue — its completion notification will arrive.`, { ...base, status: "running" });
|
|
597
|
+
if (waited.interrupted) return textResult(`A user message arrived while waiting for subagent "${record.title}" (${id}); it keeps running. Act on the message first.`, { ...base, status: "running" });
|
|
598
|
+
return textResult(`Subagent ${id} is still running.`, { ...base, status: "running" });
|
|
599
|
+
},
|
|
600
|
+
});
|
|
601
|
+
ensureReadSubagentTool(pi);
|
|
602
|
+
|
|
603
|
+
// Background completion notice — badge: `DONE Explore title ···· 7s · 1 tool call`.
|
|
604
|
+
pi.registerMessageRenderer("subagent-completion", (message: { details?: CardDetails & { report?: string } }, opts: { expanded?: boolean }, theme) =>
|
|
605
|
+
renderCompletion(message.details, opts.expanded === true, theme));
|
|
606
|
+
|
|
607
|
+
// Completed subagent steps as a delegated panel (shared UNI-2 renderer;
|
|
608
|
+
// historical entries keep their cards, new ones stream below them).
|
|
609
|
+
pi.registerEntryRenderer("subagent-step", (entry: { data?: DelegatedStep & { group?: string; label?: string } }, opts: { expanded?: boolean }, theme) =>
|
|
610
|
+
renderDelegatedStep(
|
|
611
|
+
entry.data as DelegatedStep,
|
|
612
|
+
opts.expanded === true,
|
|
613
|
+
theme as never,
|
|
614
|
+
{ group: entry.data?.group ?? "subagent", label: entry.data?.label },
|
|
615
|
+
));
|
|
616
|
+
|
|
617
|
+
// ── Prompt section (only while enabled) ───────────────────────────────────
|
|
618
|
+
pi.on("before_agent_start", (event, ctx) => {
|
|
619
|
+
refresh(ctx.cwd ?? process.cwd(), ctx);
|
|
620
|
+
if (!enabled) {
|
|
621
|
+
delete event.systemPromptOptions.sections["subagents"];
|
|
622
|
+
return undefined;
|
|
810
623
|
}
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
624
|
+
const fusionActive = getSharedFusionStatus() !== undefined;
|
|
625
|
+
const out = [
|
|
626
|
+
"You can delegate self-contained subtasks to subagents with `run_subagent`. A subagent is an independent agent with its own context: it does not see this conversation, so put everything it needs in `task` — the goal, relevant paths, constraints, and exactly what to report back.",
|
|
627
|
+
"",
|
|
628
|
+
"Profiles:",
|
|
629
|
+
...profiles.map((p) => `- ${p.id}: ${p.description}`),
|
|
630
|
+
"subagent_explore is read-only and runs on a cheaper model; subagent_general can make changes and runs on your own model, so it costs like a full extra session.",
|
|
631
|
+
"",
|
|
632
|
+
"- Use subagents for broad or parallelizable work: independent searches, investigations across several areas, or implementation slices that don't touch the same files. Skip them when a direct read or search answers the question.",
|
|
633
|
+
"- Launch independent subagents in the same response so they run in parallel, and keep their work disjoint.",
|
|
634
|
+
"- Foreground (the default) waits and returns the report. Set is_background:true only when you have other work to do meanwhile; you will receive a <subagent_completion_notification> when it finishes. Don't poll read_subagent in a loop.",
|
|
635
|
+
"- Background subagents cannot ask for approval: tool calls that would need approval are denied. Run approval-needing work in the foreground.",
|
|
636
|
+
"- The user can move a foreground subagent to the background (Ctrl+B) or cancel any subagent. When that happens, continue accordingly; don't restart a cancelled subagent unless asked.",
|
|
637
|
+
"- Resume a finished, failed, or cancelled subagent with resume:<agent_id> and a follow-up task; it keeps its context.",
|
|
638
|
+
"- The user does not see subagent output directly: summarize what matters from the report, and verify anything critical before relying on it.",
|
|
639
|
+
];
|
|
640
|
+
if (fusionActive) out.push("While Fusion is active, do not use subagents other than the sidekick unless the user explicitly asks you to.");
|
|
641
|
+
event.systemPromptOptions.sections["subagents"] = out.join("\n");
|
|
642
|
+
return undefined;
|
|
819
643
|
});
|
|
820
644
|
|
|
821
|
-
//
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
645
|
+
// ── Commands ──────────────────────────────────────────────────────────────
|
|
646
|
+
pi.registerCommand("unipi:subagents", {
|
|
647
|
+
description: "Open the subagent panel (also ↓ from an empty input)",
|
|
648
|
+
handler: async (_args, ctx) => {
|
|
649
|
+
if (getSharedSubagents().length === 0) {
|
|
650
|
+
ctx.ui.notify("No subagents in this session yet.", "info");
|
|
651
|
+
return;
|
|
652
|
+
}
|
|
653
|
+
await openDock(ctx);
|
|
654
|
+
},
|
|
831
655
|
});
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
pi.on("
|
|
835
|
-
|
|
836
|
-
|
|
656
|
+
registerAgentsCommand(pi, { onChange: (cwd) => refresh(cwd) });
|
|
657
|
+
|
|
658
|
+
pi.on("session_start", (_event, ctx) => {
|
|
659
|
+
uiCtx = ctx;
|
|
660
|
+
stripInstalled = false;
|
|
661
|
+
fgInstalled = false;
|
|
662
|
+
workingShown = false;
|
|
663
|
+
manager.restore(ctx.cwd ?? process.cwd(), leadSessionId(ctx));
|
|
664
|
+
refresh(ctx.cwd ?? process.cwd(), ctx);
|
|
665
|
+
unsubInput?.();
|
|
666
|
+
unsubInput = ctx.hasUI ? ctx.ui.onTerminalInput(onTerminalInput) : undefined;
|
|
667
|
+
syncUi();
|
|
668
|
+
});
|
|
669
|
+
pi.on("session_shutdown", () => {
|
|
670
|
+
manager.shutdown();
|
|
671
|
+
unsubInput?.();
|
|
672
|
+
unsubInput = undefined;
|
|
673
|
+
unsubRegistry();
|
|
674
|
+
uiCtx = undefined;
|
|
837
675
|
});
|
|
838
676
|
|
|
839
|
-
|
|
840
|
-
function createActivityTracker(maxTurns?: number, onStreamUpdate?: () => void) {
|
|
841
|
-
const state: AgentActivity = {
|
|
842
|
-
activeTools: new Map(),
|
|
843
|
-
toolUses: 0,
|
|
844
|
-
turnCount: 1,
|
|
845
|
-
maxTurns,
|
|
846
|
-
tokens: "",
|
|
847
|
-
responseText: "",
|
|
848
|
-
};
|
|
849
|
-
|
|
850
|
-
const callbacks = {
|
|
851
|
-
onToolActivity: (activity: { type: "start" | "end"; toolName: string }) => {
|
|
852
|
-
if (activity.type === "start") {
|
|
853
|
-
state.activeTools.set(activity.toolName + "_" + Date.now(), activity.toolName);
|
|
854
|
-
} else {
|
|
855
|
-
for (const [key, name] of state.activeTools) {
|
|
856
|
-
if (name === activity.toolName) {
|
|
857
|
-
state.activeTools.delete(key);
|
|
858
|
-
break;
|
|
859
|
-
}
|
|
860
|
-
}
|
|
861
|
-
state.toolUses++;
|
|
862
|
-
}
|
|
863
|
-
state.tokens = safeFormatTokens(state.session);
|
|
864
|
-
onStreamUpdate?.();
|
|
865
|
-
},
|
|
866
|
-
onTextDelta: (_delta: string, fullText: string) => {
|
|
867
|
-
state.responseText = fullText;
|
|
868
|
-
onStreamUpdate?.();
|
|
869
|
-
},
|
|
870
|
-
onTurnEnd: (turnCount: number) => {
|
|
871
|
-
state.turnCount = turnCount;
|
|
872
|
-
onStreamUpdate?.();
|
|
873
|
-
},
|
|
874
|
-
onSessionCreated: (session: any) => {
|
|
875
|
-
state.session = session;
|
|
876
|
-
},
|
|
877
|
-
};
|
|
878
|
-
|
|
879
|
-
return { state, callbacks };
|
|
880
|
-
}
|
|
881
|
-
|
|
882
|
-
// ---- Agent tool ----
|
|
883
|
-
|
|
884
|
-
const builtinTypes = BUILTIN_TYPES.join(", ");
|
|
885
|
-
const enabledTypes = manager.getKnownTypes().filter((type) => manager.isTypeEnabled(type));
|
|
886
|
-
const availableTypes = enabledTypes.join(", ") || "none";
|
|
887
|
-
|
|
888
|
-
pi.registerTool(
|
|
889
|
-
defineTool({
|
|
890
|
-
name: "spawn_helper",
|
|
891
|
-
label: "Spawn Helper",
|
|
892
|
-
description: `Launch a sub-agent for parallel work.
|
|
893
|
-
|
|
894
|
-
Available agent types: ${availableTypes}
|
|
895
|
-
Custom types can be defined in:
|
|
896
|
-
- ~/.unipi/config/agents/<name>.md (global)
|
|
897
|
-
- <workspace>/.unipi/config/agents/<name>.md (project)
|
|
898
|
-
|
|
899
|
-
Guidelines:
|
|
900
|
-
- Use "explore" or "scout" for parallel file reads / fast codebase recon
|
|
901
|
-
- Use "work" or "worker" for parallel file writes (transparent locking)
|
|
902
|
-
- Use "reviewer" for code review of diffs, plans, or solutions
|
|
903
|
-
- Use "researcher" for web research (needs web-api tools)
|
|
904
|
-
- Use "oracle" for a second opinion on decisions before acting
|
|
905
|
-
- Use "delegate" for a lightweight general delegate
|
|
906
|
-
- Use run_in_background for work you don't need immediately
|
|
907
|
-
- ESC kills all running agents immediately
|
|
908
|
-
- Agents inherit the parent model by default`,
|
|
909
|
-
parameters: SpawnHelperParams,
|
|
910
|
-
|
|
911
|
-
// ---- Rich inline rendering ----
|
|
912
|
-
|
|
913
|
-
renderCall(args, theme) {
|
|
914
|
-
const displayName = args.type ? args.type : "Agent";
|
|
915
|
-
const desc = args.description ?? "";
|
|
916
|
-
return new Text(
|
|
917
|
-
"▸ " + theme.fg("toolTitle", theme.bold(displayName)) + (desc ? " " + theme.fg("muted", desc) : ""),
|
|
918
|
-
0,
|
|
919
|
-
0,
|
|
920
|
-
);
|
|
921
|
-
},
|
|
922
|
-
|
|
923
|
-
renderResult(result, { expanded, isPartial }, theme) {
|
|
924
|
-
const details = result.details as any;
|
|
925
|
-
if (!details) {
|
|
926
|
-
const text = result.content[0]?.type === "text" ? result.content[0].text : "";
|
|
927
|
-
return new Text(text, 0, 0);
|
|
928
|
-
}
|
|
929
|
-
|
|
930
|
-
// inlineToolDisplay: "summary" keeps one stable row per state.
|
|
931
|
-
if (config.inlineToolDisplay === "summary") {
|
|
932
|
-
const glyph = isPartial || details.status === "running"
|
|
933
|
-
? theme.fg("accent", SPINNER[details.spinnerFrame ?? 0])
|
|
934
|
-
: details.status === "completed"
|
|
935
|
-
? theme.fg("success", "✓")
|
|
936
|
-
: details.status === "background"
|
|
937
|
-
? theme.fg("dim", "■")
|
|
938
|
-
: theme.fg("error", "✗");
|
|
939
|
-
const label = details.status === "completed"
|
|
940
|
-
? "completed"
|
|
941
|
-
: details.status === "background"
|
|
942
|
-
? "background"
|
|
943
|
-
: details.status === "running"
|
|
944
|
-
? "running"
|
|
945
|
-
: details.status;
|
|
946
|
-
return new Text(theme.fg("muted", `${glyph} ${label}`), 0, 0);
|
|
947
|
-
}
|
|
948
|
-
|
|
949
|
-
// Stats helper
|
|
950
|
-
const stats = (d: any) => {
|
|
951
|
-
const parts: string[] = [];
|
|
952
|
-
if (d.turnCount != null && d.turnCount > 0) parts.push(formatTurns(d.turnCount, d.maxTurns));
|
|
953
|
-
if (d.toolUses > 0) parts.push(`${d.toolUses} tool use${d.toolUses === 1 ? "" : "s"}`);
|
|
954
|
-
if (d.tokens) parts.push(d.tokens);
|
|
955
|
-
return parts.map((p) => theme.fg("dim", p)).join(" " + theme.fg("dim", "·") + " ");
|
|
956
|
-
};
|
|
957
|
-
|
|
958
|
-
// Running
|
|
959
|
-
if (isPartial || details.status === "running") {
|
|
960
|
-
const frame = SPINNER[details.spinnerFrame ?? 0];
|
|
961
|
-
const s = stats(details);
|
|
962
|
-
let line = theme.fg("accent", frame) + (s ? " " + s : "");
|
|
963
|
-
line += "\n" + theme.fg("dim", ` ⎿ ${details.activity ?? "thinking…"}`);
|
|
964
|
-
return new Text(line, 0, 0);
|
|
965
|
-
}
|
|
966
|
-
|
|
967
|
-
// Background launched
|
|
968
|
-
if (details.status === "background") {
|
|
969
|
-
return new Text(theme.fg("dim", ` ⎿ Running in background (ID: ${details.agentId})`), 0, 0);
|
|
970
|
-
}
|
|
971
|
-
|
|
972
|
-
// Completed
|
|
973
|
-
if (details.status === "completed") {
|
|
974
|
-
const duration = formatMs(details.durationMs);
|
|
975
|
-
const s = stats(details);
|
|
976
|
-
let line = theme.fg("success", "✓") + (s ? " " + s : "");
|
|
977
|
-
line += " " + theme.fg("dim", "·") + " " + theme.fg("dim", duration);
|
|
978
|
-
|
|
979
|
-
if (expanded) {
|
|
980
|
-
const resultText = result.content[0]?.type === "text" ? result.content[0].text : "";
|
|
981
|
-
if (resultText) {
|
|
982
|
-
const rlines = resultText.split("\n").slice(0, 50);
|
|
983
|
-
for (const l of rlines) {
|
|
984
|
-
line += "\n" + theme.fg("dim", ` ${l}`);
|
|
985
|
-
}
|
|
986
|
-
}
|
|
987
|
-
} else {
|
|
988
|
-
line += "\n" + theme.fg("dim", " ⎿ Done");
|
|
989
|
-
}
|
|
990
|
-
return new Text(line, 0, 0);
|
|
991
|
-
}
|
|
992
|
-
|
|
993
|
-
// Error / Aborted / Stopped
|
|
994
|
-
const isError = details.status === "error";
|
|
995
|
-
const isStopped = details.status === "stopped";
|
|
996
|
-
const s = stats(details);
|
|
997
|
-
let line = (isStopped ? theme.fg("dim", "■") : theme.fg("error", "✗")) + (s ? " " + s : "");
|
|
998
|
-
|
|
999
|
-
if (isError) {
|
|
1000
|
-
line += "\n" + theme.fg("error", ` ⎿ Error: ${details.error ?? "unknown"}`);
|
|
1001
|
-
} else if (isStopped) {
|
|
1002
|
-
line += "\n" + theme.fg("dim", " ⎿ Stopped");
|
|
1003
|
-
} else {
|
|
1004
|
-
line += "\n" + theme.fg("warning", " ⎿ Aborted (max turns exceeded)");
|
|
1005
|
-
}
|
|
1006
|
-
return new Text(line, 0, 0);
|
|
1007
|
-
},
|
|
1008
|
-
|
|
1009
|
-
// ---- Execute ----
|
|
1010
|
-
|
|
1011
|
-
execute: async (toolCallId, params, signal, onUpdate, ctx) => {
|
|
1012
|
-
widget.setUICtx(ctx.ui);
|
|
1013
|
-
fleetView.setUICtx(ctx.ui);
|
|
1014
|
-
|
|
1015
|
-
// Route through the parity handler (actions, workflowScript, legacy
|
|
1016
|
-
// single-child) — widget/notify plumbing lives in the deps adapters.
|
|
1017
|
-
onUpdateForForeground = (details) => {
|
|
1018
|
-
onUpdate?.({
|
|
1019
|
-
content: [{ type: "text", text: `${details.toolUses ?? 0} tool uses...` }],
|
|
1020
|
-
details,
|
|
1021
|
-
});
|
|
1022
|
-
};
|
|
1023
|
-
try {
|
|
1024
|
-
return (await handleSpawnHelper(handlerDeps, ctx, params as Record<string, unknown>, signal)) as never;
|
|
1025
|
-
} finally {
|
|
1026
|
-
onUpdateForForeground = () => {};
|
|
1027
|
-
}
|
|
1028
|
-
},
|
|
1029
|
-
}),
|
|
1030
|
-
);
|
|
1031
|
-
|
|
1032
|
-
// ---- get_helper_result tool ----
|
|
1033
|
-
|
|
1034
|
-
pi.registerTool(
|
|
1035
|
-
defineTool({
|
|
1036
|
-
name: "get_helper_result",
|
|
1037
|
-
label: "Get Helper Result",
|
|
1038
|
-
description: "Check status and retrieve results from a background agent or async run. Use view: true to open a live conversation overlay; nonBlocking: true to subscribe and be woken on completion.",
|
|
1039
|
-
parameters: GetHelperResultParams,
|
|
1040
|
-
execute: async (_toolCallId, rawParams, _signal, _onUpdate, ctx) => {
|
|
1041
|
-
const params = rawParams as Record<string, unknown>;
|
|
1042
|
-
const id = (params.id ?? params.agent_id) as string | undefined;
|
|
1043
|
-
const nonBlocking = params.nonBlocking === true;
|
|
1044
|
-
const waitAll = params.all === true;
|
|
1045
|
-
const timeoutMs = typeof params.timeoutMs === "number" ? params.timeoutMs : undefined;
|
|
1046
|
-
|
|
1047
|
-
// ---- Async process runs ----
|
|
1048
|
-
const asyncPayload = id ? readAsyncResultFile(RESULTS_DIR, id) : undefined;
|
|
1049
|
-
if (asyncPayload && !nonBlocking) {
|
|
1050
|
-
const duration = asyncPayload.durationMs ? `${(asyncPayload.durationMs / 1000).toFixed(1)}s` : "";
|
|
1051
|
-
return textResult(
|
|
1052
|
-
`Run: ${asyncPayload.runId}\nAgent: ${asyncPayload.agent ?? "unknown"} | Status: ${asyncPayload.state ?? (asyncPayload.success ? "completed" : "failed")}\n\n${asyncPayload.output ?? asyncPayload.error ?? "(no output)"}`,
|
|
1053
|
-
{ status: asyncPayload.success ? "completed" : "error", runId: asyncPayload.runId },
|
|
1054
|
-
);
|
|
1055
|
-
}
|
|
1056
|
-
if (nonBlocking && id) {
|
|
1057
|
-
// Persist a wake subscription: when the result file appears, the
|
|
1058
|
-
// watcher delivers a followUp notification that wakes this session.
|
|
1059
|
-
if (readAsyncResultFile(RESULTS_DIR, id)) {
|
|
1060
|
-
return textResult(`Run ${id} already completed.`, { status: "completed", runId: id });
|
|
1061
|
-
}
|
|
1062
|
-
writePendingSubscription(RESULTS_DIR, asyncSessionId, id);
|
|
1063
|
-
return textResult(
|
|
1064
|
-
`Subscribed to run ${id}. This session will be woken on completion or failure.`,
|
|
1065
|
-
{ status: "subscribed", runId: id },
|
|
1066
|
-
);
|
|
1067
|
-
}
|
|
1068
|
-
|
|
1069
|
-
// ---- In-process records ----
|
|
1070
|
-
const record = manager.getRecord(id as string);
|
|
1071
|
-
if (!record) {
|
|
1072
|
-
if (waitAll) {
|
|
1073
|
-
// Wait for ALL active in-process agents.
|
|
1074
|
-
const active = manager.listAgents().filter((a) => a.status === "running" || a.status === "queued");
|
|
1075
|
-
if (active.length === 0) return textResult("No active agents.");
|
|
1076
|
-
await Promise.race([
|
|
1077
|
-
Promise.allSettled(active.map((a) => a.promise).filter(Boolean)),
|
|
1078
|
-
new Promise((resolve) => setTimeout(resolve, timeoutMs ?? 1_800_000)),
|
|
1079
|
-
]);
|
|
1080
|
-
return textResult(`All ${active.length} agent(s) settled (or wait timed out).`);
|
|
1081
|
-
}
|
|
1082
|
-
return textResult(`Helper not found: "${id}". It may have been cleaned up.`);
|
|
1083
|
-
}
|
|
1084
|
-
|
|
1085
|
-
// Open conversation viewer overlay if requested
|
|
1086
|
-
if (params.view && record.session) {
|
|
1087
|
-
const activity = agentActivity.get(record.id);
|
|
1088
|
-
await withHerdrBlocked(
|
|
1089
|
-
pi,
|
|
1090
|
-
"helper viewer",
|
|
1091
|
-
() => ctx.ui.custom<undefined>(
|
|
1092
|
-
(tui, theme, _keybindings, done) => {
|
|
1093
|
-
return new ConversationViewer(
|
|
1094
|
-
tui,
|
|
1095
|
-
record.session!,
|
|
1096
|
-
{
|
|
1097
|
-
type: record.type,
|
|
1098
|
-
description: record.description,
|
|
1099
|
-
status: record.status,
|
|
1100
|
-
toolUses: record.toolUses,
|
|
1101
|
-
startedAt: record.startedAt,
|
|
1102
|
-
completedAt: record.completedAt,
|
|
1103
|
-
},
|
|
1104
|
-
activity,
|
|
1105
|
-
theme,
|
|
1106
|
-
done,
|
|
1107
|
-
);
|
|
1108
|
-
},
|
|
1109
|
-
{
|
|
1110
|
-
overlay: true,
|
|
1111
|
-
overlayOptions: { anchor: "center", width: "90%" },
|
|
1112
|
-
},
|
|
1113
|
-
),
|
|
1114
|
-
);
|
|
1115
|
-
}
|
|
1116
|
-
|
|
1117
|
-
if (params.wait && record.status === "running" && record.promise) {
|
|
1118
|
-
record.resultConsumed = true;
|
|
1119
|
-
await record.promise;
|
|
1120
|
-
}
|
|
1121
|
-
|
|
1122
|
-
const duration = record.completedAt
|
|
1123
|
-
? `${((record.completedAt - record.startedAt) / 1000).toFixed(1)}s`
|
|
1124
|
-
: "running";
|
|
1125
|
-
|
|
1126
|
-
let output =
|
|
1127
|
-
`Agent: ${record.id}\n` +
|
|
1128
|
-
`Type: ${record.type} | Status: ${record.status} | Tool uses: ${record.toolUses} | Duration: ${duration}\n` +
|
|
1129
|
-
`Description: ${record.description}\n\n`;
|
|
1130
|
-
|
|
1131
|
-
if (record.status === "running") {
|
|
1132
|
-
output += "Agent is still running. Use wait: true or check back later.";
|
|
1133
|
-
} else if (record.status === "error") {
|
|
1134
|
-
output += `Error: ${record.error}`;
|
|
1135
|
-
} else {
|
|
1136
|
-
const bounded = boundHelperOutput(
|
|
1137
|
-
record.result?.trim() || "No output.",
|
|
1138
|
-
64 * 1024,
|
|
1139
|
-
record.resultArtifactPath,
|
|
1140
|
-
);
|
|
1141
|
-
record.resultArtifactPath = bounded.artifactPath;
|
|
1142
|
-
output += bounded.text;
|
|
1143
|
-
}
|
|
1144
|
-
|
|
1145
|
-
if (record.status !== "running" && record.status !== "queued") {
|
|
1146
|
-
record.resultConsumed = true;
|
|
1147
|
-
}
|
|
1148
|
-
|
|
1149
|
-
return textResult(output);
|
|
1150
|
-
},
|
|
1151
|
-
}),
|
|
1152
|
-
);
|
|
677
|
+
emitEvent(pi, UNIPI_EVENTS.MODULE_READY, { module: "subagents" });
|
|
1153
678
|
}
|