@nredd/pi-subagents 0.22.0-nredd.8 → 0.22.0-nredd.9
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 +136 -55
- package/package.json +14 -11
- package/src/abortable.ts +1 -1
- package/src/agent-file-toggle.ts +0 -1
- package/src/agent-manager.ts +360 -103
- package/src/agent-runner.ts +121 -63
- package/src/child-context.ts +19 -1
- package/src/completion-outbox.ts +22 -3
- package/src/cross-extension-rpc.ts +10 -6
- package/src/custom-agents.ts +2 -1
- package/src/default-agents.ts +19 -17
- package/src/detached-results.ts +18 -2
- package/src/enterprise-local.ts +2 -1
- package/src/error-message.ts +11 -0
- package/src/git-path.ts +66 -0
- package/src/group-join.ts +4 -10
- package/src/index.ts +731 -231
- package/src/invocation-config.ts +1 -5
- package/src/mention-clone.ts +4 -3
- package/src/model-resolver.ts +51 -14
- package/src/model-selection.ts +17 -6
- package/src/model-tiers.ts +5 -0
- package/src/nested-tools.ts +80 -21
- package/src/notification-result.ts +118 -0
- package/src/plan-mode-children.ts +176 -0
- package/src/prompts.ts +7 -2
- package/src/quota-admission.ts +16 -6
- package/src/resource-loader.ts +20 -6
- package/src/schedule.ts +65 -19
- package/src/settings.ts +21 -1
- package/src/skill-loader.ts +244 -94
- package/src/status-note.ts +28 -5
- package/src/types.ts +52 -2
- package/src/ui/agent-mention.ts +5 -2
- package/src/ui/agent-widget.ts +41 -22
- package/src/ui/conversation-viewer.ts +4 -2
- package/src/ui/fallback-flag.ts +28 -4
- package/src/ui/fleet-list.ts +89 -54
- package/src/ui/notification-view.ts +4 -1
- package/src/ui/workflow-card.ts +1 -8
- package/src/ui/workflow-dialog.ts +31 -18
- package/src/workflow/host.ts +4 -5
- package/src/workflow/meta.ts +7 -4
- package/src/workflow/progress.ts +4 -3
- package/src/workflow/runtime.ts +23 -7
- package/src/workflow/saved.ts +2 -4
- package/src/workflow/task.ts +12 -7
- package/src/workflow/tool-description.ts +2 -2
- package/src/workflow/worker-source.ts +6 -2
- package/src/worktree.ts +309 -19
package/src/default-agents.ts
CHANGED
|
@@ -6,7 +6,17 @@
|
|
|
6
6
|
|
|
7
7
|
import type { AgentConfig } from "./types.js";
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
/**
|
|
10
|
+
* Tool surface shared by Explore and Plan. `bash` is left out because a prompt cannot
|
|
11
|
+
* make an unrestricted shell read-only, and extensions are off because extension tools
|
|
12
|
+
* can run commands or write. A `.pi/agents/<Name>.md` override can opt back in with an
|
|
13
|
+
* explicit `extensions:` list.
|
|
14
|
+
*/
|
|
15
|
+
const READ_ONLY_SURFACE: Pick<AgentConfig, "builtinToolNames" | "extensions" | "skills"> = {
|
|
16
|
+
builtinToolNames: ["read", "grep", "find", "ls"],
|
|
17
|
+
extensions: false,
|
|
18
|
+
skills: true,
|
|
19
|
+
};
|
|
10
20
|
|
|
11
21
|
export const DEFAULT_AGENTS: Map<string, AgentConfig> = new Map([
|
|
12
22
|
[
|
|
@@ -31,9 +41,7 @@ export const DEFAULT_AGENTS: Map<string, AgentConfig> = new Map([
|
|
|
31
41
|
name: "Explore",
|
|
32
42
|
displayName: "Explore",
|
|
33
43
|
description: "Fast read-only search agent for locating code. Use it to find files by pattern (eg. \"src/components/**/*.tsx\"), grep for symbols or keywords (eg. \"API endpoints\"), or answer \"where is X defined / which files reference Y.\" Do NOT use it for code review, design-doc auditing, cross-file consistency checks, or open-ended analysis — it reads excerpts rather than whole files and will miss content past its read window. When calling, specify search breadth: \"quick\" for a single targeted lookup, \"medium\" for moderate exploration, or \"very thorough\" to search across multiple locations and naming conventions.",
|
|
34
|
-
|
|
35
|
-
extensions: true,
|
|
36
|
-
skills: true,
|
|
44
|
+
...READ_ONLY_SURFACE,
|
|
37
45
|
// Bind the role per machine, not to a provider or model generation.
|
|
38
46
|
model: "@cheap",
|
|
39
47
|
systemPrompt: `# CRITICAL: READ-ONLY MODE - NO FILE MODIFICATIONS
|
|
@@ -49,13 +57,10 @@ You are STRICTLY PROHIBITED from:
|
|
|
49
57
|
- Using redirect operators (>, >>, |) or heredocs to write to files
|
|
50
58
|
- Running ANY commands that change system state
|
|
51
59
|
|
|
52
|
-
Use Bash ONLY for read-only operations: ls, git status, git log, git diff, find, cat, head, tail.
|
|
53
|
-
|
|
54
60
|
# Tool Usage
|
|
55
|
-
- Use the find tool for file pattern matching
|
|
56
|
-
- Use the grep tool for content search
|
|
57
|
-
- Use the read tool for reading files
|
|
58
|
-
- Use Bash ONLY for read-only operations
|
|
61
|
+
- Use the find tool for file pattern matching
|
|
62
|
+
- Use the grep tool for content search
|
|
63
|
+
- Use the read tool for reading files
|
|
59
64
|
- Make independent tool calls in parallel for efficiency
|
|
60
65
|
- Adapt search approach based on thoroughness level specified
|
|
61
66
|
|
|
@@ -74,9 +79,7 @@ Use Bash ONLY for read-only operations: ls, git status, git log, git diff, find,
|
|
|
74
79
|
name: "Plan",
|
|
75
80
|
displayName: "Plan",
|
|
76
81
|
description: "Software architect agent for designing implementation plans. Use this when you need to plan the implementation strategy for a task. Returns step-by-step plans, identifies critical files, and considers architectural trade-offs.",
|
|
77
|
-
|
|
78
|
-
extensions: true,
|
|
79
|
-
skills: true,
|
|
82
|
+
...READ_ONLY_SURFACE,
|
|
80
83
|
systemPrompt: `# CRITICAL: READ-ONLY MODE - NO FILE MODIFICATIONS
|
|
81
84
|
You are a software architect and planning specialist.
|
|
82
85
|
Your role is EXCLUSIVELY to explore the codebase and design implementation plans.
|
|
@@ -104,10 +107,9 @@ You are STRICTLY PROHIBITED from:
|
|
|
104
107
|
- Follow existing patterns where appropriate
|
|
105
108
|
|
|
106
109
|
# Tool Usage
|
|
107
|
-
- Use the find tool for file pattern matching
|
|
108
|
-
- Use the grep tool for content search
|
|
109
|
-
- Use the read tool for reading files
|
|
110
|
-
- Use Bash ONLY for read-only operations
|
|
110
|
+
- Use the find tool for file pattern matching
|
|
111
|
+
- Use the grep tool for content search
|
|
112
|
+
- Use the read tool for reading files
|
|
111
113
|
|
|
112
114
|
# Output Format
|
|
113
115
|
- Use absolute file paths
|
package/src/detached-results.ts
CHANGED
|
@@ -13,7 +13,9 @@ import { createHash } from "node:crypto";
|
|
|
13
13
|
import { mkdirSync, readdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
14
14
|
import { tmpdir } from "node:os";
|
|
15
15
|
import { join } from "node:path";
|
|
16
|
-
import type {
|
|
16
|
+
import type { EvictedNote } from "./agent-manager.js";
|
|
17
|
+
import type { NoticeParts } from "./notification-result.js";
|
|
18
|
+
import type { AgentRecord, AgentTombstone } from "./types.js";
|
|
17
19
|
import type { AgentActivity } from "./ui/agent-widget.js";
|
|
18
20
|
|
|
19
21
|
/** One finished agent, in the shape `session_start` needs to re-announce it. */
|
|
@@ -25,6 +27,14 @@ export interface DetachedResult {
|
|
|
25
27
|
details: unknown;
|
|
26
28
|
/** History entry for a result that finished detached; absent if already appended. */
|
|
27
29
|
record?: Record<string, unknown>;
|
|
30
|
+
/** The `subagents:completed`/`subagents:failed` event the live path would have emitted, replayed on delivery. */
|
|
31
|
+
lifecycle?: { channel: "subagents:completed" | "subagents:failed"; data: Record<string, unknown> };
|
|
32
|
+
/** What the reopened session keeps so these agents' ids still answer after their records are gone. */
|
|
33
|
+
evicted?: EvictedNote[];
|
|
34
|
+
/** Reopenable sessions among them, so a steer or resume of these ids continues the saved conversation. */
|
|
35
|
+
tombstones?: AgentTombstone[];
|
|
36
|
+
/** A full-result notice's parts, so delivery clips it against its batch's budget; `content` is the fallback. */
|
|
37
|
+
notice?: NoticeParts & { footer: string };
|
|
28
38
|
}
|
|
29
39
|
|
|
30
40
|
const SINK_KEY = Symbol.for("pi-subagents:detached-sink");
|
|
@@ -96,7 +106,8 @@ export function watchDetached(sessionFile: string, onSpooled: () => void): () =>
|
|
|
96
106
|
export interface RunningDetached {
|
|
97
107
|
record: AgentRecord;
|
|
98
108
|
activity?: AgentActivity;
|
|
99
|
-
abort
|
|
109
|
+
/** `by` as for `AgentManager.abort`: `"parent"` for `stop_subagent`, omitted for a human. */
|
|
110
|
+
abort(by?: AgentRecord["stoppedBy"]): boolean;
|
|
100
111
|
steer(message: string): boolean;
|
|
101
112
|
}
|
|
102
113
|
|
|
@@ -127,6 +138,11 @@ export function unregisterRunningDetached(sessionFile: string, id: string): void
|
|
|
127
138
|
if (byId?.size === 0) running().delete(sessionFile);
|
|
128
139
|
}
|
|
129
140
|
|
|
141
|
+
/** Every orphan still running in the process with the session file it is registered (and wakes) under. */
|
|
142
|
+
export function listAllRunningDetached(): Array<{ sessionFile: string; agent: RunningDetached }> {
|
|
143
|
+
return [...running()].flatMap(([sessionFile, byId]) => [...byId.values()].map((agent) => ({ sessionFile, agent })));
|
|
144
|
+
}
|
|
145
|
+
|
|
130
146
|
/** Orphans still running for `sessionFile`, in registration order. */
|
|
131
147
|
export function listRunningDetached(sessionFile: string | undefined): RunningDetached[] {
|
|
132
148
|
if (sessionFile === undefined) return [];
|
package/src/enterprise-local.ts
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
10
10
|
import { join } from "node:path";
|
|
11
|
+
import { errorMessage } from "./error-message.js";
|
|
11
12
|
import { resolveAgentDir } from "./home-paths.js";
|
|
12
13
|
|
|
13
14
|
const MANIFEST_VERSION = 1;
|
|
@@ -59,7 +60,7 @@ export function readEnterpriseLocalManifest(homeDir?: string): EnterpriseLocalMa
|
|
|
59
60
|
}
|
|
60
61
|
return manifest as unknown as EnterpriseLocalManifest;
|
|
61
62
|
} catch (error) {
|
|
62
|
-
const reason =
|
|
63
|
+
const reason = errorMessage(error);
|
|
63
64
|
console.warn(`[pi-subagents] Ignoring invalid ${path}: ${reason}`);
|
|
64
65
|
return undefined;
|
|
65
66
|
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* error-message.ts — the text a caught value reports.
|
|
3
|
+
*
|
|
4
|
+
* `catch` binds `unknown`: an `Error` gives its message, and anything else a
|
|
5
|
+
* caller threw (a string, a rejected non-Error) is stringified as-is.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/** The message of `error` when it is an `Error`, else `String(error)`. */
|
|
9
|
+
export function errorMessage(error: unknown): string {
|
|
10
|
+
return error instanceof Error ? error.message : String(error);
|
|
11
|
+
}
|
package/src/git-path.ts
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* git-path.ts -- decode the C-style quoted paths git prints without `-z`.
|
|
3
|
+
*
|
|
4
|
+
* `pi.exec` decodes each stdout chunk as UTF-8 on its own, so a multibyte
|
|
5
|
+
* character split across two chunks becomes U+FFFD. With `core.quotePath=true`
|
|
6
|
+
* git escapes every non-ASCII byte (and control characters, `"` and `\`) as
|
|
7
|
+
* `"..."` with octal escapes, so a listing read without `-z` is pure ASCII and
|
|
8
|
+
* survives any chunking; this module turns it back into the real path.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const SIMPLE_ESCAPES: Record<string, number> = {
|
|
12
|
+
a: 0x07,
|
|
13
|
+
b: 0x08,
|
|
14
|
+
f: 0x0c,
|
|
15
|
+
n: 0x0a,
|
|
16
|
+
r: 0x0d,
|
|
17
|
+
t: 0x09,
|
|
18
|
+
v: 0x0b,
|
|
19
|
+
'"': 0x22,
|
|
20
|
+
"\\": 0x5c,
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const UTF8 = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true });
|
|
24
|
+
|
|
25
|
+
/** Args that make `git` quote every non-ASCII path byte; put them before the subcommand. */
|
|
26
|
+
export const QUOTE_PATH_ARGS = ["-c", "core.quotePath=true"];
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Decode one path as git prints it: a plain name is returned as is, a `"..."` name is
|
|
30
|
+
* unescaped to bytes and decoded as UTF-8. Throws on anything that cannot name a file
|
|
31
|
+
* as a JS string: raw non-ASCII (the listing was not quoted, so it may be mis-decoded),
|
|
32
|
+
* a malformed escape, or bytes that are not valid UTF-8.
|
|
33
|
+
*/
|
|
34
|
+
export function unquoteGitPath(text: string): string {
|
|
35
|
+
const quoted = text.length >= 2 && text.startsWith('"') && text.endsWith('"');
|
|
36
|
+
const body = quoted ? text.slice(1, -1) : text;
|
|
37
|
+
for (let i = 0; i < body.length; i++) {
|
|
38
|
+
if (body.charCodeAt(i) > 0x7f) throw new Error(`git printed a non-ASCII path unquoted (${JSON.stringify(text)}); it may be mis-decoded`);
|
|
39
|
+
}
|
|
40
|
+
if (!quoted) return text;
|
|
41
|
+
const bytes: number[] = [];
|
|
42
|
+
for (let i = 0; i < body.length; i++) {
|
|
43
|
+
const char = body.charAt(i);
|
|
44
|
+
if (char === '"') throw new Error(`malformed quoted git path ${JSON.stringify(text)}`);
|
|
45
|
+
if (char !== "\\") {
|
|
46
|
+
bytes.push(body.charCodeAt(i));
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
const next = body.charAt(i + 1);
|
|
50
|
+
const octal = /^[0-3][0-7]{2}$/.test(body.slice(i + 1, i + 4)) ? body.slice(i + 1, i + 4) : undefined;
|
|
51
|
+
if (octal) {
|
|
52
|
+
bytes.push(Number.parseInt(octal, 8));
|
|
53
|
+
i += 3;
|
|
54
|
+
} else if (next in SIMPLE_ESCAPES) {
|
|
55
|
+
bytes.push(SIMPLE_ESCAPES[next] as number);
|
|
56
|
+
i += 1;
|
|
57
|
+
} else {
|
|
58
|
+
throw new Error(`malformed escape in quoted git path ${JSON.stringify(text)}`);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
try {
|
|
62
|
+
return UTF8.decode(Uint8Array.from(bytes));
|
|
63
|
+
} catch {
|
|
64
|
+
throw new Error(`git path ${JSON.stringify(text)} is not valid UTF-8 and cannot be inspected`);
|
|
65
|
+
}
|
|
66
|
+
}
|
package/src/group-join.ts
CHANGED
|
@@ -15,7 +15,6 @@ interface AgentGroup {
|
|
|
15
15
|
agentIds: Set<string>;
|
|
16
16
|
completedRecords: Map<string, AgentRecord>;
|
|
17
17
|
timeoutHandle?: ReturnType<typeof setTimeout>;
|
|
18
|
-
delivered: boolean;
|
|
19
18
|
/** Shorter timeout for stragglers after a partial delivery. */
|
|
20
19
|
isStraggler: boolean;
|
|
21
20
|
}
|
|
@@ -40,7 +39,6 @@ export class GroupJoinManager {
|
|
|
40
39
|
groupId,
|
|
41
40
|
agentIds: new Set(agentIds),
|
|
42
41
|
completedRecords: new Map(),
|
|
43
|
-
delivered: false,
|
|
44
42
|
isStraggler: false,
|
|
45
43
|
};
|
|
46
44
|
this.groups.set(groupId, group);
|
|
@@ -61,7 +59,7 @@ export class GroupJoinManager {
|
|
|
61
59
|
if (!groupId) return 'pass';
|
|
62
60
|
|
|
63
61
|
const group = this.groups.get(groupId);
|
|
64
|
-
if (!group
|
|
62
|
+
if (!group) return 'pass';
|
|
65
63
|
|
|
66
64
|
group.completedRecords.set(record.id, record);
|
|
67
65
|
|
|
@@ -83,7 +81,6 @@ export class GroupJoinManager {
|
|
|
83
81
|
}
|
|
84
82
|
|
|
85
83
|
private onTimeout(group: AgentGroup): void {
|
|
86
|
-
if (group.delivered) return;
|
|
87
84
|
group.timeoutHandle = undefined;
|
|
88
85
|
|
|
89
86
|
// Partial delivery — some agents still running
|
|
@@ -112,18 +109,15 @@ export class GroupJoinManager {
|
|
|
112
109
|
clearTimeout(group.timeoutHandle);
|
|
113
110
|
group.timeoutHandle = undefined;
|
|
114
111
|
}
|
|
115
|
-
group.delivered = true;
|
|
116
112
|
this.deliverCb([...group.completedRecords.values()], partial);
|
|
117
|
-
this.cleanupGroup(group
|
|
113
|
+
this.cleanupGroup(group);
|
|
118
114
|
}
|
|
119
115
|
|
|
120
|
-
private cleanupGroup(
|
|
121
|
-
const group = this.groups.get(groupId);
|
|
122
|
-
if (!group) return;
|
|
116
|
+
private cleanupGroup(group: AgentGroup): void {
|
|
123
117
|
for (const id of group.agentIds) {
|
|
124
118
|
this.agentToGroup.delete(id);
|
|
125
119
|
}
|
|
126
|
-
this.groups.delete(groupId);
|
|
120
|
+
this.groups.delete(group.groupId);
|
|
127
121
|
}
|
|
128
122
|
|
|
129
123
|
/** Check if an agent is in a group. */
|