From e2948b720fd9ac9fdc77d15a9802e6a08f7a6fb6 Mon Sep 17 00:00:00 2001 From: Patrick Nikoletich Date: Thu, 26 Feb 2026 20:53:03 -0800 Subject: [PATCH] Add query() convenience API for async iterator pattern Adds a query() function that wraps CopilotClient + session creation into a simple async generator, similar to the Claude Agent SDK's query() API. - New QueryOptions type in types.ts - New query() async generator in query.ts (auto-creates client/session, yields SessionEvent, supports maxTurns, auto-reads COPILOT_CLI_URL) - Exported from index.ts - New todo-tracker.ts sample demonstrating the pattern Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- nodejs/samples/todo-tracker.ts | 58 +++++++++++++++ nodejs/src/index.ts | 2 + nodejs/src/query.ts | 132 +++++++++++++++++++++++++++++++++ nodejs/src/types.ts | 52 +++++++++++++ 4 files changed, 244 insertions(+) create mode 100644 nodejs/samples/todo-tracker.ts create mode 100644 nodejs/src/query.ts diff --git a/nodejs/samples/todo-tracker.ts b/nodejs/samples/todo-tracker.ts new file mode 100644 index 000000000..21c482a78 --- /dev/null +++ b/nodejs/samples/todo-tracker.ts @@ -0,0 +1,58 @@ +/** + * TodoTracker — using the Copilot SDK's query() convenience API. + * + * Run: COPILOT_CLI_URL=localhost:PORT npx tsx todo-tracker.ts + * (start the CLI first with: copilot --headless) + */ + +import { z } from "zod"; +import { query, defineTool } from "@github/copilot-sdk"; + +class TodoTracker { + private todos: any[] = []; + + displayProgress() { + if (this.todos.length === 0) return; + const completed = this.todos.filter((t) => t.status === "completed").length; + const inProgress = this.todos.filter((t) => t.status === "in_progress").length; + const total = this.todos.length; + console.log(`\nProgress: ${completed}/${total} completed`); + console.log(`Currently working on: ${inProgress} task(s)\n`); + this.todos.forEach((todo, index) => { + const icon = + todo.status === "completed" ? "✅" : todo.status === "in_progress" ? "🔧" : "❌"; + const text = todo.status === "in_progress" ? todo.activeForm : todo.content; + console.log(`${index + 1}. ${icon} ${text}`); + }); + } + + todoWriteTool = defineTool("TodoWrite", { + description: "Write or update the todo list for the current task.", + parameters: z.object({ + todos: z.array( + z.object({ + content: z.string(), + status: z.enum(["completed", "in_progress", "pending"]), + activeForm: z.string().optional(), + }), + ), + }), + handler: ({ todos }) => { + this.todos = todos; + this.displayProgress(); + return "Todo list updated."; + }, + }); + + async trackQuery(prompt: string) { + for await (const event of query({ prompt, tools: [this.todoWriteTool], maxTurns: 20 })) { + if (event.type === "assistant.message_delta") { + process.stdout.write(event.data.deltaContent); + } + } + } +} + +// Usage +const tracker = new TodoTracker(); +await tracker.trackQuery("Build a complete authentication system with todos"); diff --git a/nodejs/src/index.ts b/nodejs/src/index.ts index f2655f2fc..7597ae99a 100644 --- a/nodejs/src/index.ts +++ b/nodejs/src/index.ts @@ -11,6 +11,7 @@ export { CopilotClient } from "./client.js"; export { CopilotSession, type AssistantMessageEvent } from "./session.js"; export { defineTool, approveAll } from "./types.js"; +export { query } from "./query.js"; export type { ConnectionState, CopilotClientOptions, @@ -30,6 +31,7 @@ export type { PermissionHandler, PermissionRequest, PermissionRequestResult, + QueryOptions, ResumeSessionConfig, SessionConfig, SessionEvent, diff --git a/nodejs/src/query.ts b/nodejs/src/query.ts new file mode 100644 index 000000000..67a065058 --- /dev/null +++ b/nodejs/src/query.ts @@ -0,0 +1,132 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +/** + * `query()` — a convenience wrapper that provides a simple async-iterator API + * over the Copilot SDK. It creates a client + session, sends a prompt, and + * yields every {@link SessionEvent} as it arrives. + * + * @example + * ```typescript + * import { query, defineTool } from "@github/copilot-sdk"; + * + * for await (const event of query({ prompt: "Hello!", tools: [myTool] })) { + * if (event.type === "assistant.message_delta") { + * process.stdout.write(event.data.deltaContent); + * } + * } + * ``` + * + * @module query + */ + +import { CopilotClient } from "./client.js"; +import { approveAll, type QueryOptions, type SessionEvent } from "./types.js"; + +/** + * Send a prompt and yield every session event as an async iterator. + * + * Internally creates a {@link CopilotClient} and session, sends the prompt, + * and tears everything down when the iterator finishes or is broken out of. + * + * The generator ends when: + * - The session becomes idle (model finished), or + * - `maxTurns` tool-calling turns have been reached, or + * - The consumer breaks out of the `for await` loop. + */ +export async function* query(options: QueryOptions): AsyncGenerator { + const cliUrl = options.cliUrl ?? process.env.COPILOT_CLI_URL; + const client = new CopilotClient({ + ...(cliUrl ? { cliUrl } : {}), + ...(options.cliPath ? { cliPath: options.cliPath } : {}), + ...(options.githubToken ? { githubToken: options.githubToken } : {}), + }); + + try { + const session = await client.createSession({ + model: options.model, + tools: options.tools ?? [], + streaming: options.streaming ?? true, + systemMessage: options.systemMessage, + onPermissionRequest: options.onPermissionRequest ?? approveAll, + }); + + // Bridge the event-driven API to an async iterator via a simple queue. + let resolve: ((value: IteratorResult) => void) | null = null; + const buffer: SessionEvent[] = []; + let done = false; + let turns = 0; + + const finish = () => { + done = true; + if (resolve) { + resolve({ value: undefined as unknown as SessionEvent, done: true }); + resolve = null; + } + }; + + session.on((event: SessionEvent) => { + if (done) return; + + // Count tool-calling turns for maxTurns support. + if ( + options.maxTurns && + event.type === "assistant.message" && + event.data.toolRequests?.length + ) { + turns++; + if (turns >= options.maxTurns) { + if (resolve) { + resolve({ value: event, done: false }); + resolve = null; + } else { + buffer.push(event); + } + finish(); + return; + } + } + + if (event.type === "session.idle") { + if (resolve) { + resolve({ value: event, done: false }); + resolve = null; + } else { + buffer.push(event); + } + finish(); + return; + } + + if (resolve) { + resolve({ value: event, done: false }); + resolve = null; + } else { + buffer.push(event); + } + }); + + await session.send({ prompt: options.prompt }); + + while (!done || buffer.length > 0) { + if (buffer.length > 0) { + yield buffer.shift()!; + } else if (done) { + break; + } else { + yield await new Promise((r) => { + resolve = (result) => { + if (result.done) { + r(undefined as unknown as SessionEvent); + } else { + r(result.value); + } + }; + }); + } + } + } finally { + await client.stop(); + } +} diff --git a/nodejs/src/types.ts b/nodejs/src/types.ts index 3a0ccbce7..8ea1a1c35 100644 --- a/nodejs/src/types.ts +++ b/nodejs/src/types.ts @@ -1052,3 +1052,55 @@ export interface ForegroundSessionInfo { /** Workspace path of the foreground session */ workspacePath?: string; } + +// ============================================================================ +// Query Options (convenience API) +// ============================================================================ + +/** + * Options for the `query()` convenience function. + * Combines the essential CopilotClient and SessionConfig options + * into a single flat configuration. + */ +export interface QueryOptions { + /** The user prompt to send. */ + prompt: string; + + /** Tools exposed to the model. */ + // eslint-disable-next-line @typescript-eslint/no-explicit-any + tools?: Tool[]; + + /** Model to use (e.g. "gpt-5", "claude-sonnet-4.5"). */ + model?: string; + + /** + * Maximum number of agentic turns (assistant responses that include tool calls). + * The generator will end after this many tool-calling turns. + * If not set, the agent runs until it is idle. + */ + maxTurns?: number; + + /** Enable streaming delta events. @default true */ + streaming?: boolean; + + /** + * URL of an existing Copilot CLI server (e.g. "localhost:8080"). + * When provided, the client will not spawn a CLI process. + */ + cliUrl?: string; + + /** Path to the CLI executable. */ + cliPath?: string; + + /** GitHub token for authentication. */ + githubToken?: string; + + /** + * Handler for permission requests. + * @default approveAll + */ + onPermissionRequest?: PermissionHandler; + + /** System message configuration. */ + systemMessage?: SystemMessageConfig; +}