Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions src/agent-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,8 @@ interface SpawnArgs {

export interface SpawnOptions {
description: string;
/** Caller-supplied session name; falls back to the agent config/type when omitted. */
sessionName?: string;
model?: Model<any>;
maxTurns?: number;
isolated?: boolean;
Expand Down Expand Up @@ -250,6 +252,7 @@ export class AgentManager {
const promise = runAgent(ctx, type, prompt, {
pi,
agentId: id,
sessionName: options.sessionName,
model: options.model,
maxTurns: options.maxTurns,
isolated: options.isolated,
Expand Down
7 changes: 6 additions & 1 deletion src/agent-runner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,8 @@ export interface RunOptions {
pi: ExtensionAPI;
/** Manager-assigned id; suffixes session name to disambiguate parallel spawns (e.g. `Explore#a1b2c3d4`). */
agentId?: string;
/** Caller-supplied session name; takes precedence over the agent config/type. */
sessionName?: string;
model?: Model<any>;
maxTurns?: number;
signal?: AbortSignal;
Expand Down Expand Up @@ -613,7 +615,10 @@ export async function runAgent(

const { session } = await runInChildSessionContext(() => createAgentSession(sessionOpts));

const baseSessionName = agentConfig?.name ?? type;
const suppliedSessionName = typeof options.sessionName === "string" && options.sessionName.trim()
? options.sessionName.trim()
: undefined;
const baseSessionName = suppliedSessionName ?? agentConfig?.name ?? type;
session.setSessionName(
options.agentId ? `${baseSessionName}#${options.agentId.slice(0, 8)}` : baseSessionName,
);
Expand Down
8 changes: 8 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -815,6 +815,11 @@ Terse command-style prompts produce shallow, generic work.
description: Type.String({
description: "A short (3-5 word) description of the task (shown in UI).",
}),
name: Type.Optional(
Type.String({
description: "Optional human-readable session name. The agent ID suffix is added automatically.",
}),
),
subagent_type: Type.String({
description: `The type of specialized agent to use. Available types: ${getAvailableTypes().join(", ")}. Custom agents from .pi/agents/*.md (project) or ${getAgentDir()}/agents/*.md (global) are also available.`,
}),
Expand Down Expand Up @@ -1071,6 +1076,7 @@ Terse command-style prompts produce shallow, generic work.
const job = scheduler.addJob({
name: params.description as string,
description: params.description as string,
sessionName: params.name as string | undefined,
schedule: params.schedule as string,
subagent_type: subagentType,
prompt: params.prompt as string,
Expand Down Expand Up @@ -1130,6 +1136,7 @@ Terse command-style prompts produce shallow, generic work.
try {
id = manager.spawn(pi, ctx, subagentType, params.prompt, {
description: params.description,
sessionName: params.name,
model,
maxTurns: effectiveMaxTurns,
isolated,
Expand Down Expand Up @@ -1253,6 +1260,7 @@ Terse command-style prompts produce shallow, generic work.
try {
const fgResult = await manager.spawnAndWait(pi, ctx, subagentType, params.prompt, {
description: params.description,
sessionName: params.name,
model,
maxTurns: effectiveMaxTurns,
isolated,
Expand Down
3 changes: 3 additions & 0 deletions src/nested-tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ const NESTED_TOOL_NAMES = ["Agent", "get_subagent_result", "steer_subagent"] as

interface NestedSpawnOptions {
description: string;
sessionName?: string;
model?: Model<any>;
maxTurns?: number;
isolated?: boolean;
Expand Down Expand Up @@ -92,6 +93,7 @@ export function createNestedSubagentTools(context: NestedToolContext): ToolDefin
parameters: Type.Object({
prompt: Type.String({ description: "Self-contained task for the nested agent." }),
description: Type.String({ description: "Short 3-5 word task description." }),
name: Type.Optional(Type.String({ description: "Optional human-readable session name. The agent ID suffix is added automatically." })),
subagent_type: Type.String({ description: `Allowed nested agent type. Available: ${available().join(", ") || "none"}.` }),
model: Type.Optional(Type.String({ description: "Optional provider/model override." })),
thinking: Type.Optional(Type.String({ description: "Optional thinking level." })),
Expand Down Expand Up @@ -153,6 +155,7 @@ export function createNestedSubagentTools(context: NestedToolContext): ToolDefin
);
const options: NestedSpawnOptions = {
description: params.description,
sessionName: params.name,
model,
maxTurns: invocation.maxTurns,
isolated: invocation.isolated,
Expand Down
3 changes: 3 additions & 0 deletions src/schedule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ export type ScheduleChangeEvent =
export interface NewJobInput {
name: string;
description: string;
sessionName?: string;
schedule: string;
subagent_type: SubagentType;
prompt: string;
Expand Down Expand Up @@ -96,6 +97,7 @@ export class SubagentScheduler {
id: nanoid(10),
name: input.name,
description: input.description,
sessionName: input.sessionName,
schedule: detected.normalized,
scheduleType: detected.type,
intervalMs: detected.intervalMs,
Expand Down Expand Up @@ -240,6 +242,7 @@ export class SubagentScheduler {
try {
agentId = manager.spawn(pi, ctx, job.subagent_type, job.prompt, {
description: job.description,
sessionName: job.sessionName,
isBackground: true,
bypassQueue: true,
model: resolvedModel,
Expand Down
2 changes: 2 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,8 @@ export interface ScheduledSubagent {
intervalMs?: number;

// spawn params (subset of Agent tool params; no inherit_context, no resume)
/** Optional caller-supplied session name. */
sessionName?: string;
subagent_type: SubagentType;
prompt: string;
model?: string;
Expand Down
18 changes: 18 additions & 0 deletions test/agent-manager.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -647,6 +647,24 @@ describe("AgentManager — SpawnOptions.cwd passthrough (#96)", () => {
});
});

describe("AgentManager — session name passthrough", () => {
let manager: AgentManager;
afterEach(() => manager?.dispose());

it("passes SpawnOptions.sessionName to runAgent", async () => {
vi.mocked(runAgent).mockClear();
resolvedRun();
manager = new AgentManager();
const id = manager.spawn(mockPi, mockCtx, "general-purpose", "test", {
description: "test",
sessionName: "CH-005 verify",
});
await manager.getRecord(id)!.promise;

expect(vi.mocked(runAgent).mock.lastCall![3].sessionName).toBe("CH-005 verify");
});
});

describe("AgentManager — abort() state machine", () => {
let manager: AgentManager;
afterEach(() => manager?.dispose());
Expand Down
26 changes: 26 additions & 0 deletions test/agent-runner.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,32 @@ describe("agent-runner final output capture", () => {

expect(session.setSessionName).toHaveBeenCalledWith("Explore#a1b2c3d4");
});

it("prefers a caller-supplied session name and preserves the short agentId suffix", async () => {
const { session } = createSession("NAMED");
createAgentSession.mockResolvedValue({ session });

await runAgent(ctx, "Explore", "go", {
pi,
agentId: "a1b2c3d4e5f6",
sessionName: " CH-005 plan ",
});

expect(session.setSessionName).toHaveBeenCalledWith("CH-005 plan#a1b2c3d4");
});

it("falls back to the configured name when a caller supplies only whitespace", async () => {
const { session } = createSession("NAMED");
createAgentSession.mockResolvedValue({ session });

await runAgent(ctx, "Explore", "go", {
pi,
agentId: "a1b2c3d4e5f6",
sessionName: " ",
});

expect(session.setSessionName).toHaveBeenCalledWith("Explore#a1b2c3d4");
});
});

// ─── message_end → onAssistantUsage wiring (issue #38) ─────────────────
Expand Down
8 changes: 7 additions & 1 deletion test/nested-tools.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,13 +74,19 @@ describe("child-safe nested Agent tool", () => {
const result = await execute(agent, {
subagent_type: "reviewer",
description: "review evidence",
name: "CH-005 evidence review",
prompt: "Review it",
});

expect(result.isError).toBe(false);
expect(spawnAndWait).toHaveBeenCalledWith(
expect.anything(), expect.anything(), "reviewer", "Review it",
expect.objectContaining({ depth: 2, parentAgentId: "parent-1", maxSubagentDepth: 2 }),
expect.objectContaining({
depth: 2,
parentAgentId: "parent-1",
maxSubagentDepth: 2,
sessionName: "CH-005 evidence review",
}),
);
});

Expand Down
11 changes: 11 additions & 0 deletions test/schedule.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -294,6 +294,17 @@ describe("SubagentScheduler — fire path", () => {
expect(optsArg.isBackground).toBe(true);
});

it("preserves a caller-supplied session name when a scheduled job fires", () => {
scheduler.addJob({
name: "named-job", description: "x", sessionName: "GH-123 scheduled review", schedule: "1s",
subagent_type: "general-purpose", prompt: "x",
});

vi.advanceTimersByTime(1_000);
const optsArg = manager.spawn.mock.calls[0][4];
expect(optsArg.sessionName).toBe("GH-123 scheduled review");
});

it("disabled jobs do not fire", () => {
const job = scheduler.addJob({
name: "off", description: "x", schedule: "1s",
Expand Down