@github/copilot-sdk 0.1.32 → 0.1.33-preview.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/session.d.ts CHANGED
@@ -2,9 +2,10 @@
2
2
  * Copilot Session - represents a single conversation session with the Copilot CLI.
3
3
  * @module session
4
4
  */
5
- import type { MessageConnection } from "vscode-jsonrpc/node";
5
+ import type { MessageConnection } from "vscode-jsonrpc/node.js";
6
6
  import { createSessionRpc } from "./generated/rpc.js";
7
7
  import type { MessageOptions, PermissionHandler, PermissionRequestResult, SessionEvent, SessionEventHandler, SessionEventType, SessionHooks, Tool, ToolHandler, TypedSessionEventHandler, UserInputHandler, UserInputResponse } from "./types.js";
8
+ export declare const NO_RESULT_PERMISSION_V2_ERROR = "Permission handlers cannot return 'no-result' when connected to a protocol v2 server.";
8
9
  /** Assistant message event - the final response from the assistant. */
9
10
  export type AssistantMessageEvent = Extract<SessionEvent, {
10
11
  type: "assistant.message";
@@ -37,7 +38,7 @@ export type AssistantMessageEvent = Extract<SessionEvent, {
37
38
  export declare class CopilotSession {
38
39
  readonly sessionId: string;
39
40
  private connection;
40
- private readonly _workspacePath?;
41
+ private _workspacePath?;
41
42
  private eventHandlers;
42
43
  private typedEventHandlers;
43
44
  private toolHandlers;
@@ -339,4 +340,24 @@ export declare class CopilotSession {
339
340
  * ```
340
341
  */
341
342
  setModel(model: string): Promise<void>;
343
+ /**
344
+ * Log a message to the session timeline.
345
+ * The message appears in the session event stream and is visible to SDK consumers
346
+ * and (for non-ephemeral messages) persisted to the session event log on disk.
347
+ *
348
+ * @param message - Human-readable message text
349
+ * @param options - Optional log level and ephemeral flag
350
+ *
351
+ * @example
352
+ * ```typescript
353
+ * await session.log("Processing started");
354
+ * await session.log("Disk usage high", { level: "warning" });
355
+ * await session.log("Connection failed", { level: "error" });
356
+ * await session.log("Debug info", { ephemeral: true });
357
+ * ```
358
+ */
359
+ log(message: string, options?: {
360
+ level?: "info" | "warning" | "error";
361
+ ephemeral?: boolean;
362
+ }): Promise<void>;
342
363
  }
package/dist/session.js CHANGED
@@ -1,5 +1,6 @@
1
- import { ConnectionError, ResponseError } from "vscode-jsonrpc/node";
1
+ import { ConnectionError, ResponseError } from "vscode-jsonrpc/node.js";
2
2
  import { createSessionRpc } from "./generated/rpc.js";
3
+ const NO_RESULT_PERMISSION_V2_ERROR = "Permission handlers cannot return 'no-result' when connected to a protocol v2 server.";
3
4
  class CopilotSession {
4
5
  /**
5
6
  * Creates a new CopilotSession instance.
@@ -239,6 +240,9 @@ class CopilotSession {
239
240
  const result = await this.permissionHandler(permissionRequest, {
240
241
  sessionId: this.sessionId
241
242
  });
243
+ if (result.kind === "no-result") {
244
+ return;
245
+ }
242
246
  await this.rpc.permissions.handlePendingPermissionRequest({ requestId, result });
243
247
  } catch (_error) {
244
248
  try {
@@ -335,8 +339,14 @@ class CopilotSession {
335
339
  const result = await this.permissionHandler(request, {
336
340
  sessionId: this.sessionId
337
341
  });
342
+ if (result.kind === "no-result") {
343
+ throw new Error(NO_RESULT_PERMISSION_V2_ERROR);
344
+ }
338
345
  return result;
339
- } catch (_error) {
346
+ } catch (error) {
347
+ if (error instanceof Error && error.message === NO_RESULT_PERMISSION_V2_ERROR) {
348
+ throw error;
349
+ }
340
350
  return { kind: "denied-no-approval-rule-and-could-not-request-from-user" };
341
351
  }
342
352
  }
@@ -501,7 +511,27 @@ class CopilotSession {
501
511
  async setModel(model) {
502
512
  await this.rpc.model.switchTo({ modelId: model });
503
513
  }
514
+ /**
515
+ * Log a message to the session timeline.
516
+ * The message appears in the session event stream and is visible to SDK consumers
517
+ * and (for non-ephemeral messages) persisted to the session event log on disk.
518
+ *
519
+ * @param message - Human-readable message text
520
+ * @param options - Optional log level and ephemeral flag
521
+ *
522
+ * @example
523
+ * ```typescript
524
+ * await session.log("Processing started");
525
+ * await session.log("Disk usage high", { level: "warning" });
526
+ * await session.log("Connection failed", { level: "error" });
527
+ * await session.log("Debug info", { ephemeral: true });
528
+ * ```
529
+ */
530
+ async log(message, options) {
531
+ await this.rpc.log({ message, ...options });
532
+ }
504
533
  }
505
534
  export {
506
- CopilotSession
535
+ CopilotSession,
536
+ NO_RESULT_PERMISSION_V2_ERROR
507
537
  };
package/dist/types.d.ts CHANGED
@@ -56,8 +56,7 @@ export interface CopilotClientOptions {
56
56
  */
57
57
  autoStart?: boolean;
58
58
  /**
59
- * Auto-restart the CLI server if it crashes
60
- * @default true
59
+ * @deprecated This option has no effect and will be removed in a future release.
61
60
  */
62
61
  autoRestart?: boolean;
63
62
  /**
@@ -77,6 +76,13 @@ export interface CopilotClientOptions {
77
76
  * @default true (but defaults to false when githubToken is provided)
78
77
  */
79
78
  useLoggedInUser?: boolean;
79
+ /**
80
+ * Custom handler for listing available models.
81
+ * When provided, client.listModels() calls this handler instead of
82
+ * querying the CLI server. Useful in BYOK mode to return models
83
+ * available from your custom provider.
84
+ */
85
+ onListModels?: () => Promise<ModelInfo[]> | ModelInfo[];
80
86
  }
81
87
  /**
82
88
  * Configuration for creating a session
@@ -129,6 +135,10 @@ export interface Tool<TArgs = unknown> {
129
135
  * will return an error.
130
136
  */
131
137
  overridesBuiltInTool?: boolean;
138
+ /**
139
+ * When true, the tool can execute without a permission prompt.
140
+ */
141
+ skipPermission?: boolean;
132
142
  }
133
143
  /**
134
144
  * Helper to define a tool with Zod schema and get type inference for the handler.
@@ -139,6 +149,7 @@ export declare function defineTool<T = unknown>(name: string, config: {
139
149
  parameters?: ZodSchema<T> | Record<string, unknown>;
140
150
  handler: ToolHandler<T>;
141
151
  overridesBuiltInTool?: boolean;
152
+ skipPermission?: boolean;
142
153
  }): Tool<T>;
143
154
  export interface ToolCallRequestPayload {
144
155
  sessionId: string;
@@ -186,7 +197,9 @@ export interface PermissionRequest {
186
197
  [key: string]: unknown;
187
198
  }
188
199
  import type { SessionPermissionsHandlePendingPermissionRequestParams } from "./generated/rpc.js";
189
- export type PermissionRequestResult = SessionPermissionsHandlePendingPermissionRequestParams["result"];
200
+ export type PermissionRequestResult = SessionPermissionsHandlePendingPermissionRequestParams["result"] | {
201
+ kind: "no-result";
202
+ };
190
203
  export type PermissionHandler = (request: PermissionRequest, invocation: {
191
204
  sessionId: string;
192
205
  }) => Promise<PermissionRequestResult> | PermissionRequestResult;
@@ -586,6 +599,12 @@ export interface SessionConfig {
586
599
  * Custom agent configurations for the session.
587
600
  */
588
601
  customAgents?: CustomAgentConfig[];
602
+ /**
603
+ * Name of the custom agent to activate when the session starts.
604
+ * Must match the `name` of one of the agents in `customAgents`.
605
+ * Equivalent to calling `session.rpc.agent.select({ name })` after creation.
606
+ */
607
+ agent?: string;
589
608
  /**
590
609
  * Directories to load skills from.
591
610
  */
@@ -600,11 +619,21 @@ export interface SessionConfig {
600
619
  * Set to `{ enabled: false }` to disable.
601
620
  */
602
621
  infiniteSessions?: InfiniteSessionConfig;
622
+ /**
623
+ * Optional event handler that is registered on the session before the
624
+ * session.create RPC is issued. This guarantees that early events emitted
625
+ * by the CLI during session creation (e.g. session.start) are delivered to
626
+ * the handler.
627
+ *
628
+ * Equivalent to calling `session.on(handler)` immediately after creation,
629
+ * but executes earlier in the lifecycle so no events are missed.
630
+ */
631
+ onEvent?: SessionEventHandler;
603
632
  }
604
633
  /**
605
634
  * Configuration for resuming a session
606
635
  */
607
- export type ResumeSessionConfig = Pick<SessionConfig, "clientName" | "model" | "tools" | "systemMessage" | "availableTools" | "excludedTools" | "provider" | "streaming" | "reasoningEffort" | "onPermissionRequest" | "onUserInputRequest" | "hooks" | "workingDirectory" | "configDir" | "mcpServers" | "customAgents" | "skillDirectories" | "disabledSkills" | "infiniteSessions"> & {
636
+ export type ResumeSessionConfig = Pick<SessionConfig, "clientName" | "model" | "tools" | "systemMessage" | "availableTools" | "excludedTools" | "provider" | "streaming" | "reasoningEffort" | "onPermissionRequest" | "onUserInputRequest" | "hooks" | "workingDirectory" | "configDir" | "mcpServers" | "customAgents" | "agent" | "skillDirectories" | "disabledSkills" | "infiniteSessions" | "onEvent"> & {
608
637
  /**
609
638
  * When true, skips emitting the session.resume event.
610
639
  * Useful for reconnecting to a session without triggering resume-related side effects.
@@ -0,0 +1,263 @@
1
+ # Agent Extension Authoring Guide
2
+
3
+ A precise, step-by-step reference for agents writing Copilot CLI extensions programmatically.
4
+
5
+ ## Workflow
6
+
7
+ ### Step 1: Scaffold the extension
8
+
9
+ Use the `extensions_manage` tool with `operation: "scaffold"`:
10
+
11
+ ```
12
+ extensions_manage({ operation: "scaffold", name: "my-extension" })
13
+ ```
14
+
15
+ This creates `.github/extensions/my-extension/extension.mjs` with a working skeleton.
16
+ For user-scoped extensions (persist across all repos), add `location: "user"`.
17
+
18
+ ### Step 2: Edit the extension file
19
+
20
+ Modify the generated `extension.mjs` using `edit` or `create` tools. The file must:
21
+ - Be named `extension.mjs` (only `.mjs` is supported)
22
+ - Use ES module syntax (`import`/`export`)
23
+ - Call `joinSession({ ... })`
24
+
25
+ ### Step 3: Reload extensions
26
+
27
+ ```
28
+ extensions_reload({})
29
+ ```
30
+
31
+ This stops all running extensions and re-discovers/re-launches them. New tools are available immediately in the same turn (mid-turn refresh).
32
+
33
+ ### Step 4: Verify
34
+
35
+ ```
36
+ extensions_manage({ operation: "list" })
37
+ extensions_manage({ operation: "inspect", name: "my-extension" })
38
+ ```
39
+
40
+ Check that the extension loaded successfully and isn't marked as "failed".
41
+
42
+ ---
43
+
44
+ ## File Structure
45
+
46
+ ```
47
+ .github/extensions/<name>/extension.mjs
48
+ ```
49
+
50
+ Discovery rules:
51
+ - The CLI scans `.github/extensions/` relative to the git root
52
+ - It also scans the user's copilot config extensions directory
53
+ - Only immediate subdirectories are checked (not recursive)
54
+ - Each subdirectory must contain a file named `extension.mjs`
55
+ - Project extensions shadow user extensions on name collision
56
+
57
+ ---
58
+
59
+ ## Minimal Skeleton
60
+
61
+ ```js
62
+ import { joinSession } from "@github/copilot-sdk/extension";
63
+
64
+ await joinSession({
65
+ tools: [], // Optional — custom tools
66
+ hooks: {}, // Optional — lifecycle hooks
67
+ });
68
+ ```
69
+
70
+ ---
71
+
72
+ ## Registering Tools
73
+
74
+ ```js
75
+ tools: [
76
+ {
77
+ name: "tool_name", // Required. Must be globally unique across all extensions.
78
+ description: "What it does", // Required. Shown to the agent in tool descriptions.
79
+ parameters: { // Optional. JSON Schema for the arguments.
80
+ type: "object",
81
+ properties: {
82
+ arg1: { type: "string", description: "..." },
83
+ },
84
+ required: ["arg1"],
85
+ },
86
+ handler: async (args, invocation) => {
87
+ // args: parsed arguments matching the schema
88
+ // invocation.sessionId: current session ID
89
+ // invocation.toolCallId: unique call ID
90
+ // invocation.toolName: this tool's name
91
+ //
92
+ // Return value: string or ToolResultObject
93
+ // string → treated as success
94
+ // { textResultForLlm, resultType } → structured result
95
+ // resultType: "success" | "failure" | "rejected" | "denied"
96
+ return `Result: ${args.arg1}`;
97
+ },
98
+ },
99
+ ]
100
+ ```
101
+
102
+ **Constraints:**
103
+ - Tool names must be unique across ALL loaded extensions. Collisions cause the second extension to fail to load.
104
+ - Handler must return a string or `{ textResultForLlm: string, resultType?: string }`.
105
+ - Handler receives `(args, invocation)` — the second argument has `sessionId`, `toolCallId`, `toolName`.
106
+ - Use `session.log()` to surface messages to the user. Don't use `console.log()` (stdout is reserved for JSON-RPC).
107
+
108
+ ---
109
+
110
+ ## Registering Hooks
111
+
112
+ ```js
113
+ hooks: {
114
+ onUserPromptSubmitted: async (input, invocation) => { ... },
115
+ onPreToolUse: async (input, invocation) => { ... },
116
+ onPostToolUse: async (input, invocation) => { ... },
117
+ onSessionStart: async (input, invocation) => { ... },
118
+ onSessionEnd: async (input, invocation) => { ... },
119
+ onErrorOccurred: async (input, invocation) => { ... },
120
+ }
121
+ ```
122
+
123
+ All hook inputs include `timestamp` (unix ms) and `cwd` (working directory).
124
+ All handlers receive `invocation: { sessionId: string }` as the second argument.
125
+ All handlers may return `void`/`undefined` (no-op) or an output object.
126
+
127
+ ### onUserPromptSubmitted
128
+
129
+ **Input:** `{ prompt: string, timestamp, cwd }`
130
+
131
+ **Output (all fields optional):**
132
+ | Field | Type | Effect |
133
+ |-------|------|--------|
134
+ | `modifiedPrompt` | `string` | Replaces the user's prompt |
135
+ | `additionalContext` | `string` | Appended as hidden context the agent sees |
136
+
137
+ ### onPreToolUse
138
+
139
+ **Input:** `{ toolName: string, toolArgs: unknown, timestamp, cwd }`
140
+
141
+ **Output (all fields optional):**
142
+ | Field | Type | Effect |
143
+ |-------|------|--------|
144
+ | `permissionDecision` | `"allow" \| "deny" \| "ask"` | Override the permission check |
145
+ | `permissionDecisionReason` | `string` | Shown to user if denied |
146
+ | `modifiedArgs` | `unknown` | Replaces the tool arguments |
147
+ | `additionalContext` | `string` | Injected into the conversation |
148
+
149
+ ### onPostToolUse
150
+
151
+ **Input:** `{ toolName: string, toolArgs: unknown, toolResult: ToolResultObject, timestamp, cwd }`
152
+
153
+ **Output (all fields optional):**
154
+ | Field | Type | Effect |
155
+ |-------|------|--------|
156
+ | `modifiedResult` | `ToolResultObject` | Replaces the tool result |
157
+ | `additionalContext` | `string` | Injected into the conversation |
158
+
159
+ ### onSessionStart
160
+
161
+ **Input:** `{ source: "startup" \| "resume" \| "new", initialPrompt?: string, timestamp, cwd }`
162
+
163
+ **Output (all fields optional):**
164
+ | Field | Type | Effect |
165
+ |-------|------|--------|
166
+ | `additionalContext` | `string` | Injected as initial context |
167
+
168
+ ### onSessionEnd
169
+
170
+ **Input:** `{ reason: "complete" \| "error" \| "abort" \| "timeout" \| "user_exit", finalMessage?: string, error?: string, timestamp, cwd }`
171
+
172
+ **Output (all fields optional):**
173
+ | Field | Type | Effect |
174
+ |-------|------|--------|
175
+ | `sessionSummary` | `string` | Summary for session persistence |
176
+ | `cleanupActions` | `string[]` | Cleanup descriptions |
177
+
178
+ ### onErrorOccurred
179
+
180
+ **Input:** `{ error: string, errorContext: "model_call" \| "tool_execution" \| "system" \| "user_input", recoverable: boolean, timestamp, cwd }`
181
+
182
+ **Output (all fields optional):**
183
+ | Field | Type | Effect |
184
+ |-------|------|--------|
185
+ | `errorHandling` | `"retry" \| "skip" \| "abort"` | How to handle the error |
186
+ | `retryCount` | `number` | Max retries (when errorHandling is "retry") |
187
+ | `userNotification` | `string` | Message shown to the user |
188
+
189
+ ---
190
+
191
+ ## Session Object
192
+
193
+ After `joinSession()`, the returned `session` provides:
194
+
195
+ ### session.send(options)
196
+
197
+ Send a message programmatically:
198
+ ```js
199
+ await session.send({ prompt: "Analyze the test results." });
200
+ await session.send({
201
+ prompt: "Review this file",
202
+ attachments: [{ type: "file", path: "./src/index.ts" }],
203
+ });
204
+ ```
205
+
206
+ ### session.sendAndWait(options, timeout?)
207
+
208
+ Send and block until the agent finishes (resolves on `session.idle`):
209
+ ```js
210
+ const response = await session.sendAndWait({ prompt: "What is 2+2?" });
211
+ // response?.data.content contains the agent's reply
212
+ ```
213
+
214
+ ### session.log(message, options?)
215
+
216
+ Log to the CLI timeline:
217
+ ```js
218
+ await session.log("Extension ready");
219
+ await session.log("Rate limit approaching", { level: "warning" });
220
+ await session.log("Connection failed", { level: "error" });
221
+ await session.log("Processing...", { ephemeral: true }); // transient, not persisted
222
+ ```
223
+
224
+ ### session.on(eventType, handler)
225
+
226
+ Subscribe to session events. Returns an unsubscribe function.
227
+ ```js
228
+ const unsub = session.on("tool.execution_complete", (event) => {
229
+ // event.data.toolName, event.data.success, event.data.result
230
+ });
231
+ ```
232
+
233
+ ### Key Event Types
234
+
235
+ | Event | Key Data Fields |
236
+ |-------|----------------|
237
+ | `assistant.message` | `content`, `messageId` |
238
+ | `tool.execution_start` | `toolCallId`, `toolName`, `arguments` |
239
+ | `tool.execution_complete` | `toolCallId`, `toolName`, `success`, `result`, `error` |
240
+ | `user.message` | `content`, `attachments`, `source` |
241
+ | `session.idle` | `backgroundTasks` |
242
+ | `session.error` | `errorType`, `message`, `stack` |
243
+ | `permission.requested` | `requestId`, `permissionRequest.kind` |
244
+ | `session.shutdown` | `shutdownType`, `totalPremiumRequests` |
245
+
246
+ ### session.workspacePath
247
+
248
+ Path to the session workspace directory (checkpoints, plan.md, files/). `undefined` if infinite sessions disabled.
249
+
250
+ ### session.rpc
251
+
252
+ Low-level typed RPC access to all session APIs (model, mode, plan, workspace, etc.).
253
+
254
+ ---
255
+
256
+ ## Gotchas
257
+
258
+ - **stdout is reserved for JSON-RPC.** Don't use `console.log()` — it will corrupt the protocol. Use `session.log()` to surface messages to the user.
259
+ - **Tool name collisions are fatal.** If two extensions register the same tool name, the second extension fails to initialize.
260
+ - **Don't call `session.send()` synchronously from `onUserPromptSubmitted`.** Use `setTimeout(() => session.send(...), 0)` to avoid infinite loops.
261
+ - **Extensions are reloaded on `/clear`.** Any in-memory state is lost between sessions.
262
+ - **Only `.mjs` is supported.** TypeScript (`.ts`) is not yet supported.
263
+ - **The handler's return value is the tool result.** Returning `undefined` sends an empty success. Throwing sends a failure with the error message.