mirror of
https://github.com/clawdbot/clawdbot.git
synced 2026-02-01 03:47:45 +01:00
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
|
- 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: 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: `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
|
- 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
|
- 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
|
- 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-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.
|
- 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.
|
- `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";
|
} from "./pi-embedded-helpers.js";
|
||||||
import type { EmbeddedPiRunResult } from "./pi-embedded-runner.js";
|
import type { EmbeddedPiRunResult } from "./pi-embedded-runner.js";
|
||||||
import { buildAgentSystemPrompt } from "./system-prompt.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 log = createSubsystemLogger("agent/claude-cli");
|
||||||
const CLAUDE_CLI_QUEUE_KEY = "global";
|
const CLAUDE_CLI_QUEUE_KEY = "global";
|
||||||
@@ -366,7 +369,10 @@ export async function runClaudeCliAgent(params: {
|
|||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
.join("\n");
|
.join("\n");
|
||||||
|
|
||||||
const bootstrapFiles = await loadWorkspaceBootstrapFiles(workspaceDir);
|
const bootstrapFiles = filterBootstrapFilesForSession(
|
||||||
|
await loadWorkspaceBootstrapFiles(workspaceDir),
|
||||||
|
params.sessionKey ?? params.sessionId,
|
||||||
|
);
|
||||||
const contextFiles = buildBootstrapContextFiles(bootstrapFiles);
|
const contextFiles = buildBootstrapContextFiles(bootstrapFiles);
|
||||||
const systemPrompt = buildSystemPrompt({
|
const systemPrompt = buildSystemPrompt({
|
||||||
workspaceDir,
|
workspaceDir,
|
||||||
|
|||||||
@@ -96,10 +96,14 @@ import {
|
|||||||
} from "./skills.js";
|
} from "./skills.js";
|
||||||
import { buildAgentSystemPrompt } from "./system-prompt.js";
|
import { buildAgentSystemPrompt } from "./system-prompt.js";
|
||||||
import { normalizeUsage, type UsageLike } from "./usage.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.
|
// Optional features can be implemented as Pi extensions that run in the same Node process.
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Resolve provider-specific extraParams from model config.
|
* Resolve provider-specific extraParams from model config.
|
||||||
* Auto-enables thinking mode for GLM-4.x models unless explicitly disabled.
|
* Auto-enables thinking mode for GLM-4.x models unless explicitly disabled.
|
||||||
@@ -855,8 +859,10 @@ export async function compactEmbeddedPiSession(params: {
|
|||||||
workspaceDir: effectiveWorkspace,
|
workspaceDir: effectiveWorkspace,
|
||||||
});
|
});
|
||||||
|
|
||||||
const bootstrapFiles =
|
const bootstrapFiles = filterBootstrapFilesForSession(
|
||||||
await loadWorkspaceBootstrapFiles(effectiveWorkspace);
|
await loadWorkspaceBootstrapFiles(effectiveWorkspace),
|
||||||
|
params.sessionKey ?? params.sessionId,
|
||||||
|
);
|
||||||
const contextFiles = buildBootstrapContextFiles(bootstrapFiles);
|
const contextFiles = buildBootstrapContextFiles(bootstrapFiles);
|
||||||
const tools = createClawdbotCodingTools({
|
const tools = createClawdbotCodingTools({
|
||||||
bash: {
|
bash: {
|
||||||
@@ -1194,8 +1200,10 @@ export async function runEmbeddedPiAgent(params: {
|
|||||||
workspaceDir: effectiveWorkspace,
|
workspaceDir: effectiveWorkspace,
|
||||||
});
|
});
|
||||||
|
|
||||||
const bootstrapFiles =
|
const bootstrapFiles = filterBootstrapFilesForSession(
|
||||||
await loadWorkspaceBootstrapFiles(effectiveWorkspace);
|
await loadWorkspaceBootstrapFiles(effectiveWorkspace),
|
||||||
|
params.sessionKey ?? params.sessionId,
|
||||||
|
);
|
||||||
const contextFiles = buildBootstrapContextFiles(bootstrapFiles);
|
const contextFiles = buildBootstrapContextFiles(bootstrapFiles);
|
||||||
// Tool schemas must be provider-compatible (OpenAI requires top-level `type: "object"`).
|
// Tool schemas must be provider-compatible (OpenAI requires top-level `type: "object"`).
|
||||||
// `createClawdbotCodingTools()` normalizes schemas so the session can pass them through unchanged.
|
// `createClawdbotCodingTools()` normalizes schemas so the session can pass them through unchanged.
|
||||||
|
|||||||
@@ -142,20 +142,54 @@ export function buildSubagentSystemPrompt(params: {
|
|||||||
requesterProvider?: string;
|
requesterProvider?: string;
|
||||||
childSessionKey: string;
|
childSessionKey: string;
|
||||||
label?: string;
|
label?: string;
|
||||||
|
task?: string;
|
||||||
}) {
|
}) {
|
||||||
|
const taskText =
|
||||||
|
typeof params.task === "string" && params.task.trim()
|
||||||
|
? params.task.replace(/\s+/g, " ").trim()
|
||||||
|
: "{{TASK_DESCRIPTION}}";
|
||||||
const lines = [
|
const lines = [
|
||||||
"Sub-agent context:",
|
"# Subagent Context",
|
||||||
params.label ? `Label: ${params.label}` : undefined,
|
"",
|
||||||
|
"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
|
params.requesterSessionKey
|
||||||
? `Requester session: ${params.requesterSessionKey}.`
|
? `- Requester session: ${params.requesterSessionKey}.`
|
||||||
: undefined,
|
: undefined,
|
||||||
params.requesterProvider
|
params.requesterProvider
|
||||||
? `Requester provider: ${params.requesterProvider}.`
|
? `- Requester provider: ${params.requesterProvider}.`
|
||||||
: undefined,
|
: undefined,
|
||||||
`Your session: ${params.childSessionKey}.`,
|
`- Your session: ${params.childSessionKey}.`,
|
||||||
|
"",
|
||||||
"Run the task. Provide a clear final answer (plain text).",
|
"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.',
|
'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");
|
return lines.join("\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -162,6 +162,7 @@ export function createSessionsSpawnTool(opts?: {
|
|||||||
requesterProvider: opts?.agentProvider,
|
requesterProvider: opts?.agentProvider,
|
||||||
childSessionKey,
|
childSessionKey,
|
||||||
label: label || undefined,
|
label: label || undefined,
|
||||||
|
task,
|
||||||
});
|
});
|
||||||
|
|
||||||
const childIdem = crypto.randomUUID();
|
const childIdem = crypto.randomUUID();
|
||||||
|
|||||||
@@ -2,7 +2,18 @@ import fs from "node:fs/promises";
|
|||||||
import os from "node:os";
|
import os from "node:os";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import { describe, expect, it } from "vitest";
|
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", () => {
|
describe("ensureAgentWorkspace", () => {
|
||||||
it("creates directory and bootstrap files when missing", async () => {
|
it("creates directory and bootstrap files when missing", async () => {
|
||||||
@@ -52,3 +63,71 @@ describe("ensureAgentWorkspace", () => {
|
|||||||
await expect(fs.stat(bootstrapPath)).rejects.toBeDefined();
|
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 path from "node:path";
|
||||||
import { fileURLToPath } from "node:url";
|
import { fileURLToPath } from "node:url";
|
||||||
|
|
||||||
|
import { isSubagentSessionKey } from "../routing/session-key.js";
|
||||||
import { resolveUserPath } from "../utils.js";
|
import { resolveUserPath } from "../utils.js";
|
||||||
|
|
||||||
export function resolveDefaultAgentWorkspaceDir(
|
export function resolveDefaultAgentWorkspaceDir(
|
||||||
@@ -362,3 +363,16 @@ export async function loadWorkspaceBootstrapFiles(
|
|||||||
}
|
}
|
||||||
return result;
|
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