shariq-pi-extensions 0.1.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/LICENSE +21 -0
- package/README.md +94 -0
- package/THIRD_PARTY_NOTICES.md +11 -0
- package/docs/ARCHITECTURE.md +54 -0
- package/docs/DEVELOPMENT.md +88 -0
- package/docs/EXTENSIONS.md +89 -0
- package/extensions/antigravity-provider/LICENSE +21 -0
- package/extensions/antigravity-provider/README.md +23 -0
- package/extensions/antigravity-provider/antigravity/cloud-code-assist.ts +519 -0
- package/extensions/antigravity-provider/antigravity/index.ts +67 -0
- package/extensions/antigravity-provider/antigravity/models.ts +184 -0
- package/extensions/antigravity-provider/antigravity/oauth.ts +499 -0
- package/extensions/antigravity-provider/index.ts +3 -0
- package/extensions/ask-user/README.md +21 -0
- package/extensions/ask-user/index.ts +417 -0
- package/extensions/background-terminals/README.md +43 -0
- package/extensions/background-terminals/index.ts +400 -0
- package/extensions/background-terminals/src/manager.ts +480 -0
- package/extensions/background-terminals/src/output-buffer.ts +110 -0
- package/extensions/background-terminals/src/presentation.ts +74 -0
- package/extensions/background-terminals/src/types.ts +47 -0
- package/extensions/background-terminals/src/ui.ts +478 -0
- package/extensions/context-usage/LICENSE +21 -0
- package/extensions/context-usage/README.md +20 -0
- package/extensions/context-usage/index.ts +2006 -0
- package/extensions/context-usage/lib/ansi.ts +60 -0
- package/extensions/context-usage/lib/boundary.ts +27 -0
- package/extensions/context-usage/lib/chat.ts +190 -0
- package/extensions/context-usage/lib/config.ts +30 -0
- package/extensions/context-usage/lib/fmt.ts +59 -0
- package/extensions/context-usage/lib/style.ts +260 -0
- package/extensions/copy-all/README.md +15 -0
- package/extensions/copy-all/index.ts +72 -0
- package/extensions/factory-provider/API_KEYS.md +17 -0
- package/extensions/factory-provider/AUTH.md +15 -0
- package/extensions/factory-provider/README.md +90 -0
- package/extensions/factory-provider/REFRESH_MODELS.md +185 -0
- package/extensions/factory-provider/UPDATING.md +97 -0
- package/extensions/factory-provider/factory/api-keys.ts +407 -0
- package/extensions/factory-provider/factory/auth.ts +132 -0
- package/extensions/factory-provider/factory/constants.ts +15 -0
- package/extensions/factory-provider/factory/dashboard.ts +363 -0
- package/extensions/factory-provider/factory/droid.ts +84 -0
- package/extensions/factory-provider/factory/limits.ts +342 -0
- package/extensions/factory-provider/factory/models.ts +283 -0
- package/extensions/factory-provider/factory/responses.ts +331 -0
- package/extensions/factory-provider/factory/warm-theme.ts +53 -0
- package/extensions/factory-provider/factory/websocket.ts +130 -0
- package/extensions/factory-provider/index.ts +204 -0
- package/extensions/factory-provider/scripts/audit-reasoning-efforts.py +84 -0
- package/extensions/firecrawl-web/README.md +31 -0
- package/extensions/firecrawl-web/auth.ts +92 -0
- package/extensions/firecrawl-web/client.ts +238 -0
- package/extensions/firecrawl-web/index.ts +96 -0
- package/extensions/firecrawl-web/output.ts +125 -0
- package/extensions/git-info/README.md +25 -0
- package/extensions/git-info/index.ts +162 -0
- package/extensions/git-info/src/git.ts +206 -0
- package/extensions/git-info/src/ui.ts +291 -0
- package/extensions/goal/README.md +44 -0
- package/extensions/goal/constants.ts +4 -0
- package/extensions/goal/extension.ts +1064 -0
- package/extensions/goal/index.ts +2 -0
- package/extensions/goal/types.ts +56 -0
- package/extensions/goal/ui.ts +334 -0
- package/extensions/orchestration/README.md +58 -0
- package/extensions/orchestration/dashboard.ts +192 -0
- package/extensions/orchestration/engine.ts +706 -0
- package/extensions/orchestration/index.ts +243 -0
- package/extensions/orchestration/prompts.ts +124 -0
- package/extensions/orchestration/protocol.ts +116 -0
- package/extensions/orchestration/settings-ui.ts +41 -0
- package/extensions/orchestration/settings.ts +59 -0
- package/extensions/orchestration/storage.ts +49 -0
- package/extensions/orchestration/types.ts +105 -0
- package/extensions/performance-status/README.md +5 -0
- package/extensions/performance-status/index.ts +171 -0
- package/extensions/pi-memory/README.md +53 -0
- package/extensions/pi-memory/index.ts +326 -0
- package/extensions/pi-memory/src/config.ts +75 -0
- package/extensions/pi-memory/src/database.ts +617 -0
- package/extensions/pi-memory/src/extraction.ts +178 -0
- package/extensions/pi-memory/src/project.ts +80 -0
- package/extensions/pi-memory/src/projection.ts +59 -0
- package/extensions/pi-memory/src/redaction.ts +60 -0
- package/extensions/pi-memory/src/retrieval.ts +30 -0
- package/extensions/pi-memory/src/service.ts +194 -0
- package/extensions/pi-memory/src/session.ts +146 -0
- package/extensions/pi-memory/src/types.ts +82 -0
- package/extensions/shared/README.md +18 -0
- package/extensions/shared/activity-status.ts +31 -0
- package/extensions/shared/child-session.ts +150 -0
- package/extensions/shared/context-utilization.ts +47 -0
- package/extensions/shared/dashboard-state.ts +99 -0
- package/extensions/shared/tool-call-timeout.ts +104 -0
- package/extensions/shared/tui-dashboard.ts +139 -0
- package/extensions/shell-shortcuts/README.md +17 -0
- package/extensions/shell-shortcuts/features/exit-alias.ts +10 -0
- package/extensions/shell-shortcuts/index.ts +6 -0
- package/extensions/subagents/README.md +89 -0
- package/extensions/subagents/index.ts +1386 -0
- package/extensions/subagents/src/backend.ts +69 -0
- package/extensions/subagents/src/backends/pi.ts +781 -0
- package/extensions/subagents/src/catalog.ts +85 -0
- package/extensions/subagents/src/config.ts +241 -0
- package/extensions/subagents/src/context.ts +57 -0
- package/extensions/subagents/src/coordinator.ts +64 -0
- package/extensions/subagents/src/domain.ts +294 -0
- package/extensions/subagents/src/format.ts +74 -0
- package/extensions/subagents/src/manager.ts +813 -0
- package/extensions/subagents/src/prompt.ts +94 -0
- package/extensions/subagents/src/result-delivery.ts +20 -0
- package/extensions/subagents/src/runtime.ts +48 -0
- package/extensions/subagents/src/ui/takeover.ts +695 -0
- package/extensions/subagents/src/ui/transcript.ts +182 -0
- package/extensions/subagents/src/worktree.ts +175 -0
- package/extensions/web-fetch/README.md +13 -0
- package/extensions/web-fetch/index.ts +441 -0
- package/package.json +117 -0
- package/skills/background-terminals/SKILL.md +42 -0
- package/skills/orchestration/SKILL.md +33 -0
- package/skills/subagents/SKILL.md +89 -0
- package/themes/ember-warm-dark.json +85 -0
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { randomBytes } from "node:crypto";
|
|
2
|
+
import * as fs from "node:fs";
|
|
3
|
+
import * as path from "node:path";
|
|
4
|
+
import { getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
5
|
+
import type { CapabilityMode, IsolationMode } from "./config.ts";
|
|
6
|
+
import type { SubagentStatus } from "./domain.ts";
|
|
7
|
+
import type { WorktreeInfo } from "./worktree.ts";
|
|
8
|
+
|
|
9
|
+
export interface ArchivedSubagent {
|
|
10
|
+
id: string;
|
|
11
|
+
title: string;
|
|
12
|
+
cwd: string;
|
|
13
|
+
status: SubagentStatus;
|
|
14
|
+
sessionFile: string;
|
|
15
|
+
model?: string;
|
|
16
|
+
agentType?: string;
|
|
17
|
+
persona?: string;
|
|
18
|
+
capability?: CapabilityMode;
|
|
19
|
+
isolation?: IsolationMode;
|
|
20
|
+
worktree?: WorktreeInfo;
|
|
21
|
+
updatedAt: number;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
interface CatalogFile {
|
|
25
|
+
version: 1;
|
|
26
|
+
agents: ArchivedSubagent[];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const MAX_CATALOG_AGENTS = 512;
|
|
30
|
+
|
|
31
|
+
export function allocateSubagentId() {
|
|
32
|
+
// Preserve the legacy sa-N shape while remaining collision-resistant across
|
|
33
|
+
// independent and ephemeral Pi processes.
|
|
34
|
+
const random = BigInt(`0x${randomBytes(8).toString("hex")}`).toString(10).padStart(20, "0");
|
|
35
|
+
return `sa-${Date.now()}${random}`;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function subagentCatalogPath() {
|
|
39
|
+
return path.join(getAgentDir(), "subagents", "catalog.json");
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function validRecord(value: unknown): value is ArchivedSubagent {
|
|
43
|
+
if (!value || typeof value !== "object") return false;
|
|
44
|
+
const record = value as Partial<ArchivedSubagent>;
|
|
45
|
+
return (
|
|
46
|
+
typeof record.id === "string" &&
|
|
47
|
+
typeof record.title === "string" &&
|
|
48
|
+
typeof record.cwd === "string" &&
|
|
49
|
+
typeof record.sessionFile === "string" &&
|
|
50
|
+
typeof record.updatedAt === "number" &&
|
|
51
|
+
(record.status === "running" || record.status === "done" || record.status === "error")
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function loadSubagentCatalog(): Map<string, ArchivedSubagent> {
|
|
56
|
+
try {
|
|
57
|
+
const parsed = JSON.parse(fs.readFileSync(subagentCatalogPath(), "utf8")) as Partial<CatalogFile>;
|
|
58
|
+
const agents = Array.isArray(parsed.agents) ? parsed.agents.filter(validRecord) : [];
|
|
59
|
+
return new Map(agents.map((record) => [record.id, record]));
|
|
60
|
+
} catch {
|
|
61
|
+
return new Map();
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function saveSubagentCatalog(records: Iterable<ArchivedSubagent>) {
|
|
66
|
+
const file = subagentCatalogPath();
|
|
67
|
+
fs.mkdirSync(path.dirname(file), { recursive: true, mode: 0o700 });
|
|
68
|
+
const agents = [...records]
|
|
69
|
+
.filter(validRecord)
|
|
70
|
+
.sort((a, b) => b.updatedAt - a.updatedAt)
|
|
71
|
+
.slice(0, MAX_CATALOG_AGENTS);
|
|
72
|
+
const temp = `${file}.${process.pid}.${Date.now()}.tmp`;
|
|
73
|
+
fs.writeFileSync(temp, `${JSON.stringify({ version: 1, agents }, null, 2)}\n`, {
|
|
74
|
+
mode: 0o600,
|
|
75
|
+
});
|
|
76
|
+
fs.renameSync(temp, file);
|
|
77
|
+
fs.chmodSync(file, 0o600);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function upsertSubagentCatalog(record: ArchivedSubagent) {
|
|
81
|
+
const catalog = loadSubagentCatalog();
|
|
82
|
+
const existing = catalog.get(record.id);
|
|
83
|
+
if (!existing || existing.updatedAt <= record.updatedAt) catalog.set(record.id, record);
|
|
84
|
+
saveSubagentCatalog(catalog.values());
|
|
85
|
+
}
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import { CONFIG_DIR_NAME, getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
4
|
+
import type { ReasoningEffort } from "./domain.ts";
|
|
5
|
+
|
|
6
|
+
export const AGENT_TYPES = ["general-purpose", "explore", "plan"] as const;
|
|
7
|
+
export const CAPABILITY_MODES = ["read-only", "read-write", "execute", "all"] as const;
|
|
8
|
+
export const ISOLATION_MODES = ["none", "worktree"] as const;
|
|
9
|
+
|
|
10
|
+
export type CapabilityMode = (typeof CAPABILITY_MODES)[number];
|
|
11
|
+
export type IsolationMode = (typeof ISOLATION_MODES)[number];
|
|
12
|
+
|
|
13
|
+
export interface AgentProfile {
|
|
14
|
+
description?: string;
|
|
15
|
+
instructions?: string;
|
|
16
|
+
capability?: CapabilityMode;
|
|
17
|
+
model?: string;
|
|
18
|
+
thinking?: ReasoningEffort;
|
|
19
|
+
isolation?: IsolationMode;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface PersonaProfile {
|
|
23
|
+
description?: string;
|
|
24
|
+
instructions: string;
|
|
25
|
+
model?: string;
|
|
26
|
+
thinking?: ReasoningEffort;
|
|
27
|
+
isolation?: IsolationMode;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface SubagentConfig {
|
|
31
|
+
maxConcurrent: number;
|
|
32
|
+
profiles: Record<string, AgentProfile>;
|
|
33
|
+
personas: Record<string, PersonaProfile>;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const BUILTIN_PROFILES: Record<string, AgentProfile> = {
|
|
37
|
+
"general-purpose": {
|
|
38
|
+
description: "Full-capability implementation and investigation agent",
|
|
39
|
+
capability: "all",
|
|
40
|
+
},
|
|
41
|
+
explore: {
|
|
42
|
+
description: "Read, search, and execute diagnostics without editing files",
|
|
43
|
+
capability: "execute",
|
|
44
|
+
instructions: "Investigate only as far as needed to answer the task. Do not modify files. Report concrete evidence and paths, and distinguish verified facts from inference.",
|
|
45
|
+
},
|
|
46
|
+
plan: {
|
|
47
|
+
description: "Read-only planning agent that produces an implementation plan",
|
|
48
|
+
capability: "execute",
|
|
49
|
+
instructions: "Inspect the relevant code without modifying files. Return an ordered implementation plan tied to current paths, dependencies, material risks, and validation steps."
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const DEFAULT_CONFIG: SubagentConfig = {
|
|
54
|
+
maxConcurrent: 50,
|
|
55
|
+
profiles: BUILTIN_PROFILES,
|
|
56
|
+
personas: {},
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
60
|
+
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function readJson(file: string): Record<string, unknown> | undefined {
|
|
64
|
+
try {
|
|
65
|
+
const value = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
66
|
+
return isRecord(value) ? value : undefined;
|
|
67
|
+
} catch {
|
|
68
|
+
return undefined;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function boundedInt(value: unknown, fallback: number, min: number, max: number) {
|
|
73
|
+
return typeof value === "number" && Number.isInteger(value)
|
|
74
|
+
? Math.max(min, Math.min(max, value))
|
|
75
|
+
: fallback;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function enumValue<T extends readonly string[]>(value: unknown, values: T): T[number] | undefined {
|
|
79
|
+
return typeof value === "string" && values.includes(value) ? (value as T[number]) : undefined;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function parseProfile(value: unknown): AgentProfile | undefined {
|
|
83
|
+
if (!isRecord(value)) return undefined;
|
|
84
|
+
return {
|
|
85
|
+
description: typeof value.description === "string" ? value.description : undefined,
|
|
86
|
+
instructions: typeof value.instructions === "string" ? value.instructions : undefined,
|
|
87
|
+
capability: enumValue(value.capability, CAPABILITY_MODES),
|
|
88
|
+
model: typeof value.model === "string" ? value.model : undefined,
|
|
89
|
+
thinking: enumValue(value.thinking, ["off", "minimal", "low", "medium", "high", "xhigh", "max"] as const),
|
|
90
|
+
isolation: enumValue(value.isolation, ISOLATION_MODES),
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function parsePersona(value: unknown): PersonaProfile | undefined {
|
|
95
|
+
const profile = parseProfile(value);
|
|
96
|
+
if (!profile?.instructions) return undefined;
|
|
97
|
+
return { ...profile, instructions: profile.instructions };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function mergeConfig(base: SubagentConfig, raw: Record<string, unknown> | undefined): SubagentConfig {
|
|
101
|
+
if (!raw) return base;
|
|
102
|
+
const profiles = { ...base.profiles };
|
|
103
|
+
if (isRecord(raw.profiles)) {
|
|
104
|
+
for (const [name, value] of Object.entries(raw.profiles)) {
|
|
105
|
+
const parsed = parseProfile(value);
|
|
106
|
+
if (parsed) profiles[name] = { ...profiles[name], ...parsed };
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
const personas = { ...base.personas };
|
|
110
|
+
if (isRecord(raw.personas)) {
|
|
111
|
+
for (const [name, value] of Object.entries(raw.personas)) {
|
|
112
|
+
const parsed = parsePersona(value);
|
|
113
|
+
if (parsed) personas[name] = parsed;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return {
|
|
117
|
+
maxConcurrent: boundedInt(raw.maxConcurrent, base.maxConcurrent, 1, 50),
|
|
118
|
+
profiles,
|
|
119
|
+
personas,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export type ConfigScope = "global" | "project";
|
|
124
|
+
|
|
125
|
+
export function subagentConfigPath(scope: ConfigScope, cwd: string) {
|
|
126
|
+
return scope === "global"
|
|
127
|
+
? path.join(getAgentDir(), "subagents.json")
|
|
128
|
+
: path.join(cwd, CONFIG_DIR_NAME, "subagents.json");
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export function loadConfigDocument(scope: ConfigScope, cwd: string) {
|
|
132
|
+
const file = subagentConfigPath(scope, cwd);
|
|
133
|
+
try {
|
|
134
|
+
return fs.readFileSync(file, "utf8");
|
|
135
|
+
} catch {
|
|
136
|
+
return `${JSON.stringify({ maxConcurrent: 50, profiles: {}, personas: {} }, null, 2)}\n`;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function validateProfileDocument(name: string, value: unknown, persona: boolean) {
|
|
141
|
+
if (!isRecord(value)) throw new Error(`Invalid ${persona ? "persona" : "profile"} "${name}".`);
|
|
142
|
+
if (value.description !== undefined && typeof value.description !== "string") {
|
|
143
|
+
throw new Error(`${name}.description must be a string.`);
|
|
144
|
+
}
|
|
145
|
+
if (value.instructions !== undefined && typeof value.instructions !== "string") {
|
|
146
|
+
throw new Error(`${name}.instructions must be a string.`);
|
|
147
|
+
}
|
|
148
|
+
if (persona && typeof value.instructions !== "string") {
|
|
149
|
+
throw new Error(`Invalid persona "${name}"; personas require instructions.`);
|
|
150
|
+
}
|
|
151
|
+
if (value.capability !== undefined && !enumValue(value.capability, CAPABILITY_MODES)) {
|
|
152
|
+
throw new Error(`${name}.capability must be one of ${CAPABILITY_MODES.join(", ")}.`);
|
|
153
|
+
}
|
|
154
|
+
if (value.isolation !== undefined && !enumValue(value.isolation, ISOLATION_MODES)) {
|
|
155
|
+
throw new Error(`${name}.isolation must be one of ${ISOLATION_MODES.join(", ")}.`);
|
|
156
|
+
}
|
|
157
|
+
if (value.model !== undefined && typeof value.model !== "string") {
|
|
158
|
+
throw new Error(`${name}.model must be a string.`);
|
|
159
|
+
}
|
|
160
|
+
if (
|
|
161
|
+
value.thinking !== undefined &&
|
|
162
|
+
!enumValue(value.thinking, ["off", "minimal", "low", "medium", "high", "xhigh", "max"] as const)
|
|
163
|
+
) {
|
|
164
|
+
throw new Error(`${name}.thinking is invalid.`);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export function saveConfigDocument(scope: ConfigScope, cwd: string, text: string) {
|
|
169
|
+
let raw: unknown;
|
|
170
|
+
try {
|
|
171
|
+
raw = JSON.parse(text);
|
|
172
|
+
} catch (error) {
|
|
173
|
+
throw new Error(`Invalid JSON: ${error instanceof Error ? error.message : String(error)}`);
|
|
174
|
+
}
|
|
175
|
+
if (!isRecord(raw)) throw new Error("Subagent configuration must be a JSON object.");
|
|
176
|
+
if (
|
|
177
|
+
raw.maxConcurrent !== undefined &&
|
|
178
|
+
(!Number.isInteger(raw.maxConcurrent) || (raw.maxConcurrent as number) < 1 || (raw.maxConcurrent as number) > 50)
|
|
179
|
+
) {
|
|
180
|
+
throw new Error("maxConcurrent must be an integer from 1 to 50.");
|
|
181
|
+
}
|
|
182
|
+
if (raw.profiles !== undefined && !isRecord(raw.profiles)) {
|
|
183
|
+
throw new Error("profiles must be an object keyed by profile name.");
|
|
184
|
+
}
|
|
185
|
+
for (const [name, value] of Object.entries(isRecord(raw.profiles) ? raw.profiles : {})) {
|
|
186
|
+
if (!name.trim()) throw new Error("Profile names cannot be empty.");
|
|
187
|
+
validateProfileDocument(name, value, false);
|
|
188
|
+
}
|
|
189
|
+
if (raw.personas !== undefined && !isRecord(raw.personas)) {
|
|
190
|
+
throw new Error("personas must be an object keyed by persona name.");
|
|
191
|
+
}
|
|
192
|
+
for (const [name, value] of Object.entries(isRecord(raw.personas) ? raw.personas : {})) {
|
|
193
|
+
if (!name.trim()) throw new Error("Persona names cannot be empty.");
|
|
194
|
+
validateProfileDocument(name, value, true);
|
|
195
|
+
}
|
|
196
|
+
const file = subagentConfigPath(scope, cwd);
|
|
197
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
198
|
+
fs.writeFileSync(file, `${JSON.stringify(raw, null, 2)}\n`, { mode: 0o600 });
|
|
199
|
+
fs.chmodSync(file, 0o600);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
export function loadSubagentConfig(cwd: string, projectTrusted: boolean): SubagentConfig {
|
|
203
|
+
let config = mergeConfig(DEFAULT_CONFIG, readJson(subagentConfigPath("global", cwd)));
|
|
204
|
+
if (projectTrusted) {
|
|
205
|
+
config = mergeConfig(config, readJson(subagentConfigPath("project", cwd)));
|
|
206
|
+
}
|
|
207
|
+
return config;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
export function resolveProfile(
|
|
211
|
+
config: SubagentConfig,
|
|
212
|
+
options: {
|
|
213
|
+
agentType?: string;
|
|
214
|
+
persona?: string;
|
|
215
|
+
capability?: CapabilityMode;
|
|
216
|
+
model?: string;
|
|
217
|
+
thinking?: ReasoningEffort;
|
|
218
|
+
isolation?: IsolationMode;
|
|
219
|
+
},
|
|
220
|
+
) {
|
|
221
|
+
const agentType = options.agentType?.trim() || "general-purpose";
|
|
222
|
+
const profile = config.profiles[agentType];
|
|
223
|
+
if (!profile) {
|
|
224
|
+
throw new Error(`Unknown agent_type "${agentType}". Available: ${Object.keys(config.profiles).join(", ")}.`);
|
|
225
|
+
}
|
|
226
|
+
const personaName = options.persona?.trim();
|
|
227
|
+
const persona = personaName ? config.personas[personaName] : undefined;
|
|
228
|
+
if (personaName && !persona) {
|
|
229
|
+
throw new Error(`Unknown persona "${personaName}". Available: ${Object.keys(config.personas).join(", ") || "none"}.`);
|
|
230
|
+
}
|
|
231
|
+
const instructions = [profile.instructions, persona?.instructions].filter(Boolean).join("\n\n");
|
|
232
|
+
return {
|
|
233
|
+
agentType,
|
|
234
|
+
persona: personaName,
|
|
235
|
+
instructions,
|
|
236
|
+
capability: options.capability ?? profile.capability ?? "all",
|
|
237
|
+
model: options.model ?? profile.model ?? persona?.model,
|
|
238
|
+
thinking: options.thinking ?? profile.thinking ?? persona?.thinking,
|
|
239
|
+
isolation: options.isolation ?? profile.isolation ?? persona?.isolation ?? "none",
|
|
240
|
+
} as const;
|
|
241
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import type { AssistantMessage, Message, UserMessage } from "@earendil-works/pi-ai";
|
|
2
|
+
|
|
3
|
+
export type ForkTurns = "none" | "all" | number;
|
|
4
|
+
|
|
5
|
+
export function parseForkTurns(value: string | undefined): ForkTurns {
|
|
6
|
+
const normalized = value?.trim().toLowerCase() || "none";
|
|
7
|
+
if (normalized === "none") return "none";
|
|
8
|
+
if (normalized === "all") return "all";
|
|
9
|
+
const count = Number(normalized);
|
|
10
|
+
if (!Number.isInteger(count) || count <= 0) {
|
|
11
|
+
throw new Error('fork_turns must be "none", "all", or a positive integer string.');
|
|
12
|
+
}
|
|
13
|
+
return count;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function sanitizeMessage(message: unknown): Message | undefined {
|
|
17
|
+
if (!message || typeof message !== "object") return undefined;
|
|
18
|
+
const role = (message as { role?: unknown }).role;
|
|
19
|
+
if (role === "user") {
|
|
20
|
+
const user = message as UserMessage;
|
|
21
|
+
return { ...user, content: Array.isArray(user.content) ? user.content.filter((part) => part.type === "text" || part.type === "image") : user.content };
|
|
22
|
+
}
|
|
23
|
+
if (role === "assistant") {
|
|
24
|
+
const assistant = message as AssistantMessage;
|
|
25
|
+
const content = assistant.content.filter((part) => part.type === "text");
|
|
26
|
+
if (content.length === 0) return undefined;
|
|
27
|
+
return { ...assistant, content, stopReason: "stop", errorMessage: undefined };
|
|
28
|
+
}
|
|
29
|
+
return undefined;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Fork only conversational user/final-text history. Tool calls, tool results,
|
|
34
|
+
* and thinking are deliberately excluded so the child never inherits an
|
|
35
|
+
* unresolved protocol exchange or private reasoning.
|
|
36
|
+
*/
|
|
37
|
+
export function forkConversation(messages: readonly unknown[], mode: ForkTurns): Message[] {
|
|
38
|
+
if (mode === "none") return [];
|
|
39
|
+
const sanitized = messages.map(sanitizeMessage).filter((message): message is Message => !!message);
|
|
40
|
+
if (mode === "all") return sanitized;
|
|
41
|
+
|
|
42
|
+
let usersSeen = 0;
|
|
43
|
+
let start = sanitized.length;
|
|
44
|
+
for (let index = sanitized.length - 1; index >= 0; index--) {
|
|
45
|
+
if (sanitized[index]?.role !== "user") continue;
|
|
46
|
+
usersSeen++;
|
|
47
|
+
start = index;
|
|
48
|
+
if (usersSeen >= mode) break;
|
|
49
|
+
}
|
|
50
|
+
return sanitized.slice(start);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function buildTaskPrompt(message: string, instructions: string, persona?: string) {
|
|
54
|
+
if (!instructions.trim()) return message;
|
|
55
|
+
const label = persona ? `Profile instructions with persona "${persona}"` : "Profile instructions";
|
|
56
|
+
return `<subagent_instructions>\n${label}:\n${instructions.trim()}\n</subagent_instructions>\n\nTask:\n${message}`;
|
|
57
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import type { EventBus, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import type { CapabilityMode, IsolationMode } from "./config.ts";
|
|
3
|
+
import type {
|
|
4
|
+
ReasoningEffort,
|
|
5
|
+
SubagentSnapshot,
|
|
6
|
+
} from "./domain.ts";
|
|
7
|
+
|
|
8
|
+
export const SUBAGENT_COORDINATOR_REQUEST =
|
|
9
|
+
"shariq-pi-extensions:subagents:coordinator-request";
|
|
10
|
+
|
|
11
|
+
export interface CoordinatorSpawnOptions {
|
|
12
|
+
message: string;
|
|
13
|
+
taskName: string;
|
|
14
|
+
cwd?: string;
|
|
15
|
+
model?: string;
|
|
16
|
+
thinking?: ReasoningEffort;
|
|
17
|
+
capability: CapabilityMode;
|
|
18
|
+
isolation: IsolationMode;
|
|
19
|
+
agentType?: string;
|
|
20
|
+
resumeFrom?: string;
|
|
21
|
+
concurrencyGroup?: string;
|
|
22
|
+
maxConcurrent?: number;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface SubagentCoordinator {
|
|
26
|
+
spawn(
|
|
27
|
+
ctx: ExtensionContext,
|
|
28
|
+
options: CoordinatorSpawnOptions,
|
|
29
|
+
): Promise<SubagentSnapshot>;
|
|
30
|
+
send(id: string, message: string): Promise<SubagentSnapshot>;
|
|
31
|
+
cancel(ids: ReadonlyArray<string>): Promise<void>;
|
|
32
|
+
get(id: string): Promise<SubagentSnapshot | undefined>;
|
|
33
|
+
list(): Promise<ReadonlyArray<SubagentSnapshot>>;
|
|
34
|
+
subscribe(listener: () => void): Promise<() => void>;
|
|
35
|
+
apply(id: string, targetCwd?: string): Promise<{ changed: boolean; files: string[] }>;
|
|
36
|
+
discard(id: string): Promise<void>;
|
|
37
|
+
release(id: string): Promise<void>;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
interface CoordinatorRequest {
|
|
41
|
+
accept(coordinator: SubagentCoordinator): void;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function requestSubagentCoordinator(
|
|
45
|
+
events: EventBus,
|
|
46
|
+
): SubagentCoordinator | undefined {
|
|
47
|
+
let coordinator: SubagentCoordinator | undefined;
|
|
48
|
+
events.emit(SUBAGENT_COORDINATOR_REQUEST, {
|
|
49
|
+
accept(value) {
|
|
50
|
+
coordinator = value;
|
|
51
|
+
},
|
|
52
|
+
} satisfies CoordinatorRequest);
|
|
53
|
+
return coordinator;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function isCoordinatorRequest(
|
|
57
|
+
value: unknown,
|
|
58
|
+
): value is CoordinatorRequest {
|
|
59
|
+
return (
|
|
60
|
+
!!value &&
|
|
61
|
+
typeof value === "object" &&
|
|
62
|
+
typeof (value as CoordinatorRequest).accept === "function"
|
|
63
|
+
);
|
|
64
|
+
}
|