pi-crew 0.2.25 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +100 -32
- package/docs/TEST_MATRIX.md +17 -15
- package/docs/feature-analysis-subagent4.md +305 -0
- package/docs/pi-subagent4-comparison.md +261 -0
- package/package.json +1 -1
- package/src/agents/discover-agents.ts +74 -4
- package/src/extension/register.ts +28 -27
- package/src/extension/registration/subagent-tools.ts +7 -0
- package/src/extension/registration/team-tool.ts +7 -0
- package/src/extension/team-tool.ts +29 -2
- package/src/runtime/heartbeat-watcher.ts +17 -2
- package/src/runtime/tool-progress.ts +281 -0
- package/src/tools/safe-bash-extension.ts +95 -0
- package/src/tools/safe-bash.ts +188 -0
- package/src/ui/tool-render.ts +331 -0
- package/test-lastActivityAt.mjs +167 -0
- package/test-tp.mjs +12 -0
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
# pi-subagent4 vs pi-crew: Comparative Analysis
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
| Aspect | pi-subagent4 | pi-crew |
|
|
6
|
+
|--------|--------------|---------|
|
|
7
|
+
| **Size** | ~560 lines (single file) | ~50+ files, 10K+ lines |
|
|
8
|
+
| **Architecture** | Single `subagent` tool | Full team orchestration system |
|
|
9
|
+
| **Agent Model** | 3 built-in (scout, researcher, worker) | Configurable, extensible |
|
|
10
|
+
| **Concurrency** | Semaphore (default 4) | DAG scheduler with phases |
|
|
11
|
+
| **Context** | No inheritance (must be in task) | Full context preservation |
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## 1. Extension & Registration
|
|
16
|
+
|
|
17
|
+
### pi-subagent4
|
|
18
|
+
```typescript
|
|
19
|
+
// Dynamic agent registration via globalThis bridge
|
|
20
|
+
(globalThis as any).__pi_subagents = { registerAgent, unregisterAgent };
|
|
21
|
+
|
|
22
|
+
export function registerAgent(config: AgentConfig): void {
|
|
23
|
+
agents.push(config);
|
|
24
|
+
}
|
|
25
|
+
```
|
|
26
|
+
- **File-based**: Loads agents from `.md` files at startup
|
|
27
|
+
- **Global bridge**: Uses `globalThis.__pi_subagents` for cross-module registration
|
|
28
|
+
- **Frontmatter config**: YAML frontmatter in `.md` files define agents
|
|
29
|
+
|
|
30
|
+
### pi-crew
|
|
31
|
+
- **Manifest-based**: Teams/workflows defined in `.team.md`/`.workflow.md` files
|
|
32
|
+
- **Skills system**: Extensible skill system for agents
|
|
33
|
+
- **No dynamic registration API**: Static configuration
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
## 2. Child Process Spawning
|
|
38
|
+
|
|
39
|
+
### pi-subagent4
|
|
40
|
+
```typescript
|
|
41
|
+
const args = [
|
|
42
|
+
...piBin.baseArgs,
|
|
43
|
+
"--mode", "json",
|
|
44
|
+
"-p",
|
|
45
|
+
"--no-session",
|
|
46
|
+
"--no-skills",
|
|
47
|
+
"--no-extensions",
|
|
48
|
+
"--tools", allowlist.join(","),
|
|
49
|
+
// ... custom tools, model, thinking level
|
|
50
|
+
];
|
|
51
|
+
|
|
52
|
+
const child = spawn(command, spawnArgs, { stdio: ["ignore", "pipe", "pipe"] });
|
|
53
|
+
```
|
|
54
|
+
- **JSON mode**: `--mode json` for structured output
|
|
55
|
+
- **Heavy isolation**: `--no-session --no-skills --no-extensions`
|
|
56
|
+
- **Tool allowlist**: `--tools` for fine-grained control
|
|
57
|
+
- **PI_SUBAGENT_ALLOWED**: Env var restricts nested subagents
|
|
58
|
+
|
|
59
|
+
### pi-crew
|
|
60
|
+
```typescript
|
|
61
|
+
const child = spawn(spawnSpec.command, spawnSpec.args, buildChildPiSpawnOptions(...));
|
|
62
|
+
```
|
|
63
|
+
- **Similar isolation**: Filters env vars, preserves essentials
|
|
64
|
+
- **More complex args**: Based on task config
|
|
65
|
+
- **No direct env restriction**: Uses runtime mode instead
|
|
66
|
+
|
|
67
|
+
---
|
|
68
|
+
|
|
69
|
+
## 3. Concurrency Control
|
|
70
|
+
|
|
71
|
+
### pi-subagent4
|
|
72
|
+
```typescript
|
|
73
|
+
class Semaphore {
|
|
74
|
+
constructor(private readonly max: number) {}
|
|
75
|
+
|
|
76
|
+
async run<T>(fn: () => Promise<T>): Promise<T> {
|
|
77
|
+
// Simple acquire/release pattern
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const semaphore = new Semaphore(config.maxConcurrency ?? 4);
|
|
82
|
+
```
|
|
83
|
+
- **Per-parent semaphore**: Default 4, configurable via `config.json`
|
|
84
|
+
- **Promise.all fan-out**: Parallel subagent calls in one turn
|
|
85
|
+
|
|
86
|
+
### pi-crew
|
|
87
|
+
```typescript
|
|
88
|
+
// DAG scheduler with phase-based concurrency
|
|
89
|
+
resolveBatchConcurrency({
|
|
90
|
+
workflowMaxConcurrency,
|
|
91
|
+
teamMaxConcurrency,
|
|
92
|
+
maxConcurrentWorkers,
|
|
93
|
+
workspaceMode,
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
// Tasks in same phase run concurrently
|
|
97
|
+
```
|
|
98
|
+
- **Phase-based**: Tasks grouped by workflow phase
|
|
99
|
+
- **DAG dependency**: Respects task dependencies
|
|
100
|
+
- **Configurable limits**: Per-workflow and per-team
|
|
101
|
+
|
|
102
|
+
---
|
|
103
|
+
|
|
104
|
+
## 4. Input Handling
|
|
105
|
+
|
|
106
|
+
### pi-subagent4
|
|
107
|
+
```typescript
|
|
108
|
+
// Long tasks written to temp file
|
|
109
|
+
if (task.length > 8000) {
|
|
110
|
+
const tempFile = createTempFile(task);
|
|
111
|
+
args.push("@" + tempFile);
|
|
112
|
+
} else {
|
|
113
|
+
args.push("--task", task);
|
|
114
|
+
}
|
|
115
|
+
```
|
|
116
|
+
- **8K char threshold**: Uses temp file for large tasks
|
|
117
|
+
- **Single task format**: `--task <text>` or `@<file>`
|
|
118
|
+
|
|
119
|
+
### pi-crew
|
|
120
|
+
```typescript
|
|
121
|
+
// Prompt builder with system prompt, context, task
|
|
122
|
+
const built = await buildPrompt({ task, role, goal, cwd, ... });
|
|
123
|
+
// Args built from task configuration
|
|
124
|
+
```
|
|
125
|
+
- **Prompt builder**: Constructs full prompt with context
|
|
126
|
+
- **File-based context**: Can read from workspace files
|
|
127
|
+
|
|
128
|
+
---
|
|
129
|
+
|
|
130
|
+
## 5. Output Handling
|
|
131
|
+
|
|
132
|
+
### pi-subagent4
|
|
133
|
+
```typescript
|
|
134
|
+
// JSON event stream on stdout
|
|
135
|
+
child.stdout.on("data", (data) => {
|
|
136
|
+
const lines = data.toString().split("\n");
|
|
137
|
+
for (const line of lines) {
|
|
138
|
+
if (line.startsWith("{")) {
|
|
139
|
+
const event = JSON.parse(line);
|
|
140
|
+
// tool_execution_start/end, message_end
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
```
|
|
145
|
+
- **JSON event stream**: Structured events from child process
|
|
146
|
+
- **Event types**: `tool_execution_start`, `tool_execution_end`, `message_end`
|
|
147
|
+
- **Streaming**: Real-time event processing
|
|
148
|
+
|
|
149
|
+
### pi-crew
|
|
150
|
+
```typescript
|
|
151
|
+
// JSON output mode + structured response
|
|
152
|
+
const output = await runChildPi({
|
|
153
|
+
onLifecycleEvent: (event) => { ... },
|
|
154
|
+
// ...
|
|
155
|
+
});
|
|
156
|
+
```
|
|
157
|
+
- **Lifecycle events**: spawn, spawn_error, response_timeout, etc.
|
|
158
|
+
- **Structured result**: `{ content, details, usage }`
|
|
159
|
+
|
|
160
|
+
---
|
|
161
|
+
|
|
162
|
+
## 6. Safety Features
|
|
163
|
+
|
|
164
|
+
### pi-subagent4
|
|
165
|
+
```typescript
|
|
166
|
+
// tools/safe-bash.ts
|
|
167
|
+
const DANGEROUS_PATTERNS = [
|
|
168
|
+
/\brm\s+(-[a-zA-Z]*f[a-zA-Z]*\s+)?(-[a-zA-Z]*r[a-zA-Z]*\s+)?(\/|~\/?\s|~\/?\b)/,
|
|
169
|
+
/\bsudo\b/,
|
|
170
|
+
/\bmkfs\b/,
|
|
171
|
+
// ... 15+ patterns
|
|
172
|
+
];
|
|
173
|
+
```
|
|
174
|
+
- **Regex blocklist**: 15+ dangerous command patterns
|
|
175
|
+
- **Safe bash wrapper**: Wraps built-in bash tool
|
|
176
|
+
|
|
177
|
+
### pi-crew
|
|
178
|
+
- **Env var filtering**: Strips secrets before spawning
|
|
179
|
+
- **No built-in safe bash**: Trust-based (user config required)
|
|
180
|
+
- **Sandbox modes**: scaffold, child-process, live-session
|
|
181
|
+
|
|
182
|
+
---
|
|
183
|
+
|
|
184
|
+
## 7. UI/Rendering
|
|
185
|
+
|
|
186
|
+
### pi-subagent4
|
|
187
|
+
```typescript
|
|
188
|
+
// Throttled live rendering
|
|
189
|
+
const updateThrottle = 150; // ms
|
|
190
|
+
// Context window meter for depth >= 1 subagents
|
|
191
|
+
// Tool preview extraction
|
|
192
|
+
```
|
|
193
|
+
- **150ms throttle**: Prevents UI thrashing
|
|
194
|
+
- **Context gauge**: Shows token usage
|
|
195
|
+
- **Tool preview**: Single-line argument preview
|
|
196
|
+
|
|
197
|
+
### pi-crew
|
|
198
|
+
- **Rich UI widget**: Live status, progress, model/token display
|
|
199
|
+
- **Dashboard**: Full run dashboard
|
|
200
|
+
- **Event bus**: Real-time updates
|
|
201
|
+
|
|
202
|
+
---
|
|
203
|
+
|
|
204
|
+
## 8. Agent Hierarchy
|
|
205
|
+
|
|
206
|
+
### pi-subagent4
|
|
207
|
+
```
|
|
208
|
+
worker (depth 2)
|
|
209
|
+
├─ scout (depth 1)
|
|
210
|
+
└─ researcher (depth 1)
|
|
211
|
+
```
|
|
212
|
+
- **Depth-2 cap**: Worker can spawn scout/researcher
|
|
213
|
+
- **PI_SUBAGENT_ALLOWED**: Enforces restriction
|
|
214
|
+
|
|
215
|
+
### pi-crew
|
|
216
|
+
- **No nested subagent**: Each task is independent
|
|
217
|
+
- **Team roles**: explorer, planner, executor, etc.
|
|
218
|
+
- **Phase-based**: Sequential phases with parallel within
|
|
219
|
+
|
|
220
|
+
---
|
|
221
|
+
|
|
222
|
+
## Key Insights
|
|
223
|
+
|
|
224
|
+
### What pi-subagent4 does better:
|
|
225
|
+
1. **Simpler API**: Single tool, minimal config
|
|
226
|
+
2. **Dynamic registration**: `registerAgent()` for runtime changes
|
|
227
|
+
3. **JSON event stream**: Real-time structured events
|
|
228
|
+
4. **Safe bash**: Built-in dangerous command blocking
|
|
229
|
+
5. **Context gauge**: Token monitoring per turn
|
|
230
|
+
|
|
231
|
+
### What pi-crew does better:
|
|
232
|
+
1. **Complex workflows**: DAG scheduler, phases, dependencies
|
|
233
|
+
2. **Durable state**: Manifest, events, artifacts persisted
|
|
234
|
+
3. **Worktree isolation**: Safe parallel edits
|
|
235
|
+
4. **Async runs**: Background execution with notifications
|
|
236
|
+
5. **Rich UI**: Full dashboard and widget system
|
|
237
|
+
6. **Multiple teams**: Built-in teams for different use cases
|
|
238
|
+
|
|
239
|
+
---
|
|
240
|
+
|
|
241
|
+
## Potential Improvements for pi-crew
|
|
242
|
+
|
|
243
|
+
1. **Dynamic agent registration API**
|
|
244
|
+
- Add `registerAgent(config)` similar to subagent4
|
|
245
|
+
- Allow runtime agent creation
|
|
246
|
+
|
|
247
|
+
2. **Safe bash tool**
|
|
248
|
+
- Port dangerous pattern blocklist from subagent4
|
|
249
|
+
- Configurable via project config
|
|
250
|
+
|
|
251
|
+
3. **JSON event stream parsing**
|
|
252
|
+
- Extract real-time tool events from child process
|
|
253
|
+
- Display tool progress in UI
|
|
254
|
+
|
|
255
|
+
4. **Context window monitoring**
|
|
256
|
+
- Show token usage per task
|
|
257
|
+
- Alert when approaching limits
|
|
258
|
+
|
|
259
|
+
5. **Simpler single-agent mode**
|
|
260
|
+
- Maybe a `subagent` tool for simple delegation?
|
|
261
|
+
- Current API is team/workflow based, could be heavy for simple cases
|
package/package.json
CHANGED
|
@@ -100,22 +100,92 @@ function applyAgentOverrides(agents: AgentConfig[], cwd: string, loadedConfig?:
|
|
|
100
100
|
});
|
|
101
101
|
}
|
|
102
102
|
|
|
103
|
+
// ─── Agent Discovery Cache (Phase 3a) ────────────────────────────────────
|
|
104
|
+
// Caches discoverAgents results by cwd with a short TTL to avoid repeated
|
|
105
|
+
// disk I/O when multiple callers request agents for the same project.
|
|
106
|
+
|
|
107
|
+
const DISCOVERY_CACHE_TTL_MS = 500;
|
|
108
|
+
const discoveryCache = new Map<string, { result: AgentDiscoveryResult; expiresAt: number }>();
|
|
109
|
+
const DISCOVERY_CACHE_MAX_ENTRIES = 32;
|
|
110
|
+
|
|
111
|
+
function pruneDiscoveryCache(): void {
|
|
112
|
+
const now = Date.now();
|
|
113
|
+
for (const [key, entry] of discoveryCache) {
|
|
114
|
+
if (entry.expiresAt <= now) discoveryCache.delete(key);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** Invalidate cached discovery result for a given cwd (or all if omitted). */
|
|
119
|
+
export function invalidateAgentDiscoveryCache(cwd?: string): void {
|
|
120
|
+
if (cwd) {
|
|
121
|
+
discoveryCache.delete(cwd);
|
|
122
|
+
} else {
|
|
123
|
+
discoveryCache.clear();
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
103
127
|
export function discoverAgents(cwd: string): AgentDiscoveryResult {
|
|
128
|
+
pruneDiscoveryCache();
|
|
129
|
+
const cached = discoveryCache.get(cwd);
|
|
130
|
+
if (cached && cached.expiresAt > Date.now()) {
|
|
131
|
+
return cached.result;
|
|
132
|
+
}
|
|
104
133
|
const loaded = loadConfig(cwd);
|
|
105
|
-
|
|
134
|
+
const result: AgentDiscoveryResult = {
|
|
106
135
|
builtin: applyAgentOverrides(readAgentDir(path.join(packageRoot(), "agents"), "builtin"), cwd, loaded),
|
|
107
136
|
user: applyAgentOverrides(readAgentDir(path.join(userPiRoot(), "agents"), "user"), cwd, loaded),
|
|
108
137
|
project: applyAgentOverrides(readAgentDir(path.join(projectCrewRoot(cwd), "agents"), "project"), cwd, loaded),
|
|
109
138
|
};
|
|
139
|
+
discoveryCache.set(cwd, { result, expiresAt: Date.now() + DISCOVERY_CACHE_TTL_MS });
|
|
140
|
+
while (discoveryCache.size > DISCOVERY_CACHE_MAX_ENTRIES) {
|
|
141
|
+
const oldest = discoveryCache.keys().next().value;
|
|
142
|
+
if (oldest !== undefined) discoveryCache.delete(oldest);
|
|
143
|
+
}
|
|
144
|
+
return result;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// ─── Dynamic Agent Registry (Phase 3b) ───────────────────────────────────
|
|
148
|
+
// In-memory store for runtime-registered agents. Merged into discovery results
|
|
149
|
+
// with highest priority (after project agents).
|
|
150
|
+
|
|
151
|
+
const dynamicAgents = new Map<string, AgentConfig>();
|
|
152
|
+
|
|
153
|
+
/** Register a dynamic agent at runtime. Throws if already registered. */
|
|
154
|
+
export function registerDynamicAgent(config: AgentConfig): void {
|
|
155
|
+
const key = config.name.toLowerCase();
|
|
156
|
+
if (dynamicAgents.has(key)) {
|
|
157
|
+
throw new Error(`Agent already registered: ${config.name}`);
|
|
158
|
+
}
|
|
159
|
+
dynamicAgents.set(key, { ...config, source: config.source ?? "project" });
|
|
160
|
+
invalidateAgentDiscoveryCache();
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** Unregister a previously registered dynamic agent. Throws if not found. */
|
|
164
|
+
export function unregisterDynamicAgent(name: string): void {
|
|
165
|
+
const removed = dynamicAgents.delete(name.toLowerCase());
|
|
166
|
+
if (!removed) {
|
|
167
|
+
throw new Error(`Agent not found: ${name}`);
|
|
168
|
+
}
|
|
169
|
+
invalidateAgentDiscoveryCache();
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/** List all currently registered dynamic agents. */
|
|
173
|
+
export function listDynamicAgents(): AgentConfig[] {
|
|
174
|
+
return [...dynamicAgents.values()];
|
|
110
175
|
}
|
|
111
176
|
|
|
112
177
|
export function allAgents(discovery: AgentDiscoveryResult): AgentConfig[] {
|
|
113
178
|
const byName = new Map<string, AgentConfig>();
|
|
114
|
-
// Priority: project
|
|
115
|
-
//
|
|
116
|
-
//
|
|
179
|
+
// Priority for disambiguation (security): project < builtin < user.
|
|
180
|
+
// Project config cannot override trusted builtins (security-hardening).
|
|
181
|
+
// Later entries in the loop overwrite earlier ones, so user wins.
|
|
117
182
|
for (const agent of [...discovery.project, ...discovery.builtin, ...discovery.user]) {
|
|
118
183
|
byName.set(agent.name.toLowerCase(), agent);
|
|
119
184
|
}
|
|
185
|
+
// Dynamic agents (registered at runtime) take highest precedence.
|
|
186
|
+
// They can override any discovered agent (project/builtin/user).
|
|
187
|
+
for (const agent of dynamicAgents.values()) {
|
|
188
|
+
byName.set(agent.name.toLowerCase(), agent);
|
|
189
|
+
}
|
|
120
190
|
return [...byName.values()].filter((agent) => !agent.disabled).sort((a, b) => a.name.localeCompare(b.name));
|
|
121
191
|
}
|
|
@@ -445,35 +445,36 @@ export function registerPiTeams(pi: ExtensionAPI): void {
|
|
|
445
445
|
// Register global RPC registry for cross-extension access (mirrors pi-subagents3's Symbol.for pattern)
|
|
446
446
|
// Uses lazy import to avoid pulling team-tool.ts into module load.
|
|
447
447
|
// Other extensions access via: const reg = globalThis[Symbol.for("pi-crew:registry")];
|
|
448
|
-
void import("./team-tool.ts").then(({ registerCrewGlobalRegistry }) => {
|
|
448
|
+
void import("./team-tool.ts").then(({ registerCrewGlobalRegistry, installCrewGlobalRegistry }) => {
|
|
449
|
+
// Phase 3b: installCrewGlobalRegistry creates a v2 registry with agent registration API.
|
|
450
|
+
// We then patch the manifest-backed methods with real implementations below.
|
|
449
451
|
const manifestCacheForRegistry = getManifestCache(currentCtx?.cwd ?? process.cwd());
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
},
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
const loaded = loadRunManifestById(currentCtx?.cwd ?? process.cwd(), runId);
|
|
463
|
-
if (!loaded) return true;
|
|
464
|
-
return !loaded.tasks.some((t: { status: string }) => t.status === "running" || t.status === "queued");
|
|
465
|
-
};
|
|
466
|
-
while (!check()) await new Promise((resolve) => setTimeout(resolve, 500));
|
|
467
|
-
},
|
|
468
|
-
hasRunning: (runId) => {
|
|
469
|
-
const manifest = manifestCacheForRegistry.get(runId);
|
|
470
|
-
if (!manifest) return false;
|
|
471
|
-
const { loadRunManifestById } = require("../state/state-store.ts");
|
|
452
|
+
installCrewGlobalRegistry();
|
|
453
|
+
const CREW_REGISTRY_KEY = Symbol.for("pi-crew:registry");
|
|
454
|
+
const registry = (globalThis as Record<symbol | string, unknown>)[CREW_REGISTRY_KEY] as Record<string, unknown>;
|
|
455
|
+
registry.getRecord = (runId: string) => manifestCacheForRegistry.get(runId);
|
|
456
|
+
registry.listRuns = () => manifestCacheForRegistry.list(100).map((m: { runId: string; status: string; goal: string }) => ({ runId: m.runId, status: m.status, goal: m.goal }));
|
|
457
|
+
registry.appendEvent = (runId: string, event: Record<string, unknown>) => {
|
|
458
|
+
const manifest = manifestCacheForRegistry.get(runId);
|
|
459
|
+
if (manifest) void import("../state/event-log.ts").then(({ appendEventFireAndForget }) => appendEventFireAndForget(manifest.eventsPath, event as Parameters<typeof appendEventFireAndForget>[1]));
|
|
460
|
+
};
|
|
461
|
+
registry.waitForAll = async (runId: string) => {
|
|
462
|
+
const { loadRunManifestById } = await import("../state/state-store.ts");
|
|
463
|
+
const check = (): boolean => {
|
|
472
464
|
const loaded = loadRunManifestById(currentCtx?.cwd ?? process.cwd(), runId);
|
|
473
|
-
if (!loaded) return
|
|
474
|
-
return loaded.tasks.some((t: { status: string }) => t.status === "running" || t.status === "queued");
|
|
475
|
-
}
|
|
476
|
-
|
|
465
|
+
if (!loaded) return true;
|
|
466
|
+
return !loaded.tasks.some((t: { status: string }) => t.status === "running" || t.status === "queued");
|
|
467
|
+
};
|
|
468
|
+
while (!check()) await new Promise((resolve) => setTimeout(resolve, 500));
|
|
469
|
+
};
|
|
470
|
+
registry.hasRunning = (runId: string) => {
|
|
471
|
+
const manifest = manifestCacheForRegistry.get(runId);
|
|
472
|
+
if (!manifest) return false;
|
|
473
|
+
const { loadRunManifestById } = require("../state/state-store.ts");
|
|
474
|
+
const loaded = loadRunManifestById(currentCtx?.cwd ?? process.cwd(), runId);
|
|
475
|
+
if (!loaded) return false;
|
|
476
|
+
return loaded.tasks.some((t: { status: string }) => t.status === "running" || t.status === "queued");
|
|
477
|
+
};
|
|
477
478
|
});
|
|
478
479
|
|
|
479
480
|
const cleanupRuntime = (): void => {
|
|
@@ -22,6 +22,7 @@ import { t } from "../../i18n.ts";
|
|
|
22
22
|
import { loadRunManifestById } from "../../state/state-store.ts";
|
|
23
23
|
import { readCrewAgents } from "../../runtime/crew-agent-records.ts";
|
|
24
24
|
import { formatCompactToolProgress } from "../../ui/tool-progress-formatter.ts";
|
|
25
|
+
import { renderAgentToolCall, renderAgentToolResult } from "../../ui/tool-render.ts";
|
|
25
26
|
|
|
26
27
|
const TOOL_PROGRESS_TICK_MS = 1000;
|
|
27
28
|
|
|
@@ -95,6 +96,12 @@ export function registerSubagentTools(pi: ExtensionAPI, subagentManager: Subagen
|
|
|
95
96
|
}
|
|
96
97
|
return foregroundResult;
|
|
97
98
|
},
|
|
99
|
+
renderCall(args: any, theme: any, context: any): any {
|
|
100
|
+
return renderAgentToolCall(args, theme, context);
|
|
101
|
+
},
|
|
102
|
+
renderResult(result: any, options: any, theme: any, context: any): any {
|
|
103
|
+
return renderAgentToolResult(result, options, theme, context);
|
|
104
|
+
},
|
|
98
105
|
};
|
|
99
106
|
|
|
100
107
|
const getSubagentResultTool: ToolDefinition = {
|
|
@@ -9,6 +9,7 @@ import type { createManifestCache } from "../../runtime/manifest-cache.ts";
|
|
|
9
9
|
import type { createRunSnapshotCache } from "../../ui/run-snapshot-cache.ts";
|
|
10
10
|
import type { MetricRegistry } from "../../observability/metric-registry.ts";
|
|
11
11
|
import { resolveRealContainedPath } from "../../utils/safe-paths.ts";
|
|
12
|
+
import { renderTeamToolCall, renderTeamToolResult } from "../../ui/tool-render.ts";
|
|
12
13
|
// Team tool handler — lazy-loaded because team-tool.ts imports many modules
|
|
13
14
|
import type { handleTeamTool as HandleTeamToolFn } from "../team-tool.ts";
|
|
14
15
|
let _cachedHandleTeamTool: typeof HandleTeamToolFn | undefined;
|
|
@@ -104,6 +105,12 @@ export function registerTeamTool(pi: ExtensionAPI, deps: RegisterTeamToolDeps):
|
|
|
104
105
|
stopProgress.stop();
|
|
105
106
|
}
|
|
106
107
|
},
|
|
108
|
+
renderCall(args: any, theme: any, context: any): any {
|
|
109
|
+
return renderTeamToolCall(args, theme, context);
|
|
110
|
+
},
|
|
111
|
+
renderResult(result: any, options: any, theme: any, context: any): any {
|
|
112
|
+
return renderTeamToolResult(result, options, theme, context);
|
|
113
|
+
},
|
|
107
114
|
};
|
|
108
115
|
pi.registerTool(tool);
|
|
109
116
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
2
|
import * as path from "node:path";
|
|
3
|
-
import { allAgents, discoverAgents } from "../agents/discover-agents.ts";
|
|
3
|
+
import { allAgents, discoverAgents, invalidateAgentDiscoveryCache, registerDynamicAgent, unregisterDynamicAgent, listDynamicAgents } from "../agents/discover-agents.ts";
|
|
4
|
+
import type { AgentConfig } from "../agents/agent-config.ts";
|
|
4
5
|
import { allTeams, discoverTeams } from "../teams/discover-teams.ts";
|
|
5
6
|
import { allWorkflows, discoverWorkflows } from "../workflows/discover-workflows.ts";
|
|
6
7
|
import { loadConfig, updateAutonomousConfig, updateConfig } from "../config/config.ts";
|
|
@@ -383,14 +384,25 @@ export async function handleTeamTool(params: TeamToolParamsValue, ctx: TeamConte
|
|
|
383
384
|
*/
|
|
384
385
|
const CREW_REGISTRY_KEY = Symbol.for("pi-crew:registry");
|
|
385
386
|
interface CrewRegistry {
|
|
386
|
-
version:
|
|
387
|
+
version: 2;
|
|
387
388
|
getRecord: (runId: string) => TeamRunManifest | undefined;
|
|
388
389
|
listRuns: () => Array<{ runId: string; status: string; goal: string }>;
|
|
389
390
|
appendEvent: (runId: string, event: Record<string, unknown>) => void;
|
|
390
391
|
waitForAll: (runId: string) => Promise<void>;
|
|
391
392
|
hasRunning: (runId: string) => boolean;
|
|
393
|
+
/** Register a dynamic agent at runtime. Invalidates the discovery cache. */
|
|
394
|
+
registerAgent: (config: AgentConfig) => void;
|
|
395
|
+
/** Unregister a previously registered dynamic agent. Invalidates the discovery cache. */
|
|
396
|
+
unregisterAgent: (name: string) => void;
|
|
397
|
+
/** List all currently registered dynamic agents. */
|
|
398
|
+
listDynamicAgents: () => AgentConfig[];
|
|
392
399
|
}
|
|
393
400
|
|
|
401
|
+
// ─── Dynamic Agent Registry (Phase 3b) ───────────────────────────────────
|
|
402
|
+
// The dynamic agent store lives in discover-agents.ts and is merged into
|
|
403
|
+
// discovery results with highest priority. The CrewRegistry interface exposes
|
|
404
|
+
// registerAgent/unregisterAgent/listDynamicAgents for cross-extension access.
|
|
405
|
+
|
|
394
406
|
export function registerCrewGlobalRegistry(registry: CrewRegistry): void {
|
|
395
407
|
(globalThis as Record<symbol | string, unknown>)[CREW_REGISTRY_KEY] = registry;
|
|
396
408
|
}
|
|
@@ -398,3 +410,18 @@ export function registerCrewGlobalRegistry(registry: CrewRegistry): void {
|
|
|
398
410
|
export function getCrewGlobalRegistry(): CrewRegistry | undefined {
|
|
399
411
|
return (globalThis as Record<symbol | string, unknown>)[CREW_REGISTRY_KEY] as CrewRegistry | undefined;
|
|
400
412
|
}
|
|
413
|
+
|
|
414
|
+
/** Create and install the global CrewRegistry singleton. Call once at extension init. */
|
|
415
|
+
export function installCrewGlobalRegistry(): void {
|
|
416
|
+
registerCrewGlobalRegistry({
|
|
417
|
+
version: 2,
|
|
418
|
+
getRecord: (runId: string) => undefined as unknown as TeamRunManifest,
|
|
419
|
+
listRuns: () => [],
|
|
420
|
+
appendEvent: () => {},
|
|
421
|
+
waitForAll: async () => {},
|
|
422
|
+
hasRunning: () => false,
|
|
423
|
+
registerAgent: registerDynamicAgent,
|
|
424
|
+
unregisterAgent: unregisterDynamicAgent,
|
|
425
|
+
listDynamicAgents,
|
|
426
|
+
});
|
|
427
|
+
}
|
|
@@ -96,8 +96,23 @@ export class HeartbeatWatcher {
|
|
|
96
96
|
activeKeys.add(key);
|
|
97
97
|
this.lastSeen.set(key, now);
|
|
98
98
|
|
|
99
|
-
|
|
100
|
-
|
|
99
|
+
// Check heartbeat staleness with lastActivityAt fallback
|
|
100
|
+
let elapsed = heartbeatAgeMs(task.heartbeat, now);
|
|
101
|
+
// PR #6 partial: use lastActivityAt as fallback when heartbeat is stale
|
|
102
|
+
// If heartbeat is stale but lastActivityAt is fresher, use activity age instead.
|
|
103
|
+
// This prevents false-positive dead detection for live-session tasks during long operations.
|
|
104
|
+
if (task.agentProgress?.lastActivityAt) {
|
|
105
|
+
const activityAt = new Date(task.agentProgress.lastActivityAt).getTime();
|
|
106
|
+
if (Number.isFinite(activityAt)) {
|
|
107
|
+
const activityAge = now - activityAt;
|
|
108
|
+
// Use activity age if it's fresher than heartbeat age
|
|
109
|
+
// (no upper bound - if agent has recent activity, trust it even if old)
|
|
110
|
+
if (activityAge < elapsed) {
|
|
111
|
+
elapsed = activityAge;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
const level = elapsed > thresholds.deadMs ? "dead" : elapsed > thresholds.staleMs ? "stale" : elapsed > thresholds.warnMs ? "warn" : "healthy";
|
|
101
116
|
this.opts.registry.gauge("crew.heartbeat.staleness_ms", "Heartbeat elapsed since last seen, milliseconds").set({ runId: run.runId, taskId: task.id }, Number.isFinite(elapsed) ? elapsed : thresholds.deadMs);
|
|
102
117
|
this.opts.registry.counter("crew.heartbeat.level_total", "Heartbeat classifications by level").inc({ runId: run.runId, level });
|
|
103
118
|
const previous = this.lastLevel.get(key);
|