@xynogen/pix-subagent 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 +27 -0
- package/README.md +114 -0
- package/package.json +46 -0
- package/src/agent-manager.ts +526 -0
- package/src/agent-runner.ts +849 -0
- package/src/agent-types.ts +152 -0
- package/src/context.ts +59 -0
- package/src/custom-agents.ts +165 -0
- package/src/default-agents.ts +126 -0
- package/src/env.ts +43 -0
- package/src/extension.ts +9 -0
- package/src/index.ts +216 -0
- package/src/invocation-config.ts +43 -0
- package/src/model-resolver.ts +98 -0
- package/src/once.ts +26 -0
- package/src/prompts.ts +111 -0
- package/src/tools.ts +822 -0
- package/src/types.ts +126 -0
- package/src/ui/notification.ts +75 -0
- package/src/ui/widget.ts +490 -0
- package/src/usage.ts +71 -0
package/src/index.ts
ADDED
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* index.ts — pix-subagent extension entry point.
|
|
3
|
+
*
|
|
4
|
+
* Registers 3 LLM-callable tools (agent, agent_result, agent_steer),
|
|
5
|
+
* a live above-editor widget (model shown inline), and a themed
|
|
6
|
+
* subagent-notification renderer.
|
|
7
|
+
*
|
|
8
|
+
* Best-of-both:
|
|
9
|
+
* - tintinweb/pi-subagents spawn engine (MIT) — battle-tested createAgentSession path
|
|
10
|
+
* - nicobailon/pi-subagents explicit work-splitting (allowed_tools[], model param)
|
|
11
|
+
* - pix twist: model name ALWAYS visible in widget + notification
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
15
|
+
import { AgentManager } from "./agent-manager.ts";
|
|
16
|
+
import { registerAgents } from "./agent-types.ts";
|
|
17
|
+
import { loadCustomAgents } from "./custom-agents.ts";
|
|
18
|
+
import { listAvailable } from "./model-resolver.ts";
|
|
19
|
+
import {
|
|
20
|
+
type AgentActivity,
|
|
21
|
+
createAgentResultTool,
|
|
22
|
+
createAgentSteerTool,
|
|
23
|
+
createAgentTool,
|
|
24
|
+
} from "./tools.ts";
|
|
25
|
+
import type { NotificationDetails } from "./types.ts";
|
|
26
|
+
import { registerNotificationRenderer } from "./ui/notification.ts";
|
|
27
|
+
import { AgentWidget } from "./ui/widget.ts";
|
|
28
|
+
import { getLifetimeTotal } from "./usage.ts";
|
|
29
|
+
|
|
30
|
+
const EXTENSION_KEY = "pix-subagent";
|
|
31
|
+
|
|
32
|
+
// Reload guard key — stored on globalThis so a dev-reload cleans up stale state
|
|
33
|
+
const CLEANUP_KEY = `__${EXTENSION_KEY}Cleanup`;
|
|
34
|
+
|
|
35
|
+
export default function registerPixSubagent(pi: ExtensionAPI): void {
|
|
36
|
+
// ── Cleanup stale timers from a prior reload ───────────────────────────────
|
|
37
|
+
const g = globalThis as Record<string, unknown>;
|
|
38
|
+
const prevCleanup = g[CLEANUP_KEY];
|
|
39
|
+
if (typeof prevCleanup === "function") {
|
|
40
|
+
try {
|
|
41
|
+
(prevCleanup as () => void)();
|
|
42
|
+
} catch {
|
|
43
|
+
/* best effort */
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// ── Agent registry ─────────────────────────────────────────────────────────
|
|
48
|
+
const reloadCustomAgents = () => {
|
|
49
|
+
const userAgents = loadCustomAgents(process.cwd());
|
|
50
|
+
registerAgents(userAgents);
|
|
51
|
+
};
|
|
52
|
+
reloadCustomAgents();
|
|
53
|
+
|
|
54
|
+
// ── State ──────────────────────────────────────────────────────────────────
|
|
55
|
+
const agentActivity = new Map<string, AgentActivity>();
|
|
56
|
+
|
|
57
|
+
// Debounce: brief hold so agent_result can cancel a notification it just consumed
|
|
58
|
+
const pendingNudges = new Map<string, ReturnType<typeof setTimeout>>();
|
|
59
|
+
const NUDGE_HOLD_MS = 200;
|
|
60
|
+
|
|
61
|
+
function scheduleNudge(key: string, send: () => void) {
|
|
62
|
+
cancelNudge(key);
|
|
63
|
+
pendingNudges.set(
|
|
64
|
+
key,
|
|
65
|
+
setTimeout(() => {
|
|
66
|
+
pendingNudges.delete(key);
|
|
67
|
+
try {
|
|
68
|
+
send();
|
|
69
|
+
} catch {
|
|
70
|
+
/* ignore stale context errors */
|
|
71
|
+
}
|
|
72
|
+
}, NUDGE_HOLD_MS),
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function cancelNudge(key: string) {
|
|
77
|
+
const t = pendingNudges.get(key);
|
|
78
|
+
if (t != null) {
|
|
79
|
+
clearTimeout(t);
|
|
80
|
+
pendingNudges.delete(key);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// ── AgentManager ──────────────────────────────────────────────────────────
|
|
85
|
+
const manager = new AgentManager(
|
|
86
|
+
// onComplete — fire subagent-notification nudge for each finished bg agent
|
|
87
|
+
(record) => {
|
|
88
|
+
agentActivity.delete(record.id);
|
|
89
|
+
widget.markFinished(record.id);
|
|
90
|
+
|
|
91
|
+
if (record.resultConsumed) {
|
|
92
|
+
widget.update();
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const totalTokens = getLifetimeTotal(record.lifetimeUsage);
|
|
97
|
+
const activity = agentActivity.get(record.id);
|
|
98
|
+
const resultPreview = record.result
|
|
99
|
+
? record.result.length > 500
|
|
100
|
+
? `${record.result.slice(0, 500)}…`
|
|
101
|
+
: record.result
|
|
102
|
+
: "No output.";
|
|
103
|
+
|
|
104
|
+
const details: NotificationDetails = {
|
|
105
|
+
id: record.id,
|
|
106
|
+
description: record.description,
|
|
107
|
+
status: record.status,
|
|
108
|
+
modelName: record.invocation?.modelName,
|
|
109
|
+
toolUses: record.toolUses,
|
|
110
|
+
turnCount: activity?.turnCount ?? 0,
|
|
111
|
+
maxTurns: activity?.maxTurns,
|
|
112
|
+
totalTokens,
|
|
113
|
+
durationMs: record.completedAt
|
|
114
|
+
? record.completedAt - record.startedAt
|
|
115
|
+
: 0,
|
|
116
|
+
error: record.error,
|
|
117
|
+
resultPreview,
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
scheduleNudge(record.id, () => {
|
|
121
|
+
if (record.resultConsumed) {
|
|
122
|
+
widget.update();
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
pi.sendMessage<NotificationDetails>(
|
|
126
|
+
{
|
|
127
|
+
customType: "subagent-notification",
|
|
128
|
+
content: `Agent "${record.description}" ${record.status}.`,
|
|
129
|
+
display: true,
|
|
130
|
+
details,
|
|
131
|
+
},
|
|
132
|
+
{ deliverAs: "followUp", triggerTurn: true },
|
|
133
|
+
);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
widget.update();
|
|
137
|
+
},
|
|
138
|
+
4, // maxConcurrent
|
|
139
|
+
// onStart
|
|
140
|
+
(_record) => {
|
|
141
|
+
widget.update();
|
|
142
|
+
},
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
// ── Widget ─────────────────────────────────────────────────────────────────
|
|
146
|
+
const widget = new AgentWidget(manager, agentActivity);
|
|
147
|
+
|
|
148
|
+
// ── Register renderers ─────────────────────────────────────────────────────
|
|
149
|
+
registerNotificationRenderer(pi);
|
|
150
|
+
|
|
151
|
+
// ── Build initial tool description with model list ─────────────────────────
|
|
152
|
+
// Model list is empty until session_start provides a modelRegistry.
|
|
153
|
+
// Tools are registered once; the description is rebuilt on session_start via
|
|
154
|
+
// re-registering (pi.registerTool replaces by name — verify this at smoke test).
|
|
155
|
+
// ponytail: if re-register isn't supported mid-session, the list stays empty
|
|
156
|
+
// first session; acceptable until we find a hook to refresh.
|
|
157
|
+
let currentModelList: string[] = [];
|
|
158
|
+
|
|
159
|
+
function registerTools() {
|
|
160
|
+
pi.registerTool(
|
|
161
|
+
createAgentTool(
|
|
162
|
+
pi as Parameters<typeof manager.spawnAndWait>[0],
|
|
163
|
+
manager,
|
|
164
|
+
agentActivity,
|
|
165
|
+
reloadCustomAgents,
|
|
166
|
+
currentModelList,
|
|
167
|
+
),
|
|
168
|
+
);
|
|
169
|
+
pi.registerTool(createAgentResultTool(manager, agentActivity));
|
|
170
|
+
pi.registerTool(createAgentSteerTool(manager));
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
registerTools();
|
|
174
|
+
|
|
175
|
+
// ── Lifecycle ──────────────────────────────────────────────────────────────
|
|
176
|
+
pi.on("session_start", (_event, ctx) => {
|
|
177
|
+
manager.clearCompleted();
|
|
178
|
+
agentActivity.clear();
|
|
179
|
+
|
|
180
|
+
// Refresh model list from live registry
|
|
181
|
+
const newList = listAvailable(ctx.modelRegistry);
|
|
182
|
+
if (newList.join(",") !== currentModelList.join(",")) {
|
|
183
|
+
currentModelList = newList;
|
|
184
|
+
// Re-register tools with fresh description
|
|
185
|
+
// ponytail: if pi throws on duplicate tool names, skip the re-register
|
|
186
|
+
try {
|
|
187
|
+
registerTools();
|
|
188
|
+
} catch {
|
|
189
|
+
/* description may be stale; non-fatal */
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
pi.on("tool_execution_start", (_event, ctx) => {
|
|
195
|
+
widget.setUICtx(ctx.ui as Parameters<typeof widget.setUICtx>[0]);
|
|
196
|
+
widget.onTurnStart();
|
|
197
|
+
widget.ensureTimer();
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
pi.on("session_shutdown", () => {
|
|
201
|
+
for (const t of pendingNudges.values()) clearTimeout(t);
|
|
202
|
+
pendingNudges.clear();
|
|
203
|
+
manager.abortAll();
|
|
204
|
+
manager.dispose();
|
|
205
|
+
widget.dispose();
|
|
206
|
+
agentActivity.clear();
|
|
207
|
+
if (g[CLEANUP_KEY] === runtimeCleanup) delete g[CLEANUP_KEY];
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
const runtimeCleanup = () => {
|
|
211
|
+
for (const t of pendingNudges.values()) clearTimeout(t);
|
|
212
|
+
pendingNudges.clear();
|
|
213
|
+
widget.dispose();
|
|
214
|
+
};
|
|
215
|
+
g[CLEANUP_KEY] = runtimeCleanup;
|
|
216
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* invocation-config.ts — Resolve per-call agent invocation options.
|
|
3
|
+
*
|
|
4
|
+
* Ported from tintinweb/pi-subagents (MIT). Trimmed: dropped isolation/joinMode.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { AgentConfig, ThinkingLevel } from "./types.ts";
|
|
8
|
+
|
|
9
|
+
interface AgentInvocationParams {
|
|
10
|
+
model?: string;
|
|
11
|
+
thinking?: string;
|
|
12
|
+
max_turns?: number;
|
|
13
|
+
run_in_background?: boolean;
|
|
14
|
+
inherit_context?: boolean;
|
|
15
|
+
isolated?: boolean;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function resolveAgentInvocationConfig(
|
|
19
|
+
agentConfig: AgentConfig | undefined,
|
|
20
|
+
params: AgentInvocationParams,
|
|
21
|
+
): {
|
|
22
|
+
modelInput?: string;
|
|
23
|
+
modelFromParams: boolean;
|
|
24
|
+
thinking?: ThinkingLevel;
|
|
25
|
+
maxTurns?: number;
|
|
26
|
+
inheritContext: boolean;
|
|
27
|
+
runInBackground: boolean;
|
|
28
|
+
isolated: boolean;
|
|
29
|
+
} {
|
|
30
|
+
return {
|
|
31
|
+
modelInput: agentConfig?.model ?? params.model,
|
|
32
|
+
modelFromParams: agentConfig?.model == null && params.model != null,
|
|
33
|
+
thinking: (agentConfig?.thinking ?? params.thinking) as
|
|
34
|
+
| ThinkingLevel
|
|
35
|
+
| undefined,
|
|
36
|
+
maxTurns: agentConfig?.maxTurns ?? params.max_turns,
|
|
37
|
+
inheritContext:
|
|
38
|
+
agentConfig?.inheritContext ?? params.inherit_context ?? false,
|
|
39
|
+
runInBackground:
|
|
40
|
+
agentConfig?.runInBackground ?? params.run_in_background ?? false,
|
|
41
|
+
isolated: agentConfig?.isolated ?? params.isolated ?? false,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Model resolution: exact match ("provider/modelId") with fuzzy fallback.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export interface ModelEntry {
|
|
6
|
+
id: string;
|
|
7
|
+
name: string;
|
|
8
|
+
provider: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface ModelRegistry {
|
|
12
|
+
find(provider: string, modelId: string): any;
|
|
13
|
+
getAll(): any[];
|
|
14
|
+
getAvailable?(): any[];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Resolve a model string to a Model instance.
|
|
19
|
+
* Tries exact match first ("provider/modelId"), then fuzzy match against all available models.
|
|
20
|
+
* Returns the Model on success, or an error message string on failure.
|
|
21
|
+
*/
|
|
22
|
+
export function resolveModel(
|
|
23
|
+
input: string,
|
|
24
|
+
registry: ModelRegistry,
|
|
25
|
+
): any | string {
|
|
26
|
+
// Available models (those with auth configured)
|
|
27
|
+
const all = (registry.getAvailable?.() ?? registry.getAll()) as ModelEntry[];
|
|
28
|
+
const availableSet = new Set(
|
|
29
|
+
all.map((m) => `${m.provider}/${m.id}`.toLowerCase()),
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
// 1. Exact match: "provider/modelId" — only if available (has auth)
|
|
33
|
+
const slashIdx = input.indexOf("/");
|
|
34
|
+
if (slashIdx !== -1) {
|
|
35
|
+
const provider = input.slice(0, slashIdx);
|
|
36
|
+
const modelId = input.slice(slashIdx + 1);
|
|
37
|
+
if (availableSet.has(input.toLowerCase())) {
|
|
38
|
+
const found = registry.find(provider, modelId);
|
|
39
|
+
if (found) return found;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// 2. Fuzzy match against available models
|
|
44
|
+
const query = input.toLowerCase();
|
|
45
|
+
|
|
46
|
+
// Score each model: prefer exact id match > id contains > name contains > provider+id contains
|
|
47
|
+
let bestMatch: ModelEntry | undefined;
|
|
48
|
+
let bestScore = 0;
|
|
49
|
+
|
|
50
|
+
for (const m of all) {
|
|
51
|
+
const id = m.id.toLowerCase();
|
|
52
|
+
const name = m.name.toLowerCase();
|
|
53
|
+
const full = `${m.provider}/${m.id}`.toLowerCase();
|
|
54
|
+
|
|
55
|
+
let score = 0;
|
|
56
|
+
if (id === query || full === query) {
|
|
57
|
+
score = 100; // exact
|
|
58
|
+
} else if (id.includes(query) || full.includes(query)) {
|
|
59
|
+
score = 60 + (query.length / id.length) * 30; // substring, prefer tighter matches
|
|
60
|
+
} else if (name.includes(query)) {
|
|
61
|
+
score = 40 + (query.length / name.length) * 20;
|
|
62
|
+
} else if (
|
|
63
|
+
query
|
|
64
|
+
.split(/[\s\-/]+/)
|
|
65
|
+
.every(
|
|
66
|
+
(part) =>
|
|
67
|
+
id.includes(part) ||
|
|
68
|
+
name.includes(part) ||
|
|
69
|
+
m.provider.toLowerCase().includes(part),
|
|
70
|
+
)
|
|
71
|
+
) {
|
|
72
|
+
score = 20; // all parts present somewhere
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (score > bestScore) {
|
|
76
|
+
bestScore = score;
|
|
77
|
+
bestMatch = m;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (bestMatch && bestScore >= 20) {
|
|
82
|
+
const found = registry.find(bestMatch.provider, bestMatch.id);
|
|
83
|
+
if (found) return found;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// 3. No match — list available models
|
|
87
|
+
const modelList = all
|
|
88
|
+
.map((m) => ` ${m.provider}/${m.id}`)
|
|
89
|
+
.sort()
|
|
90
|
+
.join("\n");
|
|
91
|
+
return `Model not found: "${input}".\n\nAvailable models:\n${modelList}`;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** List available models as "provider/id" strings (for tool-description injection). */
|
|
95
|
+
export function listAvailable(registry: ModelRegistry): string[] {
|
|
96
|
+
const all = (registry.getAvailable?.() ?? registry.getAll()) as ModelEntry[];
|
|
97
|
+
return all.map((m) => `${m.provider}/${m.id}`).sort();
|
|
98
|
+
}
|
package/src/once.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-instance idempotency guard for extension activation.
|
|
3
|
+
*
|
|
4
|
+
* pix-core (the meta-package) invokes this package's factory, and a standalone
|
|
5
|
+
* install makes Pi invoke it again — sometimes against the SAME `pi`. We must
|
|
6
|
+
* dedupe that. But Pi rebuilds the extension runtime on /new, /resume, /fork,
|
|
7
|
+
* and /reload, handing the factory a BRAND-NEW `pi`; that must re-register.
|
|
8
|
+
*
|
|
9
|
+
* Keying the registry on the `pi` instance satisfies both: same instance =>
|
|
10
|
+
* skip, new instance => run. The registry lives on globalThis because jiti
|
|
11
|
+
* (`moduleCache: false`) re-evaluates this module on every load pass, so a
|
|
12
|
+
* module-scoped WeakMap would not be shared between the aggregator pass and the
|
|
13
|
+
* standalone pass within a single session.
|
|
14
|
+
*/
|
|
15
|
+
export function once(pi: object, key: string, fn: () => void): void {
|
|
16
|
+
const g = globalThis as { __pixOnce?: WeakMap<object, Set<string>> };
|
|
17
|
+
const registry = (g.__pixOnce ??= new WeakMap<object, Set<string>>());
|
|
18
|
+
let loaded = registry.get(pi);
|
|
19
|
+
if (!loaded) {
|
|
20
|
+
loaded = new Set<string>();
|
|
21
|
+
registry.set(pi, loaded);
|
|
22
|
+
}
|
|
23
|
+
if (loaded.has(key)) return;
|
|
24
|
+
loaded.add(key);
|
|
25
|
+
fn();
|
|
26
|
+
}
|
package/src/prompts.ts
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* prompts.ts — System prompt builder for agents.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { AgentConfig, EnvInfo } from "./types.ts";
|
|
6
|
+
|
|
7
|
+
/** Extra sections to inject into the system prompt (memory, skills, etc.). */
|
|
8
|
+
export interface PromptExtras {
|
|
9
|
+
/** Persistent memory content to inject (first 200 lines of MEMORY.md + instructions). */
|
|
10
|
+
memoryBlock?: string;
|
|
11
|
+
/** Preloaded skill contents to inject. */
|
|
12
|
+
skillBlocks?: { name: string; content: string }[];
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Build the system prompt for an agent from its config.
|
|
17
|
+
*
|
|
18
|
+
* - "replace" mode: env header + config.systemPrompt (full control, no parent identity)
|
|
19
|
+
* - "append" mode: parent system prompt + sub-agent context + env header + config.systemPrompt
|
|
20
|
+
* - "append" with empty systemPrompt: pure parent clone
|
|
21
|
+
*
|
|
22
|
+
* Both modes include an `<active_agent name="${config.name}"/>` tag so downstream
|
|
23
|
+
* extensions (e.g. permission/policy systems) can resolve per-agent policy
|
|
24
|
+
* inside the child session by parsing the system prompt. In replace mode the tag
|
|
25
|
+
* is prepended; in append mode it follows the shared inherited content so the
|
|
26
|
+
* parent prompt forms an identical, cacheable byte prefix with the parent
|
|
27
|
+
* session (the LLM's KV cache can then reuse those tokens across every spawn).
|
|
28
|
+
*
|
|
29
|
+
* @param parentSystemPrompt The parent agent's effective system prompt (for append mode).
|
|
30
|
+
* @param extras Optional extra sections to inject (memory, preloaded skills).
|
|
31
|
+
*/
|
|
32
|
+
export function buildAgentPrompt(
|
|
33
|
+
config: AgentConfig,
|
|
34
|
+
cwd: string,
|
|
35
|
+
env: EnvInfo,
|
|
36
|
+
parentSystemPrompt?: string,
|
|
37
|
+
extras?: PromptExtras,
|
|
38
|
+
): string {
|
|
39
|
+
const activeAgentTag = `<active_agent name="${config.name}"/>\n\n`;
|
|
40
|
+
|
|
41
|
+
const envBlock = `# Environment
|
|
42
|
+
Working directory: ${cwd}
|
|
43
|
+
${env.isGitRepo ? `Git repository: yes\nBranch: ${env.branch}` : "Not a git repository"}
|
|
44
|
+
Platform: ${env.platform}`;
|
|
45
|
+
|
|
46
|
+
// Build optional extras suffix
|
|
47
|
+
const extraSections: string[] = [];
|
|
48
|
+
if (extras?.memoryBlock) {
|
|
49
|
+
extraSections.push(extras.memoryBlock);
|
|
50
|
+
}
|
|
51
|
+
if (extras?.skillBlocks?.length) {
|
|
52
|
+
for (const skill of extras.skillBlocks) {
|
|
53
|
+
extraSections.push(
|
|
54
|
+
`\n# Preloaded Skill: ${skill.name}\n${skill.content}`,
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
const extrasSuffix =
|
|
59
|
+
extraSections.length > 0 ? `\n\n${extraSections.join("\n")}` : "";
|
|
60
|
+
|
|
61
|
+
if (config.promptMode === "append") {
|
|
62
|
+
const identity = parentSystemPrompt || genericBase;
|
|
63
|
+
|
|
64
|
+
const bridge = `<sub_agent_context>
|
|
65
|
+
You are operating as a sub-agent invoked to handle a specific task.
|
|
66
|
+
- Use the read tool instead of cat/head/tail
|
|
67
|
+
- Use the edit tool instead of sed/awk
|
|
68
|
+
- Use the write tool instead of echo/heredoc
|
|
69
|
+
- Use the find tool instead of bash find/ls for file search
|
|
70
|
+
- Use the grep tool instead of bash grep/rg for content search
|
|
71
|
+
- Make independent tool calls in parallel
|
|
72
|
+
- Use absolute file paths
|
|
73
|
+
- Do not use emojis
|
|
74
|
+
- Be concise but complete
|
|
75
|
+
</sub_agent_context>`;
|
|
76
|
+
|
|
77
|
+
const customSection = config.systemPrompt?.trim()
|
|
78
|
+
? `\n\n<agent_instructions>\n${config.systemPrompt}\n</agent_instructions>`
|
|
79
|
+
: "";
|
|
80
|
+
|
|
81
|
+
// Place shared/stable content first so the LLM's KV cache can reuse the
|
|
82
|
+
// inherited prefix across all subagent invocations. The parent prompt is
|
|
83
|
+
// placed verbatim (no wrapper tag) so it forms an identical byte prefix
|
|
84
|
+
// with the parent session, maximising KV cache hits. The <active_agent>
|
|
85
|
+
// tag and env block vary per call and are placed after the cached prefix.
|
|
86
|
+
return (
|
|
87
|
+
identity +
|
|
88
|
+
"\n\n" +
|
|
89
|
+
bridge +
|
|
90
|
+
"\n\n" +
|
|
91
|
+
activeAgentTag +
|
|
92
|
+
envBlock +
|
|
93
|
+
customSection +
|
|
94
|
+
extrasSuffix
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// "replace" mode — env header + the config's full system prompt
|
|
99
|
+
const replaceHeader = `You are a pi coding agent sub-agent.
|
|
100
|
+
You have been invoked to handle a specific task autonomously.
|
|
101
|
+
|
|
102
|
+
${envBlock}`;
|
|
103
|
+
|
|
104
|
+
return `${activeAgentTag + replaceHeader}\n\n${config.systemPrompt}${extrasSuffix}`;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** Fallback base prompt when parent system prompt is unavailable in append mode. */
|
|
108
|
+
const genericBase = `# Role
|
|
109
|
+
You are a general-purpose coding agent for complex, multi-step tasks.
|
|
110
|
+
You have full access to read, write, edit files, and execute commands.
|
|
111
|
+
Do what has been asked; nothing more, nothing less.`;
|