Skip to content
Draft
Show file tree
Hide file tree
Changes from all 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
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,14 @@ See [GitHub 发布](https://github.com/github/copilot-sdk/releases) for the fu

## [Unreleased]

### Feature: model policy for built-in sub-agents (`subagentModel`)

Session create and resume accept a new optional `subagentModel` option that configures which model the runtime's built-in `task` sub-agents (e.g. `explore`, `general-purpose`) use, addressing [github/copilot-sdk#1640](https://github.com/github/copilot-sdk/issues/1640). It is built entirely on the CLI's existing (experimental) subagent settings mechanism (`session.tools.updateSubagent设置` / `Subagent设置Entry`): the SDK issues that call automatically right after the session is created, for the agent types you name.

Two policy modes are available: `{ mode: "inherit-parent", agentTypes }` applies the session's own `model` to the listed built-in agent types (requires `model` to be set), and `{ mode: "fixed", model, agentTypes }` applies an explicit model. Because the runtime's `agent_type` values are open-ended and not enumerated by the wire protocol, there is no wildcard for "all built-ins" — name the agent types you want affected. Omitting `subagentModel` entirely makes no settings call and preserves the runtime's existing model-selection behavior for built-in sub-agents exactly as before.

This does not change custom agents, which already accept their own `model` via `CustomAgentConfig`. Currently implemented for the Node.js SDK; the underlying `updateSubagent设置` RPC is already generated for every SDK, so the same convenience wrapper can be ported to the other language bindings following this pattern.

### Feature: rotating session-scoped GitHub credentials

All six SDKs can now acquire short-lived GitHub credentials through a session-scoped callback. The SDK registers the callback before session create or resume, maps `initial` and `refresh` requests to the owning session, and removes registrations on rollback, replacement, session close, and client close. Static per-session `gitHubToken` credentials remain supported and are mutually exclusive with the callback.
Expand Down
28 changes: 28 additions & 0 deletions docs/features/custom-agents.md
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,34 @@ In addition to per-agent configuration above, you can set `agent` on the **sessi
| Session Config Property | Type | Description |
|-------------------------|------|-------------|
| `agent` | `string` | Name of the custom agent to pre-select at session creation. Must match a `name` in `customAgents`. |
| `subagentModel` | `SubagentModelPolicy` | Model policy for the runtime's **built-in** sub-agents (e.g. `explore`, `general-purpose`). See [Model policy for built-in sub-agents](#model-policy-for-built-in-sub-agents) below. |

## Model policy for built-in sub-agents

`model` and `reasoningEffort` on a `customAgents` entry only affect agents *you* define. The runtime's own **built-in** sub-agents — `explore`, `general-purpose`, and any others spawned by the built-in `task` tool — historically had no supported way to be pointed at the parent session's model or an explicit model (see [github/copilot-sdk#1640](https://github.com/github/copilot-sdk/issues/1640)).

`subagentModel` closes that gap for the Node.js SDK by wrapping the CLI's existing (experimental) subagent settings mechanism (`session.tools.updateSubagent设置`). The SDK calls it automatically right after the session is created:

<!-- docs-validate: skip -->
```typescript
const session = await client.createSession({
model: "claude-sonnet-4.5",
subagentModel: {
mode: "inherit-parent",
agentTypes: ["explore", "general-purpose"],
},
onPermissionRequest: async () => ({ kind: "approve-once" }),
});
```

- `{ mode: "inherit-parent", agentTypes }` — use this session's `model` for the listed built-in agent types. Throws if the session's `model` is not set.
- `{ mode: "fixed", model, agentTypes }` — use an explicit model (and optional `reasoningEffort`) for the listed built-in agent types.
- Omit `subagentModel` entirely to preserve the runtime's existing model-selection behavior for built-in sub-agents, unchanged.
Comment on lines +289 to +291

There is intentionally no wildcard for "all built-in sub-agents": the runtime's `agent_type` values are open-ended strings that aren't enumerated by the wire protocol, so name the agent types you want to affect. This option does not affect custom agents (use their own `model`/`reasoningEffort` fields instead) and does not change the parent session's model.

> [!NOTE]
> This SDK-side wrapper is currently available for the Node.js SDK only. The underlying `session.tools.updateSubagent设置` RPC is generated for every language binding, so the same convenience wrapper can be added to the other SDKs following this pattern; see the CHANGELOG for details.

## Per-agent skills

Expand Down
54 changes: 54 additions & 0 deletions nodejs/src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1512,6 +1512,58 @@ export class CopilotClient {
}
}

/**
* Applies {@link SessionConfigBase.subagentModel}, if configured, by
* translating it into a call to the runtime's existing (experimental)
* `session.tools.updateSubagent设置` RPC. No call is made when the
* policy is omitted, preserving the runtime's default behavior exactly.
*/
private async applySubagentModelPolicy(
session: CopilotSession,
config: SessionConfigBase
): Promise<void> {
const policy = config.subagentModel;
if (policy === undefined) {
return;
}
if (policy.agentTypes.length === 0) {
throw new Error("subagentModel.agentTypes must include at least one agent type");
Comment on lines +1529 to +1530
}

let model: string;
if (policy.mode === "inherit-parent") {
if (!config.model) {
throw new Error(
'subagentModel: mode "inherit-parent" requires config.model to be set'
);
}
model = config.model;
} else {
model = policy.model;
}

const agents: Record<string, { model: string; effortLevel?: string }> = {};
for (const agentType of policy.agentTypes) {
agents[agentType] = {
model,
...(policy.reasoningEffort !== undefined
? { effortLevel: policy.reasoningEffort }
: {}),
};
}

try {
await session.rpc.tools.updateSubagent设置({ subagents: { agents } });
} catch (e) {
try {
await session.disconnect();
} catch {
// Swallow: original error is the one the caller needs.
}
throw e;
}
}

async createSession(config: SessionConfig): Promise<CopilotSession> {
if (config.gitHubToken !== undefined && config.gitHubTokenProvider !== undefined) {
throw new Error("gitHubToken and gitHubTokenProvider are mutually exclusive");
Expand Down Expand Up @@ -1759,6 +1811,7 @@ export class CopilotClient {
session.setCapabilities(capabilities);

await this.updateSessionOptionsForMode(session, config);
await this.applySubagentModelPolicy(session, config);
this.commitGitHubTokenProvider(returnedSessionId, gitHubTokenProviderRegistrationId);
} catch (e) {
if (registeredId !== undefined) {
Expand Down Expand Up @@ -2035,6 +2088,7 @@ export class CopilotClient {
}

await this.updateSessionOptionsForMode(session, config);
await this.applySubagentModelPolicy(session, config);
this.commitGitHubTokenProvider(sessionId, gitHubTokenProviderRegistrationId);
} catch (e) {
this.sessions.delete(sessionId);
Expand Down
52 changes: 52 additions & 0 deletions nodejs/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1886,6 +1886,35 @@ export interface DefaultAgentConfig {
excludedTools?: string[];
}

/**
* Model policy for the runtime's built-in sub-agents (spawned by the
* built-in `task` tool, e.g. `explore`, `general-purpose`).
*
* - `{ mode: "inherit-parent" }`: use this session's {@link SessionConfigBase.model}
* for the listed {@link agentTypes}. Requires `model` to be set on the session;
* throws if it is omitted.
* - `{ mode: "fixed", model }`: use an explicit model for the listed {@link agentTypes}.
* - Omitted entirely (the default): preserve the runtime's existing
* model-selection behavior for built-in sub-agents.
*/
export type SubagentModelPolicy =
| {
mode: "inherit-parent";
/** Built-in sub-agent types to apply this policy to (e.g. `["explore", "general-purpose"]`). */
agentTypes: string[];
/** Reasoning effort override forwarded alongside the inherited model, when supported. */
reasoningEffort?: ReasoningEffort;
}
| {
mode: "fixed";
/** Model identifier to use for the listed built-in sub-agent types. */
model: string;
/** Built-in sub-agent types to apply this policy to (e.g. `["explore", "general-purpose"]`). */
agentTypes: string[];
/** Reasoning effort override for the fixed model, when supported. */
reasoningEffort?: ReasoningEffort;
};

/**
* Configuration for infinite sessions with automatic context compaction and workspace persistence.
* When enabled, sessions automatically manage context window limits through background compaction
Expand Down Expand Up @@ -2425,6 +2454,29 @@ export interface SessionConfigBase {
*/
excludedBuiltinAgents?: string[];

/**
* Model policy applied to built-in sub-agents (e.g. `explore`,
* `general-purpose`) that the runtime's built-in `task` tool spawns.
*
* This does not affect custom agents, which already accept their own
* {@link CustomAgentConfig.model}. It also does not change the parent
* session's model.
*
* Implemented on top of the runtime's existing (experimental) subagent
* settings mechanism: after the session is created, the SDK calls
* `session.rpc.tools.updateSubagent设置` for each agent type listed
* in {@link SubagentModelPolicy.agentTypes}. There is currently no
* wildcard to target "all built-in sub-agents" — the runtime's
* `agent_type` values are open-ended and not enumerated by the protocol,
* so callers must name the built-in agent types they want to affect
* (for example `["explore", "general-purpose"]`).
*
* When omitted, no settings call is made and the runtime's existing
* model-selection behavior for built-in sub-agents is preserved exactly
* as before.
*/
subagentModel?: SubagentModelPolicy;

/**
* Built-in skill names to include in the session. In `mode: "empty"`,
* omitting this option excludes all runtime-bundled skills; specifying names
Expand Down
121 changes: 121 additions & 0 deletions nodejs/test/client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -439,6 +439,127 @@ describe("CopilotClient", () => {
);
});

it("applies inherit-parent subagentModel by forwarding the session model to updateSubagent设置", async () => {
const client = new CopilotClient();
await client.start();
onTestFinished(() => stopClient(client));

const spy = vi
.spyOn((client as any).connection!, "sendRequest")
.mockImplementation(async (method: string, params: any) => {
if (method === "session.create") {
return { sessionId: params.sessionId, workspacePath: "/workspace" };
}
if (method === "session.tools.updateSubagent设置") {
return {};
}
throw new Error(`Unexpected method: ${method}`);
});

await client.createSession({
sessionId: "create-with-subagent-model-policy",
model: "claude-sonnet-4.5",
subagentModel: { mode: "inherit-parent", agentTypes: ["explore", "general-purpose"] },
onPermissionRequest: approveAll,
});

expect(spy).toHaveBeenCalledWith("session.tools.updateSubagent设置", {
sessionId: "create-with-subagent-model-policy",
subagents: {
agents: {
explore: { model: "claude-sonnet-4.5" },
"general-purpose": { model: "claude-sonnet-4.5" },
},
},
});
});

it("applies fixed subagentModel with an explicit model and reasoning effort", async () => {
const client = new CopilotClient();
await client.start();
onTestFinished(() => stopClient(client));

const spy = vi
.spyOn((client as any).connection!, "sendRequest")
.mockImplementation(async (method: string, params: any) => {
if (method === "session.create") {
return { sessionId: params.sessionId, workspacePath: "/workspace" };
}
if (method === "session.tools.updateSubagent设置") {
return {};
}
throw new Error(`Unexpected method: ${method}`);
});

await client.createSession({
sessionId: "create-with-fixed-subagent-model-policy",
subagentModel: {
mode: "fixed",
model: "claude-haiku-4.5",
agentTypes: ["explore"],
reasoningEffort: "low",
},
onPermissionRequest: approveAll,
});

expect(spy).toHaveBeenCalledWith("session.tools.updateSubagent设置", {
sessionId: "create-with-fixed-subagent-model-policy",
subagents: {
agents: {
explore: { model: "claude-haiku-4.5", effortLevel: "low" },
},
},
});
});

it("omits any updateSubagent设置 call when subagentModel is not configured", async () => {
const client = new CopilotClient();
await client.start();
onTestFinished(() => stopClient(client));

const spy = vi
.spyOn((client as any).connection!, "sendRequest")
.mockImplementation(async (method: string, params: any) => {
if (method === "session.create") {
return { sessionId: params.sessionId, workspacePath: "/workspace" };
}
throw new Error(`Unexpected method: ${method}`);
});

await client.createSession({
sessionId: "create-without-subagent-model-policy",
onPermissionRequest: approveAll,
});

expect(spy).not.toHaveBeenCalledWith(
"session.tools.updateSubagent设置",
expect.anything()
);
});

it("rejects inherit-parent subagentModel when the session model is not set", async () => {
const client = new CopilotClient();
await client.start();
onTestFinished(() => stopClient(client));

vi.spyOn((client as any).connection!, "sendRequest").mockImplementation(
async (method: string, params: any) => {
if (method === "session.create") {
return { sessionId: params.sessionId, workspacePath: "/workspace" };
}
throw new Error(`Unexpected method: ${method}`);
}
);

await expect(
client.createSession({
sessionId: "create-with-invalid-subagent-model-policy",
subagentModel: { mode: "inherit-parent", agentTypes: ["explore"] },
onPermissionRequest: approveAll,
})
).rejects.toThrow(/inherit-parent.*requires config\.model/);
});

it("registers MCP OAuth interest after cloud create only when an auth handler is configured", async () => {
const client = new CopilotClient();
await client.start();
Expand Down
28 changes: 28 additions & 0 deletions nodejs/test/e2e/session_config.e2e.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -549,6 +549,34 @@ describe("Session Configuration", async () => {
await session1.disconnect();
});

it("should apply an inherit-parent subagentModel policy on create without error", async () => {
const session = await client.createSession({
onPermissionRequest: approveAll,
model: "claude-sonnet-4.5",
subagentModel: {
mode: "inherit-parent",
agentTypes: ["explore", "general-purpose"],
},
});

await session.disconnect();
});

it("should apply a fixed subagentModel policy on resume without error", async () => {
const session1 = await client.createSession({ onPermissionRequest: approveAll });
const session2 = await client.resumeSession(session1.sessionId, {
onPermissionRequest: approveAll,
subagentModel: {
mode: "fixed",
model: "claude-haiku-4.5",
agentTypes: ["explore"],
},
});

await session2.disconnect();
await session1.disconnect();
});

it("should enable citations for Anthropic file attachments on create", async () => {
const handler = new RecordingRequestHandler();
const citationClient = new CopilotClient({
Expand Down