@github/copilot-sdk 0.1.32 → 0.1.33-unstable.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.
@@ -0,0 +1,265 @@
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 { approveAll } from "@github/copilot-sdk";
63
+ import { joinSession } from "@github/copilot-sdk/extension";
64
+
65
+ await joinSession({
66
+ onPermissionRequest: approveAll, // Required — handle permission requests
67
+ tools: [], // Optional — custom tools
68
+ hooks: {}, // Optional — lifecycle hooks
69
+ });
70
+ ```
71
+
72
+ ---
73
+
74
+ ## Registering Tools
75
+
76
+ ```js
77
+ tools: [
78
+ {
79
+ name: "tool_name", // Required. Must be globally unique across all extensions.
80
+ description: "What it does", // Required. Shown to the agent in tool descriptions.
81
+ parameters: { // Optional. JSON Schema for the arguments.
82
+ type: "object",
83
+ properties: {
84
+ arg1: { type: "string", description: "..." },
85
+ },
86
+ required: ["arg1"],
87
+ },
88
+ handler: async (args, invocation) => {
89
+ // args: parsed arguments matching the schema
90
+ // invocation.sessionId: current session ID
91
+ // invocation.toolCallId: unique call ID
92
+ // invocation.toolName: this tool's name
93
+ //
94
+ // Return value: string or ToolResultObject
95
+ // string → treated as success
96
+ // { textResultForLlm, resultType } → structured result
97
+ // resultType: "success" | "failure" | "rejected" | "denied"
98
+ return `Result: ${args.arg1}`;
99
+ },
100
+ },
101
+ ]
102
+ ```
103
+
104
+ **Constraints:**
105
+ - Tool names must be unique across ALL loaded extensions. Collisions cause the second extension to fail to load.
106
+ - Handler must return a string or `{ textResultForLlm: string, resultType?: string }`.
107
+ - Handler receives `(args, invocation)` — the second argument has `sessionId`, `toolCallId`, `toolName`.
108
+ - Use `session.log()` to surface messages to the user. Don't use `console.log()` (stdout is reserved for JSON-RPC).
109
+
110
+ ---
111
+
112
+ ## Registering Hooks
113
+
114
+ ```js
115
+ hooks: {
116
+ onUserPromptSubmitted: async (input, invocation) => { ... },
117
+ onPreToolUse: async (input, invocation) => { ... },
118
+ onPostToolUse: async (input, invocation) => { ... },
119
+ onSessionStart: async (input, invocation) => { ... },
120
+ onSessionEnd: async (input, invocation) => { ... },
121
+ onErrorOccurred: async (input, invocation) => { ... },
122
+ }
123
+ ```
124
+
125
+ All hook inputs include `timestamp` (unix ms) and `cwd` (working directory).
126
+ All handlers receive `invocation: { sessionId: string }` as the second argument.
127
+ All handlers may return `void`/`undefined` (no-op) or an output object.
128
+
129
+ ### onUserPromptSubmitted
130
+
131
+ **Input:** `{ prompt: string, timestamp, cwd }`
132
+
133
+ **Output (all fields optional):**
134
+ | Field | Type | Effect |
135
+ |-------|------|--------|
136
+ | `modifiedPrompt` | `string` | Replaces the user's prompt |
137
+ | `additionalContext` | `string` | Appended as hidden context the agent sees |
138
+
139
+ ### onPreToolUse
140
+
141
+ **Input:** `{ toolName: string, toolArgs: unknown, timestamp, cwd }`
142
+
143
+ **Output (all fields optional):**
144
+ | Field | Type | Effect |
145
+ |-------|------|--------|
146
+ | `permissionDecision` | `"allow" \| "deny" \| "ask"` | Override the permission check |
147
+ | `permissionDecisionReason` | `string` | Shown to user if denied |
148
+ | `modifiedArgs` | `unknown` | Replaces the tool arguments |
149
+ | `additionalContext` | `string` | Injected into the conversation |
150
+
151
+ ### onPostToolUse
152
+
153
+ **Input:** `{ toolName: string, toolArgs: unknown, toolResult: ToolResultObject, timestamp, cwd }`
154
+
155
+ **Output (all fields optional):**
156
+ | Field | Type | Effect |
157
+ |-------|------|--------|
158
+ | `modifiedResult` | `ToolResultObject` | Replaces the tool result |
159
+ | `additionalContext` | `string` | Injected into the conversation |
160
+
161
+ ### onSessionStart
162
+
163
+ **Input:** `{ source: "startup" \| "resume" \| "new", initialPrompt?: string, timestamp, cwd }`
164
+
165
+ **Output (all fields optional):**
166
+ | Field | Type | Effect |
167
+ |-------|------|--------|
168
+ | `additionalContext` | `string` | Injected as initial context |
169
+
170
+ ### onSessionEnd
171
+
172
+ **Input:** `{ reason: "complete" \| "error" \| "abort" \| "timeout" \| "user_exit", finalMessage?: string, error?: string, timestamp, cwd }`
173
+
174
+ **Output (all fields optional):**
175
+ | Field | Type | Effect |
176
+ |-------|------|--------|
177
+ | `sessionSummary` | `string` | Summary for session persistence |
178
+ | `cleanupActions` | `string[]` | Cleanup descriptions |
179
+
180
+ ### onErrorOccurred
181
+
182
+ **Input:** `{ error: string, errorContext: "model_call" \| "tool_execution" \| "system" \| "user_input", recoverable: boolean, timestamp, cwd }`
183
+
184
+ **Output (all fields optional):**
185
+ | Field | Type | Effect |
186
+ |-------|------|--------|
187
+ | `errorHandling` | `"retry" \| "skip" \| "abort"` | How to handle the error |
188
+ | `retryCount` | `number` | Max retries (when errorHandling is "retry") |
189
+ | `userNotification` | `string` | Message shown to the user |
190
+
191
+ ---
192
+
193
+ ## Session Object
194
+
195
+ After `joinSession()`, the returned `session` provides:
196
+
197
+ ### session.send(options)
198
+
199
+ Send a message programmatically:
200
+ ```js
201
+ await session.send({ prompt: "Analyze the test results." });
202
+ await session.send({
203
+ prompt: "Review this file",
204
+ attachments: [{ type: "file", path: "./src/index.ts" }],
205
+ });
206
+ ```
207
+
208
+ ### session.sendAndWait(options, timeout?)
209
+
210
+ Send and block until the agent finishes (resolves on `session.idle`):
211
+ ```js
212
+ const response = await session.sendAndWait({ prompt: "What is 2+2?" });
213
+ // response?.data.content contains the agent's reply
214
+ ```
215
+
216
+ ### session.log(message, options?)
217
+
218
+ Log to the CLI timeline:
219
+ ```js
220
+ await session.log("Extension ready");
221
+ await session.log("Rate limit approaching", { level: "warning" });
222
+ await session.log("Connection failed", { level: "error" });
223
+ await session.log("Processing...", { ephemeral: true }); // transient, not persisted
224
+ ```
225
+
226
+ ### session.on(eventType, handler)
227
+
228
+ Subscribe to session events. Returns an unsubscribe function.
229
+ ```js
230
+ const unsub = session.on("tool.execution_complete", (event) => {
231
+ // event.data.toolName, event.data.success, event.data.result
232
+ });
233
+ ```
234
+
235
+ ### Key Event Types
236
+
237
+ | Event | Key Data Fields |
238
+ |-------|----------------|
239
+ | `assistant.message` | `content`, `messageId` |
240
+ | `tool.execution_start` | `toolCallId`, `toolName`, `arguments` |
241
+ | `tool.execution_complete` | `toolCallId`, `toolName`, `success`, `result`, `error` |
242
+ | `user.message` | `content`, `attachments`, `source` |
243
+ | `session.idle` | `backgroundTasks` |
244
+ | `session.error` | `errorType`, `message`, `stack` |
245
+ | `permission.requested` | `requestId`, `permissionRequest.kind` |
246
+ | `session.shutdown` | `shutdownType`, `totalPremiumRequests` |
247
+
248
+ ### session.workspacePath
249
+
250
+ Path to the session workspace directory (checkpoints, plan.md, files/). `undefined` if infinite sessions disabled.
251
+
252
+ ### session.rpc
253
+
254
+ Low-level typed RPC access to all session APIs (model, mode, plan, workspace, etc.).
255
+
256
+ ---
257
+
258
+ ## Gotchas
259
+
260
+ - **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.
261
+ - **Tool name collisions are fatal.** If two extensions register the same tool name, the second extension fails to initialize.
262
+ - **Don't call `session.send()` synchronously from `onUserPromptSubmitted`.** Use `setTimeout(() => session.send(...), 0)` to avoid infinite loops.
263
+ - **Extensions are reloaded on `/clear`.** Any in-memory state is lost between sessions.
264
+ - **Only `.mjs` is supported.** TypeScript (`.ts`) is not yet supported.
265
+ - **The handler's return value is the tool result.** Returning `undefined` sends an empty success. Throwing sends a failure with the error message.