fix: limit subagent bootstrap context
This commit is contained in:
@@ -29,6 +29,7 @@
|
||||
- Models: centralize model override validation + hooks Gmail warnings in doctor. (#602) — thanks @steipete
|
||||
- Agents: avoid base-to-string error stringification in model fallback. (#604) — thanks @steipete
|
||||
- Agents: `sessions_spawn` inherits the requester's provider for child runs (avoid WhatsApp fallback). (#528) — thanks @rlmestre
|
||||
- Agents: sub-agent context now injects only AGENTS.md + TOOLS.md (omits identity/user/soul/heartbeat/bootstrap). — thanks @steipete
|
||||
- Gateway/CLI: harden agent provider routing + validation (Slack/MS Teams + aliases). (follow-up #528) — thanks @steipete
|
||||
- Agents: treat billing/insufficient-credits errors as failover-worthy so model fallbacks kick in. (#486) — thanks @steipete
|
||||
- Auth: default billing disable backoff to 5h (doubling, 24h cap) and surface disabled/cooldown profiles in `models list` + doctor. (#486) — thanks @steipete
|
||||
|
||||
@@ -98,3 +98,4 @@ Sub-agents use a dedicated in-process queue lane:
|
||||
- Sub-agent announce is **best-effort**. If the gateway restarts, pending “announce back” work is lost.
|
||||
- Sub-agents still share the same gateway process resources; treat `maxConcurrent` as a safety valve.
|
||||
- `sessions_spawn` is always non-blocking: it returns `{ status: "accepted", runId, childSessionKey }` immediately.
|
||||
- Sub-agent context only injects `AGENTS.md` + `TOOLS.md` (no `SOUL.md`, `IDENTITY.md`, `USER.md`, `HEARTBEAT.md`, or `BOOTSTRAP.md`).
|
||||
|
||||
@@ -18,7 +18,10 @@ import {
|
||||
} from "./pi-embedded-helpers.js";
|
||||
import type { EmbeddedPiRunResult } from "./pi-embedded-runner.js";
|
||||
import { buildAgentSystemPrompt } from "./system-prompt.js";
|
||||
import { loadWorkspaceBootstrapFiles } from "./workspace.js";
|
||||
import {
|
||||
filterBootstrapFilesForSession,
|
||||
loadWorkspaceBootstrapFiles,
|
||||
} from "./workspace.js";
|
||||
|
||||
const log = createSubsystemLogger("agent/claude-cli");
|
||||
const CLAUDE_CLI_QUEUE_KEY = "global";
|
||||
@@ -366,7 +369,10 @@ export async function runClaudeCliAgent(params: {
|
||||
.filter(Boolean)
|
||||
.join("\n");
|
||||
|
||||
const bootstrapFiles = await loadWorkspaceBootstrapFiles(workspaceDir);
|
||||
const bootstrapFiles = filterBootstrapFilesForSession(
|
||||
await loadWorkspaceBootstrapFiles(workspaceDir),
|
||||
params.sessionKey ?? params.sessionId,
|
||||
);
|
||||
const contextFiles = buildBootstrapContextFiles(bootstrapFiles);
|
||||
const systemPrompt = buildSystemPrompt({
|
||||
workspaceDir,
|
||||
|
||||
@@ -96,10 +96,14 @@ import {
|
||||
} from "./skills.js";
|
||||
import { buildAgentSystemPrompt } from "./system-prompt.js";
|
||||
import { normalizeUsage, type UsageLike } from "./usage.js";
|
||||
import { loadWorkspaceBootstrapFiles } from "./workspace.js";
|
||||
import {
|
||||
filterBootstrapFilesForSession,
|
||||
loadWorkspaceBootstrapFiles,
|
||||
} from "./workspace.js";
|
||||
|
||||
// Optional features can be implemented as Pi extensions that run in the same Node process.
|
||||
|
||||
|
||||
/**
|
||||
* Resolve provider-specific extraParams from model config.
|
||||
* Auto-enables thinking mode for GLM-4.x models unless explicitly disabled.
|
||||
@@ -855,8 +859,10 @@ export async function compactEmbeddedPiSession(params: {
|
||||
workspaceDir: effectiveWorkspace,
|
||||
});
|
||||
|
||||
const bootstrapFiles =
|
||||
await loadWorkspaceBootstrapFiles(effectiveWorkspace);
|
||||
const bootstrapFiles = filterBootstrapFilesForSession(
|
||||
await loadWorkspaceBootstrapFiles(effectiveWorkspace),
|
||||
params.sessionKey ?? params.sessionId,
|
||||
);
|
||||
const contextFiles = buildBootstrapContextFiles(bootstrapFiles);
|
||||
const tools = createClawdbotCodingTools({
|
||||
bash: {
|
||||
@@ -1194,8 +1200,10 @@ export async function runEmbeddedPiAgent(params: {
|
||||
workspaceDir: effectiveWorkspace,
|
||||
});
|
||||
|
||||
const bootstrapFiles =
|
||||
await loadWorkspaceBootstrapFiles(effectiveWorkspace);
|
||||
const bootstrapFiles = filterBootstrapFilesForSession(
|
||||
await loadWorkspaceBootstrapFiles(effectiveWorkspace),
|
||||
params.sessionKey ?? params.sessionId,
|
||||
);
|
||||
const contextFiles = buildBootstrapContextFiles(bootstrapFiles);
|
||||
// Tool schemas must be provider-compatible (OpenAI requires top-level `type: "object"`).
|
||||
// `createClawdbotCodingTools()` normalizes schemas so the session can pass them through unchanged.
|
||||
|
||||
@@ -142,20 +142,54 @@ export function buildSubagentSystemPrompt(params: {
|
||||
requesterProvider?: string;
|
||||
childSessionKey: string;
|
||||
label?: string;
|
||||
task?: string;
|
||||
}) {
|
||||
const taskText =
|
||||
typeof params.task === "string" && params.task.trim()
|
||||
? params.task.replace(/\s+/g, " ").trim()
|
||||
: "{{TASK_DESCRIPTION}}";
|
||||
const lines = [
|
||||
"Sub-agent context:",
|
||||
params.label ? `Label: ${params.label}` : undefined,
|
||||
"# Subagent Context",
|
||||
"",
|
||||
"You are a **subagent** spawned by the main agent for a specific task.",
|
||||
"",
|
||||
"## Your Role",
|
||||
`- You were created to handle: ${taskText}`,
|
||||
"- Complete this task and report back. That's your entire purpose.",
|
||||
"- You are NOT the main agent. Don't try to be.",
|
||||
"",
|
||||
"## Rules",
|
||||
"1. **Stay focused** - Do your assigned task, nothing else",
|
||||
"2. **Report completion** - When done, summarize results clearly",
|
||||
"3. **Don't initiate** - No heartbeats, no proactive actions, no side quests",
|
||||
"4. **Ask the spawner** - If blocked or confused, report back rather than improvising",
|
||||
"5. **Be ephemeral** - You may be terminated after task completion. That's fine.",
|
||||
"",
|
||||
"## What You DON'T Do",
|
||||
"- NO user conversations (that's main agent's job)",
|
||||
"- NO external messages (email, tweets, etc.) unless explicitly tasked",
|
||||
"- NO cron jobs or persistent state",
|
||||
"- NO pretending to be the main agent",
|
||||
"",
|
||||
"## Output Format",
|
||||
"When complete, respond with:",
|
||||
"- **Status:** success | failed | blocked",
|
||||
"- **Result:** [what you accomplished]",
|
||||
"- **Notes:** [anything the main agent should know] - discuss gimme options",
|
||||
"",
|
||||
"## Session Context",
|
||||
params.label ? `- Label: ${params.label}` : undefined,
|
||||
params.requesterSessionKey
|
||||
? `Requester session: ${params.requesterSessionKey}.`
|
||||
? `- Requester session: ${params.requesterSessionKey}.`
|
||||
: undefined,
|
||||
params.requesterProvider
|
||||
? `Requester provider: ${params.requesterProvider}.`
|
||||
? `- Requester provider: ${params.requesterProvider}.`
|
||||
: undefined,
|
||||
`Your session: ${params.childSessionKey}.`,
|
||||
`- Your session: ${params.childSessionKey}.`,
|
||||
"",
|
||||
"Run the task. Provide a clear final answer (plain text).",
|
||||
'After you finish, you may be asked to produce an "announce" message to post back to the requester chat.',
|
||||
].filter(Boolean);
|
||||
].filter((line): line is string => line !== undefined);
|
||||
return lines.join("\n");
|
||||
}
|
||||
|
||||
|
||||
@@ -162,6 +162,7 @@ export function createSessionsSpawnTool(opts?: {
|
||||
requesterProvider: opts?.agentProvider,
|
||||
childSessionKey,
|
||||
label: label || undefined,
|
||||
task,
|
||||
});
|
||||
|
||||
const childIdem = crypto.randomUUID();
|
||||
|
||||
@@ -2,7 +2,18 @@ import fs from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { ensureAgentWorkspace } from "./workspace.js";
|
||||
import type { WorkspaceBootstrapFile } from "./workspace.js";
|
||||
import {
|
||||
DEFAULT_AGENTS_FILENAME,
|
||||
DEFAULT_BOOTSTRAP_FILENAME,
|
||||
DEFAULT_HEARTBEAT_FILENAME,
|
||||
DEFAULT_IDENTITY_FILENAME,
|
||||
DEFAULT_SOUL_FILENAME,
|
||||
DEFAULT_TOOLS_FILENAME,
|
||||
DEFAULT_USER_FILENAME,
|
||||
ensureAgentWorkspace,
|
||||
filterBootstrapFilesForSession,
|
||||
} from "./workspace.js";
|
||||
|
||||
describe("ensureAgentWorkspace", () => {
|
||||
it("creates directory and bootstrap files when missing", async () => {
|
||||
@@ -52,3 +63,71 @@ describe("ensureAgentWorkspace", () => {
|
||||
await expect(fs.stat(bootstrapPath)).rejects.toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("filterBootstrapFilesForSession", () => {
|
||||
const files: WorkspaceBootstrapFile[] = [
|
||||
{
|
||||
name: DEFAULT_AGENTS_FILENAME,
|
||||
path: "/tmp/AGENTS.md",
|
||||
content: "agents",
|
||||
missing: false,
|
||||
},
|
||||
{
|
||||
name: DEFAULT_SOUL_FILENAME,
|
||||
path: "/tmp/SOUL.md",
|
||||
content: "soul",
|
||||
missing: false,
|
||||
},
|
||||
{
|
||||
name: DEFAULT_TOOLS_FILENAME,
|
||||
path: "/tmp/TOOLS.md",
|
||||
content: "tools",
|
||||
missing: false,
|
||||
},
|
||||
{
|
||||
name: DEFAULT_IDENTITY_FILENAME,
|
||||
path: "/tmp/IDENTITY.md",
|
||||
content: "identity",
|
||||
missing: false,
|
||||
},
|
||||
{
|
||||
name: DEFAULT_USER_FILENAME,
|
||||
path: "/tmp/USER.md",
|
||||
content: "user",
|
||||
missing: false,
|
||||
},
|
||||
{
|
||||
name: DEFAULT_HEARTBEAT_FILENAME,
|
||||
path: "/tmp/HEARTBEAT.md",
|
||||
content: "heartbeat",
|
||||
missing: false,
|
||||
},
|
||||
{
|
||||
name: DEFAULT_BOOTSTRAP_FILENAME,
|
||||
path: "/tmp/BOOTSTRAP.md",
|
||||
content: "bootstrap",
|
||||
missing: false,
|
||||
},
|
||||
];
|
||||
|
||||
it("keeps full bootstrap set for non-subagent sessions", () => {
|
||||
const result = filterBootstrapFilesForSession(
|
||||
files,
|
||||
"agent:main:session:abc",
|
||||
);
|
||||
expect(result.map((file) => file.name)).toEqual(
|
||||
files.map((file) => file.name),
|
||||
);
|
||||
});
|
||||
|
||||
it("limits bootstrap files for subagent sessions", () => {
|
||||
const result = filterBootstrapFilesForSession(
|
||||
files,
|
||||
"agent:main:subagent:abc",
|
||||
);
|
||||
expect(result.map((file) => file.name)).toEqual([
|
||||
DEFAULT_AGENTS_FILENAME,
|
||||
DEFAULT_TOOLS_FILENAME,
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3,6 +3,7 @@ import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
import { isSubagentSessionKey } from "../routing/session-key.js";
|
||||
import { resolveUserPath } from "../utils.js";
|
||||
|
||||
export function resolveDefaultAgentWorkspaceDir(
|
||||
@@ -362,3 +363,16 @@ export async function loadWorkspaceBootstrapFiles(
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
const SUBAGENT_BOOTSTRAP_ALLOWLIST = new Set([
|
||||
DEFAULT_AGENTS_FILENAME,
|
||||
DEFAULT_TOOLS_FILENAME,
|
||||
]);
|
||||
|
||||
export function filterBootstrapFilesForSession(
|
||||
files: WorkspaceBootstrapFile[],
|
||||
sessionKey?: string,
|
||||
): WorkspaceBootstrapFile[] {
|
||||
if (!sessionKey || !isSubagentSessionKey(sessionKey)) return files;
|
||||
return files.filter((file) => SUBAGENT_BOOTSTRAP_ALLOWLIST.has(file.name));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user