pi-extended-teams 2.1.18 → 2.2.4
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/LICENSE +21 -0
- package/README.md +25 -12
- package/SECURITY.md +19 -0
- package/docs/access.md +35 -0
- package/extensions/agents/read-agent.ts +157 -5
- package/extensions/agents/write-agent.ts +8 -5
- package/extensions/events/register-events.ts +1 -1
- package/extensions/index.ts +50 -5
- package/extensions/runtime/nested-read-agents.ts +1 -1
- package/extensions/runtime/teammate-interrupt.ts +222 -0
- package/extensions/runtime/types.ts +15 -0
- package/extensions/team/team-contracts.json +1 -1
- package/extensions/tools/agent-status-tool.ts +270 -0
- package/extensions/tools/task-runtime-tools.ts +24 -3
- package/extensions/tools/team-tools.ts +76 -16
- package/extensions/ui/agent-follow-view.ts +32 -6
- package/extensions/ui/agent-navigation.ts +2 -0
- package/extensions/ui/renderers.ts +4 -4
- package/extensions/ui/theme.ts +1 -1
- package/package.json +15 -10
- package/skills/teams.md +14 -11
- package/src/adapters/tmux-adapter.ts +11 -0
- package/src/orchestration/index.ts +1 -1
- package/src/utils/runtime.ts +4 -0
- package/src/utils/settings.ts +23 -1
- package/src/utils/terminal-adapter.ts +7 -0
- package/assets/pi-extended-teams-agent-navigation.png +0 -0
- package/assets/pi-extended-teams-in-action.png +0 -0
- package/extensions/agents/read-agent-session-lifecycle.test.ts +0 -605
- package/extensions/agents/read-agent.test.ts +0 -3077
- package/extensions/agents/write-agent.test.ts +0 -513
- package/extensions/events/register-events.test.ts +0 -465
- package/extensions/index.test.ts +0 -1659
- package/extensions/internal/agent-session-files.test.ts +0 -164
- package/extensions/internal/pi-command.test.ts +0 -191
- package/extensions/internal/pi-runtime-api.test.ts +0 -43
- package/extensions/internal/session-context-reference.test.ts +0 -290
- package/extensions/internal/session-files.test.ts +0 -225
- package/extensions/resources/spawn-resource-plan.test.ts +0 -233
- package/extensions/runtime/active-agent-sleep.test.ts +0 -157
- package/extensions/runtime/pending-child-controller.test.ts +0 -169
- package/extensions/team/contracts.test.ts +0 -111
- package/extensions/team/lifecycle.test.ts +0 -1095
- package/extensions/team/recipient-closure.test.ts +0 -97
- package/extensions/team/roster.test.ts +0 -129
- package/extensions/team/writer-screens.test.ts +0 -50
- package/extensions/tools/agent-communication-tools.test.ts +0 -306
- package/extensions/tools/coordination-tools.test.ts +0 -670
- package/extensions/tools/delegation-guard.test.ts +0 -95
- package/extensions/tools/file-claim-tools.test.ts +0 -104
- package/extensions/tools/predefined-tools.test.ts +0 -647
- package/extensions/tools/read-helper.test.ts +0 -35
- package/extensions/tools/task-runtime-tools.test.ts +0 -334
- package/extensions/tools/team-tools.read-agent.test.ts +0 -1924
- package/extensions/ui/agent-follow-view.test.ts +0 -578
- package/extensions/ui/agent-navigation.test.ts +0 -52
- package/extensions/ui/extensions-command.test.ts +0 -336
- package/extensions/ui/favorite-models-command.test.ts +0 -280
- package/extensions/ui/read-agent-status.test.ts +0 -117
- package/extensions/ui/status-widget.test.ts +0 -169
- package/extensions/ui-frame.test.ts +0 -69
- package/src/adapters/tmux-adapter.test.ts +0 -276
- package/src/orchestration/orchestrator.test.ts +0 -396
- package/src/utils/claims.test.ts +0 -137
- package/src/utils/hooks.test.ts +0 -75
- package/src/utils/lifecycle-tombstone.test.ts +0 -105
- package/src/utils/lock.race.child.ts +0 -44
- package/src/utils/lock.race.test.ts +0 -198
- package/src/utils/lock.test.ts +0 -90
- package/src/utils/messaging.test.ts +0 -337
- package/src/utils/model-resolution.test.ts +0 -231
- package/src/utils/models.test.ts +0 -8
- package/src/utils/predefined-teams.save-template.test.ts +0 -74
- package/src/utils/predefined-teams.test.ts +0 -441
- package/src/utils/report-events.test.ts +0 -154
- package/src/utils/runtime.test.ts +0 -314
- package/src/utils/security.test.ts +0 -43
- package/src/utils/settings.test.ts +0 -480
- package/src/utils/shared-memory.test.ts +0 -80
- package/src/utils/tasks.race.test.ts +0 -44
- package/src/utils/tasks.test.ts +0 -229
- package/src/utils/thinking-levels.test.ts +0 -56
- package/src/utils/workflow-metadata.test.ts +0 -11
- package/src/utils/write-queue.test.ts +0 -251
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Leonardo Pereira
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -6,7 +6,15 @@ pi-extended-teams lets the lead split work into bounded lanes, watch agents live
|
|
|
6
6
|
|
|
7
7
|
Read agents are the default. Edit agents are opt-in and should own isolated files.
|
|
8
8
|
|
|
9
|
-
[](assets/pi-extended-teams-in-action.png)
|
|
9
|
+
[](https://raw.githubusercontent.com/dantetekanem/pi-extended-teams/main/assets/pi-extended-teams-in-action.png)
|
|
10
|
+
|
|
11
|
+
## Try without installing
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
pi -e npm:pi-extended-teams
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
This runs the published package for the current Pi invocation without adding it to your project configuration.
|
|
10
18
|
|
|
11
19
|
## Install and run
|
|
12
20
|
|
|
@@ -22,23 +30,22 @@ Or install directly from GitHub:
|
|
|
22
30
|
pi install git:github.com/dantetekanem/pi-extended-teams
|
|
23
31
|
```
|
|
24
32
|
|
|
25
|
-
Run `/agents-favorite-models` once in Pi and choose a model and thinking level for each intent tier.
|
|
26
|
-
|
|
27
33
|
Then ask for help naturally:
|
|
28
34
|
|
|
29
35
|
```text
|
|
30
36
|
Review the current changes with separate agents for correctness, test gaps, and security. Give me the evidence so I can make the final call.
|
|
31
37
|
```
|
|
32
38
|
|
|
33
|
-
The current Pi session becomes the agent group automatically. There is no separate setup step.
|
|
39
|
+
The current Pi session becomes the agent group automatically. There is no separate setup step. Until you configure a tier, it uses the current lead-session model and thinking level. Run `/agents-favorite-models` when you want particular tiers to use different models.
|
|
34
40
|
|
|
35
41
|
## How it works
|
|
36
42
|
|
|
37
|
-
- Read
|
|
43
|
+
- Read agents run in-process. Edit agents run in separate Pi sessions through the configured terminal adapter. Both stay connected to the lead session.
|
|
38
44
|
- The activity card shows progress, intent tier, elapsed time, tokens, and tool activity.
|
|
39
|
-
- You can open an agent's transcript, send it a message, or stop it.
|
|
45
|
+
- You can open an agent's transcript, send it a message, interrupt a stuck tool command, or stop it.
|
|
40
46
|
- Completed reports return to the lead automatically and remain recoverable when needed.
|
|
41
|
-
-
|
|
47
|
+
- `get_agent_status` gives the lead or an eligible nested parent one read-only snapshot of owned active, queued, stalled, or recently completed read and edit agents.
|
|
48
|
+
- Every spawn names an intent tier instead of choosing ad hoc model settings. Configured favorites take priority; unset tiers inherit the current lead model and thinking.
|
|
42
49
|
- Edit agents can claim isolated files. Claims coordinate cooperative agents; they are not access control.
|
|
43
50
|
- Lazy session context and nested read helpers are available when a bounded task needs them.
|
|
44
51
|
|
|
@@ -46,17 +53,19 @@ This works well for multi-angle code review, root-cause investigation, parallel
|
|
|
46
53
|
|
|
47
54
|
## Live control
|
|
48
55
|
|
|
49
|
-
With the editor empty, press Down to open agent navigation. Use Down/Up to move, `l` to expand large tool logs, `m` to message an agent, `x` to stop
|
|
56
|
+
With the editor empty, press Down to open agent navigation. Use Down/Up to move, `l` to expand large tool logs, `m` to message an agent, `i` to interrupt its currently running tool command, `x` to stop the whole agent, and Escape to return.
|
|
57
|
+
|
|
58
|
+
The lead can invoke the same command-only behavior with `interrupt_teammate({ agent_name: "agent" })`. It keeps the agent's session, task context, and file claims intact so you can send follow-up work. In-process cancellation is cooperative and may report that it is still pending; for tmux-backed agents, success means Pi's Escape key was delivered, not that command settlement was independently confirmed.
|
|
50
59
|
|
|
51
|
-
[](assets/pi-extended-teams-agent-navigation.png)
|
|
60
|
+
[](https://raw.githubusercontent.com/dantetekanem/pi-extended-teams/main/assets/pi-extended-teams-agent-navigation.png)
|
|
52
61
|
|
|
53
|
-
Completed reports wake the lead automatically.
|
|
62
|
+
Completed reports wake the lead automatically. End the current turn to wait. One `get_agent_status` snapshot is allowed when current status is needed; do not poll with sleeps, loops, or repeated checks. Use `check_teammate` only when `get_agent_status` shows a suspected lifecycle failure or a persisted report needs recovery.
|
|
54
63
|
|
|
55
64
|
The lead owns decomposition, integration, and acceptance. Pi packages and spawned agents run with your system permissions, so review project-local instructions and configuration through Pi's normal trust flow.
|
|
56
65
|
|
|
57
66
|
## Intent tiers
|
|
58
67
|
|
|
59
|
-
Every spawn
|
|
68
|
+
Every spawn names a `model_slot`. Configured favorites take priority; otherwise the tier uses the current lead-session model and thinking level:
|
|
60
69
|
|
|
61
70
|
| Tier | Use it for |
|
|
62
71
|
| --- | --- |
|
|
@@ -89,10 +98,14 @@ For an edit, choose a write tier and name the files it may claim. Never run over
|
|
|
89
98
|
|
|
90
99
|
## Configuration
|
|
91
100
|
|
|
92
|
-
Global settings live at `~/.pi/agent/pi-extended-teams/settings.json`. Project overrides live at `.pi/pi-extended-teams.json`. Favorite intent tiers are global so `/agents-favorite-models` and spawning use the same choices.
|
|
101
|
+
Global settings live at `~/.pi/agent/pi-extended-teams/settings.json`. Project overrides live at `.pi/pi-extended-teams.json`. Favorite intent tiers are global so `/agents-favorite-models` and spawning use the same choices. Configuring favorites is optional; an unset tier falls back to the current lead-session model and thinking level.
|
|
93
102
|
|
|
94
103
|
Spawned sessions are private by default under `~/.pi/teams/<team>/agent-sessions/` and stay out of Pi's normal `/resume` picker.
|
|
95
104
|
|
|
105
|
+
## Security and data access
|
|
106
|
+
|
|
107
|
+
Read [SECURITY.md](SECURITY.md) for private vulnerability reporting and [docs/access.md](docs/access.md) for the subprocess, filesystem, extension, hook, and network boundaries.
|
|
108
|
+
|
|
96
109
|
## Development
|
|
97
110
|
|
|
98
111
|
```bash
|
package/SECURITY.md
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# Security policy
|
|
2
|
+
|
|
3
|
+
## Supported versions
|
|
4
|
+
|
|
5
|
+
Security fixes are made on the latest published version of `pi-extended-teams`.
|
|
6
|
+
|
|
7
|
+
## Reporting a vulnerability
|
|
8
|
+
|
|
9
|
+
Do not open a public issue for a suspected vulnerability. Use [GitHub private vulnerability reporting](https://github.com/dantetekanem/pi-extended-teams/security/advisories/new) for this repository. Include a description, affected versions, reproduction steps, and impact.
|
|
10
|
+
|
|
11
|
+
If private reporting is unavailable, contact the maintainer through the repository owner's GitHub profile. Do not include credentials, private prompts, or other sensitive data in a public issue.
|
|
12
|
+
|
|
13
|
+
Reports are acknowledged after maintainer review. Disclosure timing and any fix will be coordinated with the reporter.
|
|
14
|
+
|
|
15
|
+
## Trust boundary
|
|
16
|
+
|
|
17
|
+
This package coordinates local Pi sessions. A spawned teammate receives the prompt, project working directory, configured model selection, and the capabilities of its child Pi process. Review the access disclosure before using it with a project or credentials you do not intend a teammate to access.
|
|
18
|
+
|
|
19
|
+
See [docs/access.md](docs/access.md) for the process, file, extension, hook, and network boundaries.
|
package/docs/access.md
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# Access and execution disclosure
|
|
2
|
+
|
|
3
|
+
`pi-extended-teams` is a local orchestration extension. It creates teammates that act in the working directory selected for the teammate. A teammate is not a sandbox or a security boundary.
|
|
4
|
+
|
|
5
|
+
## Child processes
|
|
6
|
+
|
|
7
|
+
- Read teammates run in the lead Pi process through Pi's `AgentSession` API. They do not receive a tmux pane.
|
|
8
|
+
- Write teammates start a separate Pi process in a tmux pane. The extension builds and runs the local Pi launch command with the teammate's working directory, model and thinking configuration, selected extensions, and `PI_TEAM_NAME`, `PI_AGENT_NAME`, and `PI_LIFECYCLE_RUN_ID` environment variables.
|
|
9
|
+
- Before launching a write teammate, the extension runs a bounded local `pi models --all` preflight through `sh -c` to determine whether the configured model is available.
|
|
10
|
+
- On macOS, while at least one write teammate is active, the extension may run `/usr/bin/caffeinate -i -w <lead-pid>` to prevent idle system sleep. It terminates that helper when no write teammates remain or the extension is disposed.
|
|
11
|
+
- In a Herdr session, the extension can invoke the configured `herdr` binary to split, start, and close panes when moving an agent. The binary path defaults to `herdr` and can be set with `HERDR_BIN_PATH`.
|
|
12
|
+
|
|
13
|
+
## Files and local state
|
|
14
|
+
|
|
15
|
+
The extension reads settings from `~/.pi/agent/pi-extended-teams/settings.json` and `<project>/.pi/pi-extended-teams.json`. Model-provider compatibility also reads the legacy `~/.pi/pi-extended-teams.json` path. Predefined agents can be read from `~/.pi/agent/agents/` and `<project>/.pi/agents/`; team templates can be read or written at `~/.pi/teams.yaml`, `~/.pi/agent/teams.yaml`, and `<project>/.pi/teams.yaml`.
|
|
16
|
+
|
|
17
|
+
For a team named `<team>`, coordination state is stored under `~/.pi/teams/<team>/`. This includes `config.json`, inboxes, runtime status, session-context references, lifecycle quarantine and tombstones, file claims, write and read-helper queues, shared memory, report events, lead-session metadata, debug logs, and private child transcripts under `agent-sessions/`. Task records are stored under `~/.pi/tasks/<team>/`, and agent reports may also be written under `~/.pi/agent/reports/`. Cleanup removes lifecycle and queue files when they are no longer needed.
|
|
18
|
+
|
|
19
|
+
The extension reads project extension sources only when Pi marks the same working directory trusted. Teammates receive the working directory supplied at spawn time. A write teammate can use any filesystem access granted to its child Pi process and enabled tools. A read teammate's tools are restricted by the extension, but it still receives prompt and project context supplied by the lead.
|
|
20
|
+
|
|
21
|
+
## Extensions and hooks
|
|
22
|
+
|
|
23
|
+
A child Pi process starts with `--no-extensions`, then receives `pi-extended-teams` plus only lead-session extensions selected by the extension policy. The package does not automatically grant every available extension to a child. Project trust is forwarded only when the child uses the same working directory as the trusted lead session. Enabled extensions can add their own tools, hooks, file access, subprocesses, or network behavior; review them separately.
|
|
24
|
+
|
|
25
|
+
The package's documented project hook is `.pi/team-hooks/task_completed.sh`, which runs when a teammate marks a task complete. Hooks are project-supplied executable code and run with the permissions of the local Pi process. Do not enable or keep hooks you do not trust.
|
|
26
|
+
|
|
27
|
+
## Network and model providers
|
|
28
|
+
|
|
29
|
+
The extension does not make application network requests itself. Pi and any enabled extensions may make network requests. In particular, a child Pi process can send its prompt and tool context to the configured model provider, subject to Pi's provider configuration. The model provider, enabled extensions, and tools determine any additional network access.
|
|
30
|
+
|
|
31
|
+
Use a model provider and extension set appropriate for the project data. Do not delegate secrets or data that you are not authorized to share with the configured provider or local tools.
|
|
32
|
+
|
|
33
|
+
## Operator controls
|
|
34
|
+
|
|
35
|
+
The lead controls teammate creation, messages, task assignment, and stop/interrupt actions. A teammate cannot access lead-only process-control tools. File claims coordinate edits but do not enforce operating-system access control.
|
|
@@ -10,6 +10,7 @@ import { createAgentCommunicationTools, type SubmittedAgentReport } from "../too
|
|
|
10
10
|
import { requireWriteAgentTeam } from "../team/roster";
|
|
11
11
|
import { isPiPromptPlanningMember, shouldSuppressLeadReportInjection } from "../../src/utils/workflow-metadata";
|
|
12
12
|
import { canonicalPersistedModelSlot, loadSettings, requireFavoriteModelLevel } from "../../src/utils/settings";
|
|
13
|
+
import { parseQualifiedModel } from "../../src/utils/model-resolution";
|
|
13
14
|
import { closePersistedRecipient } from "../team/recipient-closure";
|
|
14
15
|
import { generateExtensionInstanceId, generateLifecycleRunId } from "../../src/utils/lifecycle-tombstone";
|
|
15
16
|
import { createLifecycleRuntime, type ShutdownTeammateOptions } from "../team/lifecycle";
|
|
@@ -45,6 +46,45 @@ export { closeReadAgentMessageDelivery } from "./read-agent-session-lifecycle";
|
|
|
45
46
|
|
|
46
47
|
const pendingParentWakeSignals = new WeakMap<RunningReadAgent, () => void>();
|
|
47
48
|
|
|
49
|
+
interface ReadAgentWakeState {
|
|
50
|
+
generation: number;
|
|
51
|
+
waiters: Set<() => void>;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const readAgentWakeStates = new WeakMap<RunningReadAgent, ReadAgentWakeState>();
|
|
55
|
+
|
|
56
|
+
function readAgentWakeState(agent: RunningReadAgent): ReadAgentWakeState {
|
|
57
|
+
let state = readAgentWakeStates.get(agent);
|
|
58
|
+
if (!state) {
|
|
59
|
+
state = { generation: 0, waiters: new Set() };
|
|
60
|
+
readAgentWakeStates.set(agent, state);
|
|
61
|
+
}
|
|
62
|
+
return state;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function readAgentWakeGeneration(agent: RunningReadAgent): number {
|
|
66
|
+
return readAgentWakeState(agent).generation;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function signalReadAgentWake(agent: RunningReadAgent): void {
|
|
70
|
+
const state = readAgentWakeState(agent);
|
|
71
|
+
state.generation += 1;
|
|
72
|
+
const waiters = Array.from(state.waiters);
|
|
73
|
+
state.waiters.clear();
|
|
74
|
+
for (const resolve of waiters) resolve();
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function waitForReadAgentWake(agent: RunningReadAgent, generation: number): Promise<void> {
|
|
78
|
+
const state = readAgentWakeState(agent);
|
|
79
|
+
if (state.generation !== generation) return Promise.resolve();
|
|
80
|
+
return new Promise<void>((resolve) => { state.waiters.add(resolve); });
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function disposeReadAgentWake(agent: RunningReadAgent): void {
|
|
84
|
+
signalReadAgentWake(agent);
|
|
85
|
+
readAgentWakeStates.delete(agent);
|
|
86
|
+
}
|
|
87
|
+
|
|
48
88
|
export interface RunReadAgentOptions {
|
|
49
89
|
isTeammate: boolean;
|
|
50
90
|
getTeamName(): string | null | undefined;
|
|
@@ -95,6 +135,52 @@ function markReadAgentActivity(
|
|
|
95
135
|
pushReadAgentEvent(agent, text);
|
|
96
136
|
}
|
|
97
137
|
|
|
138
|
+
interface ReadAgentOperationOutcome {
|
|
139
|
+
generation: number;
|
|
140
|
+
interrupted: boolean;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
async function runReadAgentSessionOperation(
|
|
144
|
+
state: RunningReadAgent,
|
|
145
|
+
operation: () => Promise<void>,
|
|
146
|
+
): Promise<ReadAgentOperationOutcome> {
|
|
147
|
+
const generation = (state.operationGeneration ?? 0) + 1;
|
|
148
|
+
state.operationGeneration = generation;
|
|
149
|
+
state.activeOperationGeneration = generation;
|
|
150
|
+
state.completedOperationError = undefined;
|
|
151
|
+
let settleOperation!: () => void;
|
|
152
|
+
const operationSettlement = new Promise<void>((resolve) => { settleOperation = resolve; });
|
|
153
|
+
state.activeOperationSettlementPromise = operationSettlement;
|
|
154
|
+
|
|
155
|
+
try {
|
|
156
|
+
let failure: unknown;
|
|
157
|
+
try {
|
|
158
|
+
await operation();
|
|
159
|
+
} catch (error) {
|
|
160
|
+
failure = error;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (state.activeOperationGeneration === generation) state.activeOperationGeneration = undefined;
|
|
164
|
+
const interrupted = state.interruptRequestedGeneration === generation;
|
|
165
|
+
if (interrupted) state.interruptRequestedGeneration = undefined;
|
|
166
|
+
state.completedOperationGeneration = generation;
|
|
167
|
+
state.completedOperationInterrupted = interrupted;
|
|
168
|
+
state.completedOperationError = interrupted ? undefined : failure;
|
|
169
|
+
|
|
170
|
+
if (interrupted) {
|
|
171
|
+
markReadAgentActivity(state, "command interrupted; waiting for lead", "thinking");
|
|
172
|
+
return { generation, interrupted: true };
|
|
173
|
+
}
|
|
174
|
+
if (failure) throw failure;
|
|
175
|
+
return { generation, interrupted: false };
|
|
176
|
+
} finally {
|
|
177
|
+
if (state.activeOperationSettlementPromise === operationSettlement) {
|
|
178
|
+
state.activeOperationSettlementPromise = undefined;
|
|
179
|
+
}
|
|
180
|
+
settleOperation();
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
98
184
|
function refreshReadAgentStats(agent: RunningReadAgent, session: AgentSession, activityAlreadyMarked = false): void {
|
|
99
185
|
const stats = session.getSessionStats();
|
|
100
186
|
const tokensUsed = stats.tokens.total;
|
|
@@ -265,12 +351,20 @@ export async function sendMessageToRunningReadAgent(agent: RunningReadAgent | un
|
|
|
265
351
|
}
|
|
266
352
|
|
|
267
353
|
const session = agent.session;
|
|
268
|
-
const delivery = session.isStreaming ? { deliverAs: "steer" as const } : undefined;
|
|
269
354
|
const deliveryResult = enqueueReadAgentMessageDelivery(
|
|
270
355
|
agent,
|
|
271
356
|
agent.name,
|
|
272
|
-
() =>
|
|
357
|
+
async () => {
|
|
358
|
+
const pendingInterrupt = agent.operationInterruptPromise;
|
|
359
|
+
if (pendingInterrupt) await pendingInterrupt.catch(() => {});
|
|
360
|
+
if (session.isStreaming) {
|
|
361
|
+
await session.sendUserMessage(content, { deliverAs: "steer" as const });
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
await runReadAgentSessionOperation(agent, () => session.sendUserMessage(content, undefined));
|
|
365
|
+
}
|
|
273
366
|
);
|
|
367
|
+
signalReadAgentWake(agent);
|
|
274
368
|
pendingParentWakeSignals.get(agent)?.();
|
|
275
369
|
await deliveryResult;
|
|
276
370
|
markReadAgentActivity(agent, "received lead message", "thinking");
|
|
@@ -350,6 +444,7 @@ async function recordReadAgentReportEvent(
|
|
|
350
444
|
metadata: {
|
|
351
445
|
...(member.prompt ? { initialPrompt: member.prompt } : {}),
|
|
352
446
|
...(modelSlot ? { modelSlot } : {}),
|
|
447
|
+
...(member.parentLifecycleRunId ? { parentLifecycleRunId: member.parentLifecycleRunId } : {}),
|
|
353
448
|
...reportMetadata,
|
|
354
449
|
},
|
|
355
450
|
});
|
|
@@ -690,7 +785,9 @@ export async function runReadAgentInProcess(
|
|
|
690
785
|
if (pendingChildParent && !pendingChildController) {
|
|
691
786
|
throw new Error(`Eligible nested read parent ${member.name} requires a pending child controller.`);
|
|
692
787
|
}
|
|
693
|
-
const
|
|
788
|
+
const parsedModel = parseQualifiedModel(member.model || "");
|
|
789
|
+
const provider = parsedModel?.provider;
|
|
790
|
+
const modelId = parsedModel?.model;
|
|
694
791
|
const model = provider && modelId ? ctx.modelRegistry.find(provider, modelId) : undefined;
|
|
695
792
|
if (!model) {
|
|
696
793
|
throw new Error(`Read agent model "${member.model}" is not available.`);
|
|
@@ -769,6 +866,7 @@ export async function runReadAgentInProcess(
|
|
|
769
866
|
? [
|
|
770
867
|
"This opted-in depth-0 write-feature/write-critical run may use restricted spawn_agent or spawn_swarm_agents for depth-1 read-only helpers. Choose any canonical read-* tier and any helper count, subject to the team's global read capacity and queue. Children report to you and cannot delegate.",
|
|
771
868
|
"Use spawn_agent with only name (optional), prompt, and model_slot. Use spawn_swarm_agents with optional defaults.model_slot plus agents using only name (optional), prompt, and model_slot.",
|
|
869
|
+
"When waiting on children, end your turn without calling report_and_exit; each child report resumes you automatically. One get_agent_status snapshot is allowed when current status is needed, but never call it repeatedly.",
|
|
772
870
|
"Do not request write tiers, cwd/team overrides, metadata, replacement of an active/queued name, or delegation outside your assigned scope.",
|
|
773
871
|
]
|
|
774
872
|
: ["You cannot spawn or create other agents. If another agent is needed, use send_message to ask team-lead; only the lead decides and performs the spawn."]),
|
|
@@ -908,10 +1006,51 @@ export async function runReadAgentInProcess(
|
|
|
908
1006
|
);
|
|
909
1007
|
};
|
|
910
1008
|
|
|
1009
|
+
const waitForPostInterruptOperation = async (
|
|
1010
|
+
interruptedGeneration: number,
|
|
1011
|
+
initialWakeGeneration: number,
|
|
1012
|
+
): Promise<boolean> => {
|
|
1013
|
+
state.operationAwaitingResume = true;
|
|
1014
|
+
let observedOperationGeneration = interruptedGeneration;
|
|
1015
|
+
let observedWakeGeneration = initialWakeGeneration;
|
|
1016
|
+
try {
|
|
1017
|
+
while (!state.stopRequested && options.isCurrentReadAgentRun(key, state)) {
|
|
1018
|
+
const currentWakeGeneration = readAgentWakeGeneration(state);
|
|
1019
|
+
if (currentWakeGeneration === observedWakeGeneration) {
|
|
1020
|
+
const wake = waitForReadAgentWake(state, observedWakeGeneration);
|
|
1021
|
+
await (state.finished ? Promise.race([wake, state.finished]) : wake);
|
|
1022
|
+
}
|
|
1023
|
+
observedWakeGeneration = readAgentWakeGeneration(state);
|
|
1024
|
+
if (state.stopRequested || !options.isCurrentReadAgentRun(key, state)) return false;
|
|
1025
|
+
|
|
1026
|
+
const deliveryTail = state.messageDeliveryTail;
|
|
1027
|
+
if (deliveryTail) await deliveryTail.catch(() => {});
|
|
1028
|
+
const completedGeneration = state.completedOperationGeneration ?? 0;
|
|
1029
|
+
if (completedGeneration <= observedOperationGeneration) continue;
|
|
1030
|
+
observedOperationGeneration = completedGeneration;
|
|
1031
|
+
if (state.completedOperationError) throw state.completedOperationError;
|
|
1032
|
+
if (state.completedOperationInterrupted) continue;
|
|
1033
|
+
return true;
|
|
1034
|
+
}
|
|
1035
|
+
return false;
|
|
1036
|
+
} finally {
|
|
1037
|
+
state.operationAwaitingResume = false;
|
|
1038
|
+
}
|
|
1039
|
+
};
|
|
1040
|
+
|
|
911
1041
|
state.acceptingMessages = true;
|
|
912
1042
|
try {
|
|
913
|
-
|
|
1043
|
+
const initialWakeGeneration = readAgentWakeGeneration(state);
|
|
1044
|
+
const initialOperation = await runReadAgentSessionOperation(
|
|
1045
|
+
state,
|
|
1046
|
+
() => session.prompt(prompt, { source: "extension" as any }),
|
|
1047
|
+
);
|
|
914
1048
|
refreshReadAgentStats(state, session);
|
|
1049
|
+
if (initialOperation.interrupted) {
|
|
1050
|
+
const resumed = await waitForPostInterruptOperation(initialOperation.generation, initialWakeGeneration);
|
|
1051
|
+
if (!resumed) return;
|
|
1052
|
+
refreshReadAgentStats(state, session);
|
|
1053
|
+
}
|
|
915
1054
|
if (pendingChildController && pendingChildParent) {
|
|
916
1055
|
while (!submittedFinalReport && !state.stopRequested) {
|
|
917
1056
|
const snapshot = pendingChildController.observeParent(pendingChildParent);
|
|
@@ -945,8 +1084,20 @@ export async function runReadAgentInProcess(
|
|
|
945
1084
|
markReadAgentActivity(state, "recovering missing report", "thinking");
|
|
946
1085
|
options.renderReadAgentStatus();
|
|
947
1086
|
try {
|
|
948
|
-
|
|
1087
|
+
const recoveryWakeGeneration = readAgentWakeGeneration(state);
|
|
1088
|
+
const recoveryOperation = await runReadAgentSessionOperation(
|
|
1089
|
+
state,
|
|
1090
|
+
() => session.prompt(EMPTY_REPORT_RECOVERY_PROMPT, { source: "extension" as any }),
|
|
1091
|
+
);
|
|
949
1092
|
refreshReadAgentStats(state, session);
|
|
1093
|
+
if (recoveryOperation.interrupted) {
|
|
1094
|
+
const resumed = await waitForPostInterruptOperation(
|
|
1095
|
+
recoveryOperation.generation,
|
|
1096
|
+
recoveryWakeGeneration,
|
|
1097
|
+
);
|
|
1098
|
+
if (!resumed) return;
|
|
1099
|
+
refreshReadAgentStats(state, session);
|
|
1100
|
+
}
|
|
950
1101
|
} catch (error) {
|
|
951
1102
|
throw unavailableReportError(
|
|
952
1103
|
`The report-only recovery turn failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
@@ -1105,6 +1256,7 @@ export async function runReadAgentInProcess(
|
|
|
1105
1256
|
}
|
|
1106
1257
|
} finally {
|
|
1107
1258
|
pendingParentWakeSignals.delete(state);
|
|
1259
|
+
disposeReadAgentWake(state);
|
|
1108
1260
|
settleSessionCreation(state.session);
|
|
1109
1261
|
// End run-owned activity even when persisted lifecycle cleanup is refused.
|
|
1110
1262
|
if (state.heartbeatTimer) clearInterval(state.heartbeatTimer);
|
|
@@ -3,7 +3,7 @@ import * as teams from "../../src/utils/teams";
|
|
|
3
3
|
import * as messaging from "../../src/utils/messaging";
|
|
4
4
|
import * as runtime from "../../src/utils/runtime";
|
|
5
5
|
import * as writeQueue from "../../src/utils/write-queue";
|
|
6
|
-
import { loadSettings,
|
|
6
|
+
import { loadSettings, normalizeFavoriteModelSlot, requireConfiguredFavoriteModel, roleForFavoriteModelSlot } from "../../src/utils/settings";
|
|
7
7
|
import type { Member } from "../../src/utils/models";
|
|
8
8
|
import { isTeamsDebugEnabled, teamDebugLogPath, writeTeamsDebugEvent } from "../internal/debug";
|
|
9
9
|
import { buildPiCommand, checkChildPiModelAvailability, getPiExtendedTeamsExtensionSource, getPiLaunchCommand } from "../internal/pi-command";
|
|
@@ -60,12 +60,15 @@ function stopAndVerifySpawnedPane(terminal: any, paneId: string): string | null
|
|
|
60
60
|
|
|
61
61
|
function assertWriterUsesConfiguredLevel(member: Member): void {
|
|
62
62
|
const settings = loadSettings({ projectDir: member.cwd });
|
|
63
|
-
const
|
|
64
|
-
|
|
63
|
+
const configured = requireConfiguredFavoriteModel(settings, member.modelSlot);
|
|
64
|
+
const slot = configured?.slot ?? normalizeFavoriteModelSlot(member.modelSlot);
|
|
65
|
+
if (!slot) throw new Error(`Unknown favorite model slot "${String(member.modelSlot)}".`);
|
|
66
|
+
const role = roleForFavoriteModelSlot(slot);
|
|
67
|
+
if (role !== "write" || member.role !== "write") {
|
|
65
68
|
throw new Error(`Write agent ${member.name} must use a write-* intent tier configured via /agents-favorite-models. Spawn agents by intent tier only.`);
|
|
66
69
|
}
|
|
67
|
-
if (member.model !==
|
|
68
|
-
throw new Error(`Write agent ${member.name} must use configured intent tier ${
|
|
70
|
+
if (configured && (member.model !== configured.config.model || member.thinking !== configured.config.thinking)) {
|
|
71
|
+
throw new Error(`Write agent ${member.name} must use configured intent tier ${configured.slot}; direct model/thinking overrides are not allowed.`);
|
|
69
72
|
}
|
|
70
73
|
}
|
|
71
74
|
|
|
@@ -14,7 +14,7 @@ import { generateLifecycleRunId } from "../../src/utils/lifecycle-tombstone";
|
|
|
14
14
|
import { cleanupStaleSessionContextReferences } from "../internal/session-context-reference";
|
|
15
15
|
import { cleanupStalePrivateAgentSessions } from "../internal/agent-session-files";
|
|
16
16
|
|
|
17
|
-
export const LEAD_ORCHESTRATION_GUIDANCE = `\n\npi-extended-teams lead orchestration rules:\n- Choose tiers by the agent's intended outcome, not by vague task importance. read-review is the normal default for focused review, verification, and bounded synthesis.\n- Use read-collect when the lane gathers bounded facts without owning the conclusion. Use read-analyze when it must explain behavior or root cause across connected evidence. Reserve read-critical for irreducible high-stakes security, architecture, concurrency, migration, or data-correctness reasoning.\n- For edits, use write-patch for a narrow localized change, write-feature for a bounded feature with a known design, write-system for a cross-cutting integration/refactor within explicitly claimed files, and write-critical only for high-risk security, concurrency, recovery, migration, or data-integrity changes.\n- Prefer the canonical read-*/write-* tiers. Legacy reading-*/writing-* names are compatibility aliases for this minor release, not intent guidance.\n- A spawned agent owns its assigned lane until it reports, blocks, fails, or the user cancels it. Do not duplicate, take over, test, edit, or synthesize that same lane in parallel; work only on clearly unrelated lanes.\n- When no unrelated work remains,
|
|
17
|
+
export const LEAD_ORCHESTRATION_GUIDANCE = `\n\npi-extended-teams lead orchestration rules:\n- Choose tiers by the agent's intended outcome, not by vague task importance. read-review is the normal default for focused review, verification, and bounded synthesis.\n- Use read-collect when the lane gathers bounded facts without owning the conclusion. Use read-analyze when it must explain behavior or root cause across connected evidence. Reserve read-critical for irreducible high-stakes security, architecture, concurrency, migration, or data-correctness reasoning.\n- For edits, use write-patch for a narrow localized change, write-feature for a bounded feature with a known design, write-system for a cross-cutting integration/refactor within explicitly claimed files, and write-critical only for high-risk security, concurrency, recovery, migration, or data-integrity changes.\n- Prefer the canonical read-*/write-* tiers. Legacy reading-*/writing-* names are compatibility aliases for this minor release, not intent guidance.\n- A spawned agent owns its assigned lane until it reports, blocks, fails, or the user cancels it. Do not duplicate, take over, test, edit, or synthesize that same lane in parallel; work only on clearly unrelated lanes.\n- When no unrelated work remains, end the turn. The extension resumes you when a report arrives. One get_agent_status snapshot is allowed when current status is needed; do not repeatedly call it, sleep, busy-wait, loop on inbox/status, send nudges, do dummy work, or treat healthy silence as failure.\n- Wait for the actual report before synthesizing. Intervene only on a reported blocker/error, actual health failure, or explicit user cancellation/change.\n- For durable bug, security, or testing claims from an agent report or backlog, concrete, reproducible findings with file/line evidence or a focused failing regression may proceed directly to TDD repair.
|
|
18
18
|
- Use a separate read-only confirmation only when evidence is missing or weak, the claim is disputed, or irreducible high-risk uncertainty remains; never reconfirm an already confirmed finding.`;
|
|
19
19
|
|
|
20
20
|
export interface RegisterEventsOptions {
|
package/extensions/index.ts
CHANGED
|
@@ -18,10 +18,12 @@ import { createReportProgressTool } from "./tools/agent-communication-tools.js";
|
|
|
18
18
|
import { registerTaskRuntimeTools } from "./tools/task-runtime-tools.js";
|
|
19
19
|
import { registerTeamTools, type TeamToolsRuntime } from "./tools/team-tools.js";
|
|
20
20
|
import { canonicalPersistedModelSlot, loadSettings, requireFavoriteModelLevel } from "../src/utils/settings";
|
|
21
|
+
import { parseQualifiedModel } from "../src/utils/model-resolution";
|
|
21
22
|
import { formatAnimatedProgress, formatContextUsage, formatElapsed, formatModelLabel } from "./ui/renderers.js";
|
|
22
23
|
import type { CompletedAgentReport, RunningReadAgent } from "./runtime/types.js";
|
|
23
24
|
import { createPendingChildController } from "./runtime/pending-child-controller.js";
|
|
24
25
|
import { createActiveAgentSleepController, runWithActiveAgentSleepAssertion } from "./runtime/active-agent-sleep.js";
|
|
26
|
+
import { createTeammateInterrupter } from "./runtime/teammate-interrupt.js";
|
|
25
27
|
export { panelBgFill, framePanel, frameWidget, frameWidgetFullWidth, logWindowStart } from "./ui/frame.js";
|
|
26
28
|
import * as messaging from "../src/utils/messaging";
|
|
27
29
|
import * as teams from "../src/utils/teams";
|
|
@@ -62,6 +64,9 @@ export default function (pi: ExtensionAPI) {
|
|
|
62
64
|
if (clearedTeamName === teamName) void drainReadHelperQueueOnce();
|
|
63
65
|
});
|
|
64
66
|
const runningReadAgents = new Map<string, RunningReadAgent>();
|
|
67
|
+
// Tmux writers have no nested AgentSession, but status rendering already
|
|
68
|
+
// maintains a lifecycle-fenced view of their active roster/runtime state.
|
|
69
|
+
const navigationWriterAgents = new Map<string, RunningReadAgent>();
|
|
65
70
|
const completedAgentReports = new Map<string, CompletedAgentReport[]>();
|
|
66
71
|
const TEAM_ACTIVITY_RENDER_DEBOUNCE_MS = 75;
|
|
67
72
|
let readAgentStatusTimer: NodeJS.Timeout | null = null;
|
|
@@ -91,8 +96,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
91
96
|
getProjectTrusted: (cwd) => parentProjectTrustForSpawn(sessionCtx, cwd),
|
|
92
97
|
createResourcePlan: createCurrentSpawnResourcePlan,
|
|
93
98
|
sleepController: activeAgentSleepController,
|
|
94
|
-
onWriterActive: (tab) => upsertWriterScreenTab(writerScreenState, tab),
|
|
95
|
-
onWriterInactive: (targetTeamName, member) => removeWriterScreenTab(writerScreenState, { teamName: targetTeamName, name: member.name, paneId: member.tmuxPaneId }),
|
|
99
|
+
onWriterActive: (tab) => { upsertWriterScreenTab(writerScreenState, tab); markTeamActivityStatusDirty(0); },
|
|
100
|
+
onWriterInactive: (targetTeamName, member) => { removeWriterScreenTab(writerScreenState, { teamName: targetTeamName, name: member.name, paneId: member.tmuxPaneId }); markTeamActivityStatusDirty(0); },
|
|
96
101
|
});
|
|
97
102
|
const { shutdownTeammate, startLeadWatchdog, stopLeadWatchdog } = createLifecycleRuntime({
|
|
98
103
|
isTeammate,
|
|
@@ -108,6 +113,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
108
113
|
onWriterInactive: (targetTeamName, member) => {
|
|
109
114
|
releaseWriteAgentSleepAssertion(targetTeamName, member);
|
|
110
115
|
removeWriterScreenTab(writerScreenState, { teamName: targetTeamName, name: member.name, paneId: member.tmuxPaneId });
|
|
116
|
+
markTeamActivityStatusDirty(0);
|
|
111
117
|
},
|
|
112
118
|
onTeammateClosing: (targetTeamName, member) => {
|
|
113
119
|
if (!member.lifecycleRunId) return;
|
|
@@ -140,6 +146,13 @@ export default function (pi: ExtensionAPI) {
|
|
|
140
146
|
return `${targetTeamName}:${targetAgentName}`;
|
|
141
147
|
}
|
|
142
148
|
|
|
149
|
+
const interruptTeammate = createTeammateInterrupter({
|
|
150
|
+
terminal,
|
|
151
|
+
runningReadAgents,
|
|
152
|
+
readAgentKey,
|
|
153
|
+
getTeamName: () => teamName,
|
|
154
|
+
});
|
|
155
|
+
|
|
143
156
|
function runInProcessAgentWithSleepAssertion(
|
|
144
157
|
...args: Parameters<typeof runReadAgentInProcess>
|
|
145
158
|
): ReturnType<typeof runReadAgentInProcess> {
|
|
@@ -232,6 +245,17 @@ export default function (pi: ExtensionAPI) {
|
|
|
232
245
|
&& runtimeHeartbeatIsRecent(runtimeStatus, now);
|
|
233
246
|
}
|
|
234
247
|
|
|
248
|
+
function navigationWriterAgent(teamName: string, member: Member, status: runtime.AgentRuntimeStatus): RunningReadAgent {
|
|
249
|
+
return {
|
|
250
|
+
runId: member.lifecycleRunId!, name: member.name, teamName,
|
|
251
|
+
startedAt: status.startedAt || member.joinedAt, tokensUsed: 0, contextUsage: status.contextUsage,
|
|
252
|
+
status: status.currentAction === "thinking" ? "thinking" : status.currentAction === "starting" ? "starting" : "working",
|
|
253
|
+
recentEvents: [], lastActivityAt: status.lastHeartbeatAt || member.joinedAt, role: "write",
|
|
254
|
+
model: member.model, thinking: member.thinking, modelSlot: canonicalPersistedModelSlot(member.modelSlot),
|
|
255
|
+
latestProgress: status.latestProgress,
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
|
|
235
259
|
function runtimeOnlyStatusLabel(status: runtime.AgentRuntimeStatus, now: number): string {
|
|
236
260
|
if (!runtimeHeartbeatIsRecent(status, now)) return "stale";
|
|
237
261
|
if (status.currentAction && status.currentAction !== "done") return status.currentAction;
|
|
@@ -592,6 +616,12 @@ export default function (pi: ExtensionAPI) {
|
|
|
592
616
|
.filter(member => member.name !== "team-lead" && member.isActive !== false && memberActivityRole(member) === "write")
|
|
593
617
|
.filter(member => !!(member.tmuxPaneId && terminal?.isAlive?.(member.tmuxPaneId)) && !runningReadAgents.has(readAgentKey(activityTeamName, member.name)) && !runtimeOnlyMemberNames.has(member.name)) ?? []
|
|
594
618
|
: [];
|
|
619
|
+
navigationWriterAgents.clear();
|
|
620
|
+
for (const { member, runtimeStatus } of runtimeOnlyWriteMembers) {
|
|
621
|
+
if (member.tmuxPaneId && member.lifecycleRunId === runtimeStatus.lifecycleRunId) {
|
|
622
|
+
navigationWriterAgents.set(member.name, navigationWriterAgent(activityTeamName!, member, runtimeStatus));
|
|
623
|
+
}
|
|
624
|
+
}
|
|
595
625
|
const unreadLeadMessages = activityTeamName ? await messaging.readInbox(activityTeamName, agentName, true, false).catch(() => []) : [];
|
|
596
626
|
leadInboxUnreadCount = unreadLeadMessages.length;
|
|
597
627
|
clearLeadInboxWidgetOnce();
|
|
@@ -779,7 +809,9 @@ export default function (pi: ExtensionAPI) {
|
|
|
779
809
|
}
|
|
780
810
|
const level = requireFavoriteModelLevel(loadSettings({ projectDir: queued.cwd }), queued.modelSlot);
|
|
781
811
|
if (level.role !== "read") throw new Error(`Read helper ${queued.name} requires a read-* intent tier configured via /agents-favorite-models, got ${level.slot}.`);
|
|
782
|
-
const
|
|
812
|
+
const parsedModel = parseQualifiedModel(level.model);
|
|
813
|
+
const provider = parsedModel?.provider;
|
|
814
|
+
const modelId = parsedModel?.model;
|
|
783
815
|
const model = provider && modelId ? sessionCtx.modelRegistry?.find?.(provider, modelId) : undefined;
|
|
784
816
|
if (!model) throw new Error(`Read helper model \"${level.model}\" from intent tier ${level.slot} is not available in the lead session.`);
|
|
785
817
|
|
|
@@ -1084,8 +1116,20 @@ export default function (pi: ExtensionAPI) {
|
|
|
1084
1116
|
if (!isTeammate) {
|
|
1085
1117
|
pi.on("session_start", async (_event: any, ctx: any) => {
|
|
1086
1118
|
installAgentNavigation(ctx, {
|
|
1087
|
-
getAgents: () =>
|
|
1088
|
-
|
|
1119
|
+
getAgents: () => {
|
|
1120
|
+
const readers = Array.from(runningReadAgents.values())
|
|
1121
|
+
.filter(agent => !teamName || agent.teamName === teamName);
|
|
1122
|
+
return [...readers, ...Array.from(navigationWriterAgents.values())
|
|
1123
|
+
.filter(agent => !teamName || agent.teamName === teamName)]
|
|
1124
|
+
.filter((agent, index, agents) => agents.findIndex(candidate => candidate.name === agent.name) === index);
|
|
1125
|
+
},
|
|
1126
|
+
interruptAgent: async (name: string) => {
|
|
1127
|
+
const result = await interruptTeammate(name);
|
|
1128
|
+
const warning = result.status === "pending"
|
|
1129
|
+
|| result.status === "unsupported"
|
|
1130
|
+
|| result.status === "failed";
|
|
1131
|
+
ctx.ui?.notify?.(result.message, warning ? "warning" : "info");
|
|
1132
|
+
},
|
|
1089
1133
|
stopAgent: async (name: string) => {
|
|
1090
1134
|
const targetTeamName = teamName;
|
|
1091
1135
|
if (!targetTeamName) return;
|
|
@@ -1222,6 +1266,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
1222
1266
|
terminal,
|
|
1223
1267
|
runningReadAgents,
|
|
1224
1268
|
readAgentKey,
|
|
1269
|
+
interruptTeammate,
|
|
1225
1270
|
shutdownTeammate,
|
|
1226
1271
|
getTeamName: () => teamName,
|
|
1227
1272
|
});
|
|
@@ -9,7 +9,7 @@ export const NESTED_READ_MODEL_SLOTS = [
|
|
|
9
9
|
|
|
10
10
|
export type NestedReadModelSlot = (typeof NESTED_READ_MODEL_SLOTS)[number];
|
|
11
11
|
|
|
12
|
-
export const NESTED_DELEGATION_TOOL_NAMES = ["spawn_agent", "spawn_swarm_agents"] as const;
|
|
12
|
+
export const NESTED_DELEGATION_TOOL_NAMES = ["get_agent_status", "spawn_agent", "spawn_swarm_agents"] as const;
|
|
13
13
|
|
|
14
14
|
const ELIGIBLE_PARENT_MODEL_SLOTS = new Set(["write-feature", "write-critical"]);
|
|
15
15
|
|