feat: allow sessions_spawn cross-agent
This commit is contained in:
@@ -79,6 +79,7 @@
|
|||||||
- Agent: return a friendly context overflow response (413/request_too_large). Thanks @alejandroOPI for PR #395.
|
- Agent: return a friendly context overflow response (413/request_too_large). Thanks @alejandroOPI for PR #395.
|
||||||
- Sub-agents: allow `sessions_spawn` model overrides and error on invalid models. Thanks @azade-c for PR #298.
|
- Sub-agents: allow `sessions_spawn` model overrides and error on invalid models. Thanks @azade-c for PR #298.
|
||||||
- Sub-agents: skip invalid model overrides with a warning and keep the run alive; tool exceptions now return tool errors instead of crashing the agent.
|
- Sub-agents: skip invalid model overrides with a warning and keep the run alive; tool exceptions now return tool errors instead of crashing the agent.
|
||||||
|
- Sub-agents: allow `sessions_spawn` to target other agents via per-agent allowlists (`routing.agents.<agentId>.subagents.allowAgents`).
|
||||||
- Sessions: forward explicit sessionKey through gateway/chat/node bridge to avoid sub-agent sessionId mixups.
|
- Sessions: forward explicit sessionKey through gateway/chat/node bridge to avoid sub-agent sessionId mixups.
|
||||||
- Heartbeat: default interval 30m; clarified default prompt usage and HEARTBEAT.md template behavior.
|
- Heartbeat: default interval 30m; clarified default prompt usage and HEARTBEAT.md template behavior.
|
||||||
- Onboarding: write auth profiles to the multi-agent path (`~/.clawdbot/agents/main/agent/`) so the gateway finds credentials on first startup. Thanks @minghinmatthewlam for PR #327.
|
- Onboarding: write auth profiles to the multi-agent path (`~/.clawdbot/agents/main/agent/`) so the gateway finds credentials on first startup. Thanks @minghinmatthewlam for PR #327.
|
||||||
|
|||||||
@@ -126,10 +126,14 @@ Spawn a sub-agent run in an isolated session and announce the result back to the
|
|||||||
Parameters:
|
Parameters:
|
||||||
- `task` (required)
|
- `task` (required)
|
||||||
- `label?` (optional; used for logs/UI)
|
- `label?` (optional; used for logs/UI)
|
||||||
|
- `agentId?` (optional; spawn under another agent id if allowed)
|
||||||
- `model?` (optional; overrides the sub-agent model; invalid values error)
|
- `model?` (optional; overrides the sub-agent model; invalid values error)
|
||||||
- `runTimeoutSeconds?` (default 0; when set, aborts the sub-agent run after N seconds)
|
- `runTimeoutSeconds?` (default 0; when set, aborts the sub-agent run after N seconds)
|
||||||
- `cleanup?` (`delete|keep`, default `keep`)
|
- `cleanup?` (`delete|keep`, default `keep`)
|
||||||
|
|
||||||
|
Allowlist:
|
||||||
|
- `routing.agents.<agentId>.subagents.allowAgents`: list of agent ids allowed via `agentId` (`["*"]` to allow any). Default: only the requester agent.
|
||||||
|
|
||||||
Behavior:
|
Behavior:
|
||||||
- Starts a new `agent:<agentId>:subagent:<uuid>` session with `deliver: false`.
|
- Starts a new `agent:<agentId>:subagent:<uuid>` session with `deliver: false`.
|
||||||
- Sub-agents default to the full tool set **minus session tools** (configurable via `agent.subagents.tools`).
|
- Sub-agents default to the full tool set **minus session tools** (configurable via `agent.subagents.tools`).
|
||||||
|
|||||||
@@ -392,6 +392,8 @@ Run multiple isolated agents (separate workspace, `agentDir`, sessions) inside o
|
|||||||
- `browser`: per-agent sandboxed browser overrides (ignored when `scope: "shared"`)
|
- `browser`: per-agent sandboxed browser overrides (ignored when `scope: "shared"`)
|
||||||
- `prune`: per-agent sandbox pruning overrides (ignored when `scope: "shared"`)
|
- `prune`: per-agent sandbox pruning overrides (ignored when `scope: "shared"`)
|
||||||
- `tools`: per-agent sandbox tool policy (deny wins; overrides `agent.sandbox.tools`)
|
- `tools`: per-agent sandbox tool policy (deny wins; overrides `agent.sandbox.tools`)
|
||||||
|
- `subagents`: per-agent sub-agent defaults.
|
||||||
|
- `allowAgents`: allowlist of agent ids for `sessions_spawn` from this agent (`["*"]` = allow any; default: only same agent)
|
||||||
- `tools`: per-agent tool restrictions (overrides `agent.tools`; applied before sandbox tool policy).
|
- `tools`: per-agent tool restrictions (overrides `agent.tools`; applied before sandbox tool policy).
|
||||||
- `allow`: array of allowed tool names
|
- `allow`: array of allowed tool names
|
||||||
- `deny`: array of denied tool names (deny wins)
|
- `deny`: array of denied tool names (deny wins)
|
||||||
|
|||||||
@@ -177,7 +177,7 @@ Core parameters:
|
|||||||
- `sessions_list`: `kinds?`, `limit?`, `activeMinutes?`, `messageLimit?` (0 = none)
|
- `sessions_list`: `kinds?`, `limit?`, `activeMinutes?`, `messageLimit?` (0 = none)
|
||||||
- `sessions_history`: `sessionKey`, `limit?`, `includeTools?`
|
- `sessions_history`: `sessionKey`, `limit?`, `includeTools?`
|
||||||
- `sessions_send`: `sessionKey`, `message`, `timeoutSeconds?` (0 = fire-and-forget)
|
- `sessions_send`: `sessionKey`, `message`, `timeoutSeconds?` (0 = fire-and-forget)
|
||||||
- `sessions_spawn`: `task`, `label?`, `model?`, `runTimeoutSeconds?`, `cleanup?`
|
- `sessions_spawn`: `task`, `label?`, `agentId?`, `model?`, `runTimeoutSeconds?`, `cleanup?`
|
||||||
|
|
||||||
Notes:
|
Notes:
|
||||||
- `main` is the canonical direct-chat key; global/unknown are hidden.
|
- `main` is the canonical direct-chat key; global/unknown are hidden.
|
||||||
|
|||||||
@@ -24,10 +24,14 @@ Use `sessions_spawn`:
|
|||||||
Tool params:
|
Tool params:
|
||||||
- `task` (required)
|
- `task` (required)
|
||||||
- `label?` (optional)
|
- `label?` (optional)
|
||||||
|
- `agentId?` (optional; spawn under another agent id if allowed)
|
||||||
- `model?` (optional; overrides the sub-agent model; invalid values are skipped and the sub-agent runs on the default model with a warning in the tool result)
|
- `model?` (optional; overrides the sub-agent model; invalid values are skipped and the sub-agent runs on the default model with a warning in the tool result)
|
||||||
- `runTimeoutSeconds?` (default `0`; when set, the sub-agent run is aborted after N seconds)
|
- `runTimeoutSeconds?` (default `0`; when set, the sub-agent run is aborted after N seconds)
|
||||||
- `cleanup?` (`delete|keep`, default `keep`)
|
- `cleanup?` (`delete|keep`, default `keep`)
|
||||||
|
|
||||||
|
Allowlist:
|
||||||
|
- `routing.agents.<agentId>.subagents.allowAgents`: list of agent ids that can be targeted via `agentId` (`["*"]` to allow any). Default: only the requester agent.
|
||||||
|
|
||||||
Auto-archive:
|
Auto-archive:
|
||||||
- Sub-agent sessions are automatically archived after `agent.subagents.archiveAfterMinutes` (default: 60).
|
- Sub-agent sessions are automatically archived after `agent.subagents.archiveAfterMinutes` (default: 60).
|
||||||
- Archive uses `sessions.delete` and renames the transcript to `*.deleted.<timestamp>` (same folder).
|
- Archive uses `sessions.delete` and renames the transcript to `*.deleted.<timestamp>` (same folder).
|
||||||
|
|||||||
@@ -27,6 +27,9 @@ export function resolveAgentConfig(
|
|||||||
workspace?: string;
|
workspace?: string;
|
||||||
agentDir?: string;
|
agentDir?: string;
|
||||||
model?: string;
|
model?: string;
|
||||||
|
subagents?: {
|
||||||
|
allowAgents?: string[];
|
||||||
|
};
|
||||||
sandbox?: {
|
sandbox?: {
|
||||||
mode?: "off" | "non-main" | "all";
|
mode?: "off" | "non-main" | "all";
|
||||||
workspaceAccess?: "none" | "ro" | "rw";
|
workspaceAccess?: "none" | "ro" | "rw";
|
||||||
@@ -55,6 +58,10 @@ export function resolveAgentConfig(
|
|||||||
typeof entry.workspace === "string" ? entry.workspace : undefined,
|
typeof entry.workspace === "string" ? entry.workspace : undefined,
|
||||||
agentDir: typeof entry.agentDir === "string" ? entry.agentDir : undefined,
|
agentDir: typeof entry.agentDir === "string" ? entry.agentDir : undefined,
|
||||||
model: typeof entry.model === "string" ? entry.model : undefined,
|
model: typeof entry.model === "string" ? entry.model : undefined,
|
||||||
|
subagents:
|
||||||
|
typeof entry.subagents === "object" && entry.subagents
|
||||||
|
? entry.subagents
|
||||||
|
: undefined,
|
||||||
sandbox: entry.sandbox,
|
sandbox: entry.sandbox,
|
||||||
tools: entry.tools,
|
tools: entry.tools,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,20 +1,24 @@
|
|||||||
import { describe, expect, it, vi } from "vitest";
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
|
|
||||||
const callGatewayMock = vi.fn();
|
const callGatewayMock = vi.fn();
|
||||||
vi.mock("../gateway/call.js", () => ({
|
vi.mock("../gateway/call.js", () => ({
|
||||||
callGateway: (opts: unknown) => callGatewayMock(opts),
|
callGateway: (opts: unknown) => callGatewayMock(opts),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
let configOverride: ReturnType<
|
||||||
|
typeof import("../config/config.js")["loadConfig"]
|
||||||
|
> = {
|
||||||
|
session: {
|
||||||
|
mainKey: "main",
|
||||||
|
scope: "per-sender",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
vi.mock("../config/config.js", async (importOriginal) => {
|
vi.mock("../config/config.js", async (importOriginal) => {
|
||||||
const actual = await importOriginal<typeof import("../config/config.js")>();
|
const actual = await importOriginal<typeof import("../config/config.js")>();
|
||||||
return {
|
return {
|
||||||
...actual,
|
...actual,
|
||||||
loadConfig: () => ({
|
loadConfig: () => configOverride,
|
||||||
session: {
|
|
||||||
mainKey: "main",
|
|
||||||
scope: "per-sender",
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
resolveGatewayPort: () => 18789,
|
resolveGatewayPort: () => 18789,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
@@ -24,6 +28,15 @@ import { createClawdbotTools } from "./clawdbot-tools.js";
|
|||||||
import { resetSubagentRegistryForTests } from "./subagent-registry.js";
|
import { resetSubagentRegistryForTests } from "./subagent-registry.js";
|
||||||
|
|
||||||
describe("subagents", () => {
|
describe("subagents", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
configOverride = {
|
||||||
|
session: {
|
||||||
|
mainKey: "main",
|
||||||
|
scope: "per-sender",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
it("sessions_spawn announces back to the requester group provider", async () => {
|
it("sessions_spawn announces back to the requester group provider", async () => {
|
||||||
resetSubagentRegistryForTests();
|
resetSubagentRegistryForTests();
|
||||||
callGatewayMock.mockReset();
|
callGatewayMock.mockReset();
|
||||||
@@ -273,6 +286,92 @@ describe("subagents", () => {
|
|||||||
expect(childSessionKey?.startsWith("agent:main:subagent:")).toBe(true);
|
expect(childSessionKey?.startsWith("agent:main:subagent:")).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("sessions_spawn allows cross-agent spawning when configured", async () => {
|
||||||
|
resetSubagentRegistryForTests();
|
||||||
|
callGatewayMock.mockReset();
|
||||||
|
configOverride = {
|
||||||
|
session: {
|
||||||
|
mainKey: "main",
|
||||||
|
scope: "per-sender",
|
||||||
|
},
|
||||||
|
routing: {
|
||||||
|
agents: {
|
||||||
|
main: {
|
||||||
|
subagents: {
|
||||||
|
allowAgents: ["beta"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
let childSessionKey: string | undefined;
|
||||||
|
callGatewayMock.mockImplementation(async (opts: unknown) => {
|
||||||
|
const request = opts as { method?: string; params?: unknown };
|
||||||
|
if (request.method === "agent") {
|
||||||
|
const params = request.params as { sessionKey?: string } | undefined;
|
||||||
|
childSessionKey = params?.sessionKey;
|
||||||
|
return { runId: "run-1", status: "accepted", acceptedAt: 5000 };
|
||||||
|
}
|
||||||
|
if (request.method === "agent.wait") {
|
||||||
|
return { status: "timeout" };
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
});
|
||||||
|
|
||||||
|
const tool = createClawdbotTools({
|
||||||
|
agentSessionKey: "main",
|
||||||
|
agentProvider: "whatsapp",
|
||||||
|
}).find((candidate) => candidate.name === "sessions_spawn");
|
||||||
|
if (!tool) throw new Error("missing sessions_spawn tool");
|
||||||
|
|
||||||
|
const result = await tool.execute("call6", {
|
||||||
|
task: "do thing",
|
||||||
|
agentId: "beta",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.details).toMatchObject({
|
||||||
|
status: "accepted",
|
||||||
|
runId: "run-1",
|
||||||
|
});
|
||||||
|
expect(childSessionKey?.startsWith("agent:beta:subagent:")).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("sessions_spawn forbids cross-agent spawning when not allowed", async () => {
|
||||||
|
resetSubagentRegistryForTests();
|
||||||
|
callGatewayMock.mockReset();
|
||||||
|
configOverride = {
|
||||||
|
session: {
|
||||||
|
mainKey: "main",
|
||||||
|
scope: "per-sender",
|
||||||
|
},
|
||||||
|
routing: {
|
||||||
|
agents: {
|
||||||
|
main: {
|
||||||
|
subagents: {
|
||||||
|
allowAgents: ["alpha"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const tool = createClawdbotTools({
|
||||||
|
agentSessionKey: "main",
|
||||||
|
agentProvider: "whatsapp",
|
||||||
|
}).find((candidate) => candidate.name === "sessions_spawn");
|
||||||
|
if (!tool) throw new Error("missing sessions_spawn tool");
|
||||||
|
|
||||||
|
const result = await tool.execute("call7", {
|
||||||
|
task: "do thing",
|
||||||
|
agentId: "beta",
|
||||||
|
});
|
||||||
|
expect(result.details).toMatchObject({
|
||||||
|
status: "forbidden",
|
||||||
|
});
|
||||||
|
expect(callGatewayMock).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
it("sessions_spawn applies a model to the child session", async () => {
|
it("sessions_spawn applies a model to the child session", async () => {
|
||||||
resetSubagentRegistryForTests();
|
resetSubagentRegistryForTests();
|
||||||
callGatewayMock.mockReset();
|
callGatewayMock.mockReset();
|
||||||
|
|||||||
@@ -168,7 +168,7 @@
|
|||||||
"sessions_spawn": {
|
"sessions_spawn": {
|
||||||
"emoji": "🧑🔧",
|
"emoji": "🧑🔧",
|
||||||
"title": "Sub-agent",
|
"title": "Sub-agent",
|
||||||
"detailKeys": ["label", "runTimeoutSeconds", "cleanup"]
|
"detailKeys": ["label", "agentId", "runTimeoutSeconds", "cleanup"]
|
||||||
},
|
},
|
||||||
"whatsapp_login": {
|
"whatsapp_login": {
|
||||||
"emoji": "🟢",
|
"emoji": "🟢",
|
||||||
|
|||||||
@@ -28,14 +28,14 @@ export async function runAgentStep(params: {
|
|||||||
const stepIdem = crypto.randomUUID();
|
const stepIdem = crypto.randomUUID();
|
||||||
const response = (await callGateway({
|
const response = (await callGateway({
|
||||||
method: "agent",
|
method: "agent",
|
||||||
params: {
|
params: {
|
||||||
message: params.message,
|
message: params.message,
|
||||||
sessionKey: params.sessionKey,
|
sessionKey: params.sessionKey,
|
||||||
idempotencyKey: stepIdem,
|
idempotencyKey: stepIdem,
|
||||||
deliver: false,
|
deliver: false,
|
||||||
lane: params.lane ?? "nested",
|
lane: params.lane ?? "nested",
|
||||||
extraSystemPrompt: params.extraSystemPrompt,
|
extraSystemPrompt: params.extraSystemPrompt,
|
||||||
},
|
},
|
||||||
timeoutMs: 10_000,
|
timeoutMs: 10_000,
|
||||||
})) as { runId?: string; acceptedAt?: number };
|
})) as { runId?: string; acceptedAt?: number };
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import {
|
|||||||
normalizeAgentId,
|
normalizeAgentId,
|
||||||
parseAgentSessionKey,
|
parseAgentSessionKey,
|
||||||
} from "../../routing/session-key.js";
|
} from "../../routing/session-key.js";
|
||||||
|
import { resolveAgentConfig } from "../agent-scope.js";
|
||||||
import { buildSubagentSystemPrompt } from "../subagent-announce.js";
|
import { buildSubagentSystemPrompt } from "../subagent-announce.js";
|
||||||
import { registerSubagentRun } from "../subagent-registry.js";
|
import { registerSubagentRun } from "../subagent-registry.js";
|
||||||
import type { AnyAgentTool } from "./common.js";
|
import type { AnyAgentTool } from "./common.js";
|
||||||
@@ -22,6 +23,7 @@ import {
|
|||||||
const SessionsSpawnToolSchema = Type.Object({
|
const SessionsSpawnToolSchema = Type.Object({
|
||||||
task: Type.String(),
|
task: Type.String(),
|
||||||
label: Type.Optional(Type.String()),
|
label: Type.Optional(Type.String()),
|
||||||
|
agentId: Type.Optional(Type.String()),
|
||||||
model: Type.Optional(Type.String()),
|
model: Type.Optional(Type.String()),
|
||||||
runTimeoutSeconds: Type.Optional(Type.Integer({ minimum: 0 })),
|
runTimeoutSeconds: Type.Optional(Type.Integer({ minimum: 0 })),
|
||||||
// Back-compat alias. Prefer runTimeoutSeconds.
|
// Back-compat alias. Prefer runTimeoutSeconds.
|
||||||
@@ -46,6 +48,7 @@ export function createSessionsSpawnTool(opts?: {
|
|||||||
const params = args as Record<string, unknown>;
|
const params = args as Record<string, unknown>;
|
||||||
const task = readStringParam(params, "task", { required: true });
|
const task = readStringParam(params, "task", { required: true });
|
||||||
const label = typeof params.label === "string" ? params.label.trim() : "";
|
const label = typeof params.label === "string" ? params.label.trim() : "";
|
||||||
|
const requestedAgentId = readStringParam(params, "agentId");
|
||||||
const model = readStringParam(params, "model");
|
const model = readStringParam(params, "model");
|
||||||
const cleanup =
|
const cleanup =
|
||||||
params.cleanup === "keep" || params.cleanup === "delete"
|
params.cleanup === "keep" || params.cleanup === "delete"
|
||||||
@@ -96,7 +99,34 @@ export function createSessionsSpawnTool(opts?: {
|
|||||||
const requesterAgentId = normalizeAgentId(
|
const requesterAgentId = normalizeAgentId(
|
||||||
parseAgentSessionKey(requesterInternalKey)?.agentId,
|
parseAgentSessionKey(requesterInternalKey)?.agentId,
|
||||||
);
|
);
|
||||||
const childSessionKey = `agent:${requesterAgentId}:subagent:${crypto.randomUUID()}`;
|
const targetAgentId = requestedAgentId
|
||||||
|
? normalizeAgentId(requestedAgentId)
|
||||||
|
: requesterAgentId;
|
||||||
|
if (targetAgentId !== requesterAgentId) {
|
||||||
|
const allowAgents =
|
||||||
|
resolveAgentConfig(cfg, requesterAgentId)?.subagents?.allowAgents ??
|
||||||
|
[];
|
||||||
|
const allowAny = allowAgents.some(
|
||||||
|
(value) => value.trim() === "*",
|
||||||
|
);
|
||||||
|
const allowSet = new Set(
|
||||||
|
allowAgents
|
||||||
|
.filter((value) => value.trim() && value.trim() !== "*")
|
||||||
|
.map((value) => normalizeAgentId(value)),
|
||||||
|
);
|
||||||
|
if (!allowAny && !allowSet.has(targetAgentId)) {
|
||||||
|
const allowedText = allowAny
|
||||||
|
? "*"
|
||||||
|
: allowSet.size > 0
|
||||||
|
? Array.from(allowSet).join(", ")
|
||||||
|
: "none";
|
||||||
|
return jsonResult({
|
||||||
|
status: "forbidden",
|
||||||
|
error: `agentId is not allowed for sessions_spawn (allowed: ${allowedText})`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const childSessionKey = `agent:${targetAgentId}:subagent:${crypto.randomUUID()}`;
|
||||||
if (opts?.sandboxed === true) {
|
if (opts?.sandboxed === true) {
|
||||||
try {
|
try {
|
||||||
await callGateway({
|
await callGateway({
|
||||||
|
|||||||
@@ -19,7 +19,10 @@ import { buildWorkspaceSkillSnapshot } from "../agents/skills.js";
|
|||||||
import { resolveAgentTimeoutMs } from "../agents/timeout.js";
|
import { resolveAgentTimeoutMs } from "../agents/timeout.js";
|
||||||
import { hasNonzeroUsage } from "../agents/usage.js";
|
import { hasNonzeroUsage } from "../agents/usage.js";
|
||||||
import {
|
import {
|
||||||
DEFAULT_AGENT_WORKSPACE_DIR,
|
resolveAgentDir,
|
||||||
|
resolveAgentWorkspaceDir,
|
||||||
|
} from "../agents/agent-scope.js";
|
||||||
|
import {
|
||||||
ensureAgentWorkspace,
|
ensureAgentWorkspace,
|
||||||
} from "../agents/workspace.js";
|
} from "../agents/workspace.js";
|
||||||
import type { MsgContext } from "../auto-reply/templating.js";
|
import type { MsgContext } from "../auto-reply/templating.js";
|
||||||
@@ -180,7 +183,11 @@ export async function agentCommand(
|
|||||||
|
|
||||||
const cfg = loadConfig();
|
const cfg = loadConfig();
|
||||||
const agentCfg = cfg.agent;
|
const agentCfg = cfg.agent;
|
||||||
const workspaceDirRaw = cfg.agent?.workspace ?? DEFAULT_AGENT_WORKSPACE_DIR;
|
const sessionAgentId = resolveAgentIdFromSessionKey(
|
||||||
|
opts.sessionKey?.trim(),
|
||||||
|
);
|
||||||
|
const workspaceDirRaw = resolveAgentWorkspaceDir(cfg, sessionAgentId);
|
||||||
|
const agentDir = resolveAgentDir(cfg, sessionAgentId);
|
||||||
const workspace = await ensureAgentWorkspace({
|
const workspace = await ensureAgentWorkspace({
|
||||||
dir: workspaceDirRaw,
|
dir: workspaceDirRaw,
|
||||||
ensureBootstrapFiles: !cfg.agent?.skipBootstrap,
|
ensureBootstrapFiles: !cfg.agent?.skipBootstrap,
|
||||||
@@ -427,6 +434,7 @@ export async function agentCommand(
|
|||||||
lane: opts.lane,
|
lane: opts.lane,
|
||||||
abortSignal: opts.abortSignal,
|
abortSignal: opts.abortSignal,
|
||||||
extraSystemPrompt: opts.extraSystemPrompt,
|
extraSystemPrompt: opts.extraSystemPrompt,
|
||||||
|
agentDir,
|
||||||
onAgentEvent: (evt) => {
|
onAgentEvent: (evt) => {
|
||||||
if (
|
if (
|
||||||
evt.stream === "lifecycle" &&
|
evt.stream === "lifecycle" &&
|
||||||
|
|||||||
@@ -717,6 +717,10 @@ export type RoutingConfig = {
|
|||||||
workspace?: string;
|
workspace?: string;
|
||||||
agentDir?: string;
|
agentDir?: string;
|
||||||
model?: string;
|
model?: string;
|
||||||
|
subagents?: {
|
||||||
|
/** Allow spawning sub-agents under other agent ids. Use "*" to allow any. */
|
||||||
|
allowAgents?: string[];
|
||||||
|
};
|
||||||
sandbox?: {
|
sandbox?: {
|
||||||
mode?: "off" | "non-main" | "all";
|
mode?: "off" | "non-main" | "all";
|
||||||
/** Agent workspace access inside the sandbox. */
|
/** Agent workspace access inside the sandbox. */
|
||||||
|
|||||||
@@ -638,6 +638,11 @@ const RoutingSchema = z
|
|||||||
workspace: z.string().optional(),
|
workspace: z.string().optional(),
|
||||||
agentDir: z.string().optional(),
|
agentDir: z.string().optional(),
|
||||||
model: z.string().optional(),
|
model: z.string().optional(),
|
||||||
|
subagents: z
|
||||||
|
.object({
|
||||||
|
allowAgents: z.array(z.string()).optional(),
|
||||||
|
})
|
||||||
|
.optional(),
|
||||||
sandbox: z
|
sandbox: z
|
||||||
.object({
|
.object({
|
||||||
mode: z
|
mode: z
|
||||||
|
|||||||
Reference in New Issue
Block a user