@codeany/open-agent-sdk 0.1.0

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.
Files changed (57) hide show
  1. package/.env.example +8 -0
  2. package/LICENSE +21 -0
  3. package/README.md +388 -0
  4. package/examples/01-simple-query.ts +43 -0
  5. package/examples/02-multi-tool.ts +44 -0
  6. package/examples/03-multi-turn.ts +39 -0
  7. package/examples/04-prompt-api.ts +29 -0
  8. package/examples/05-custom-system-prompt.ts +26 -0
  9. package/examples/06-mcp-server.ts +49 -0
  10. package/examples/07-custom-tools.ts +87 -0
  11. package/examples/08-official-api-compat.ts +38 -0
  12. package/examples/09-subagents.ts +48 -0
  13. package/examples/10-permissions.ts +40 -0
  14. package/examples/11-custom-mcp-tools.ts +101 -0
  15. package/examples/web/index.html +365 -0
  16. package/examples/web/server.ts +157 -0
  17. package/package.json +60 -0
  18. package/src/agent.ts +425 -0
  19. package/src/engine.ts +520 -0
  20. package/src/hooks.ts +261 -0
  21. package/src/index.ts +376 -0
  22. package/src/mcp/client.ts +150 -0
  23. package/src/sdk-mcp-server.ts +78 -0
  24. package/src/session.ts +227 -0
  25. package/src/tool-helper.ts +127 -0
  26. package/src/tools/agent-tool.ts +153 -0
  27. package/src/tools/ask-user.ts +79 -0
  28. package/src/tools/bash.ts +75 -0
  29. package/src/tools/config-tool.ts +89 -0
  30. package/src/tools/cron-tools.ts +153 -0
  31. package/src/tools/edit.ts +74 -0
  32. package/src/tools/glob.ts +77 -0
  33. package/src/tools/grep.ts +168 -0
  34. package/src/tools/index.ts +232 -0
  35. package/src/tools/lsp-tool.ts +163 -0
  36. package/src/tools/mcp-resource-tools.ts +125 -0
  37. package/src/tools/notebook-edit.ts +93 -0
  38. package/src/tools/plan-tools.ts +88 -0
  39. package/src/tools/read.ts +73 -0
  40. package/src/tools/send-message.ts +96 -0
  41. package/src/tools/task-tools.ts +290 -0
  42. package/src/tools/team-tools.ts +128 -0
  43. package/src/tools/todo-tool.ts +112 -0
  44. package/src/tools/tool-search.ts +87 -0
  45. package/src/tools/types.ts +62 -0
  46. package/src/tools/web-fetch.ts +66 -0
  47. package/src/tools/web-search.ts +86 -0
  48. package/src/tools/worktree-tools.ts +140 -0
  49. package/src/tools/write.ts +42 -0
  50. package/src/types.ts +459 -0
  51. package/src/utils/compact.ts +206 -0
  52. package/src/utils/context.ts +191 -0
  53. package/src/utils/fileCache.ts +148 -0
  54. package/src/utils/messages.ts +196 -0
  55. package/src/utils/retry.ts +140 -0
  56. package/src/utils/tokens.ts +122 -0
  57. package/tsconfig.json +19 -0
@@ -0,0 +1,157 @@
1
+ /**
2
+ * Web Chat Server
3
+ *
4
+ * A lightweight HTTP server providing:
5
+ * GET / — serves the chat UI
6
+ * POST /api/chat — SSE stream of agent events
7
+ * POST /api/new — resets the session
8
+ *
9
+ * Run: npx tsx examples/web/server.ts
10
+ */
11
+
12
+ import { createServer, type IncomingMessage, type ServerResponse } from 'http'
13
+ import { readFile } from 'fs/promises'
14
+ import { join, dirname } from 'path'
15
+ import { fileURLToPath } from 'url'
16
+ import { createAgent, type Agent } from '../../src/index.js'
17
+
18
+ const __dirname = dirname(fileURLToPath(import.meta.url))
19
+ const PORT = parseInt(process.env.PORT || '8081')
20
+
21
+ let agent: Agent | null = null
22
+
23
+ function getOrCreateAgent(): Agent {
24
+ if (!agent) {
25
+ agent = createAgent({
26
+ model: process.env.CODEANY_MODEL || 'claude-sonnet-4-6',
27
+ maxTurns: 20,
28
+ })
29
+ }
30
+ return agent
31
+ }
32
+
33
+ function resetAgent(): void {
34
+ agent?.close().catch(() => {})
35
+ agent = null
36
+ }
37
+
38
+ /** Read the full request body as a string. */
39
+ function readBody(req: IncomingMessage): Promise<string> {
40
+ return new Promise((resolve, reject) => {
41
+ const chunks: Buffer[] = []
42
+ req.on('data', (c: Buffer) => chunks.push(c))
43
+ req.on('end', () => resolve(Buffer.concat(chunks).toString()))
44
+ req.on('error', reject)
45
+ })
46
+ }
47
+
48
+ /** Handle POST /api/chat — SSE stream */
49
+ async function handleChat(req: IncomingMessage, res: ServerResponse) {
50
+ const body = JSON.parse(await readBody(req))
51
+ const prompt = body.message?.trim()
52
+ if (!prompt) {
53
+ res.writeHead(400, { 'Content-Type': 'application/json' })
54
+ res.end(JSON.stringify({ error: 'empty message' }))
55
+ return
56
+ }
57
+
58
+ // SSE headers
59
+ res.writeHead(200, {
60
+ 'Content-Type': 'text/event-stream',
61
+ 'Cache-Control': 'no-cache',
62
+ 'Connection': 'keep-alive',
63
+ 'X-Accel-Buffering': 'no',
64
+ })
65
+
66
+ const send = (event: string, data: unknown) => {
67
+ res.write(`data: ${JSON.stringify({ event, data })}\n\n`)
68
+ }
69
+
70
+ const ag = getOrCreateAgent()
71
+ const startMs = Date.now()
72
+
73
+ try {
74
+ for await (const ev of ag.query(prompt)) {
75
+ switch (ev.type) {
76
+ case 'assistant': {
77
+ for (const block of ev.message.content) {
78
+ if (block.type === 'text') {
79
+ send('text', { text: block.text })
80
+ } else if (block.type === 'tool_use') {
81
+ send('tool_use', {
82
+ id: block.id,
83
+ name: block.name,
84
+ input: block.input,
85
+ })
86
+ } else if ('thinking' in block) {
87
+ send('thinking', { thinking: (block as any).thinking })
88
+ }
89
+ }
90
+ break
91
+ }
92
+ case 'tool_result':
93
+ send('tool_result', {
94
+ tool_use_id: ev.result.tool_use_id,
95
+ content: ev.result.output,
96
+ is_error: false,
97
+ })
98
+ break
99
+ case 'result':
100
+ send('result', {
101
+ num_turns: ev.num_turns ?? 0,
102
+ input_tokens: ev.usage?.input_tokens ?? 0,
103
+ output_tokens: ev.usage?.output_tokens ?? 0,
104
+ cost: ev.total_cost_usd ?? ev.cost ?? 0,
105
+ duration_ms: Date.now() - startMs,
106
+ })
107
+ break
108
+ }
109
+ }
110
+ } catch (err: any) {
111
+ send('error', { message: err.message })
112
+ }
113
+
114
+ send('done', null)
115
+ res.end()
116
+ }
117
+
118
+ /** Handle POST /api/new */
119
+ function handleNewSession(_req: IncomingMessage, res: ServerResponse) {
120
+ resetAgent()
121
+ res.writeHead(200, { 'Content-Type': 'application/json' })
122
+ res.end(JSON.stringify({ ok: true }))
123
+ }
124
+
125
+ /** Serve the static index.html */
126
+ async function serveIndex(_req: IncomingMessage, res: ServerResponse) {
127
+ const html = await readFile(join(__dirname, 'index.html'), 'utf-8')
128
+ res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' })
129
+ res.end(html)
130
+ }
131
+
132
+ // --- HTTP Server ---
133
+
134
+ const server = createServer(async (req, res) => {
135
+ const url = req.url || '/'
136
+ const method = req.method || 'GET'
137
+
138
+ try {
139
+ if (url === '/' && method === 'GET') return await serveIndex(req, res)
140
+ if (url === '/api/chat' && method === 'POST') return await handleChat(req, res)
141
+ if (url === '/api/new' && method === 'POST') return handleNewSession(req, res)
142
+
143
+ res.writeHead(404)
144
+ res.end('Not Found')
145
+ } catch (err: any) {
146
+ console.error(err)
147
+ if (!res.headersSent) {
148
+ res.writeHead(500, { 'Content-Type': 'application/json' })
149
+ }
150
+ res.end(JSON.stringify({ error: err.message }))
151
+ }
152
+ })
153
+
154
+ server.listen(PORT, () => {
155
+ console.log(`\n Open Agent SDK — Web Chat`)
156
+ console.log(` http://localhost:${PORT}\n`)
157
+ })
package/package.json ADDED
@@ -0,0 +1,60 @@
1
+ {
2
+ "name": "@codeany/open-agent-sdk",
3
+ "author": {
4
+ "name": "CodeAny",
5
+ "url": "https://codeany.ai"
6
+ },
7
+ "homepage": "https://github.com/codeany-ai/open-agent-sdk-typescript",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "https://github.com/codeany-ai/open-agent-sdk-typescript.git"
11
+ },
12
+ "bugs": {
13
+ "url": "https://github.com/codeany-ai/open-agent-sdk-typescript/issues"
14
+ },
15
+ "version": "0.1.0",
16
+ "description": "Open-source Agent SDK. Runs the full agent loop in-process — no local CLI required. Deploy anywhere: cloud, serverless, Docker, CI/CD.",
17
+ "type": "module",
18
+ "main": "./dist/index.js",
19
+ "types": "./dist/index.d.ts",
20
+ "exports": {
21
+ ".": {
22
+ "types": "./dist/index.d.ts",
23
+ "import": "./dist/index.js"
24
+ }
25
+ },
26
+ "scripts": {
27
+ "build": "tsc",
28
+ "dev": "tsc --watch",
29
+ "test": "npx tsx examples/01-simple-query.ts",
30
+ "test:all": "for f in examples/*.ts; do echo \"--- Running $f ---\"; npx tsx $f; echo; done",
31
+ "web": "npx tsx examples/web/server.ts"
32
+ },
33
+ "engines": {
34
+ "node": ">=18.0.0"
35
+ },
36
+ "dependencies": {
37
+ "@anthropic-ai/sdk": "^0.52.0",
38
+ "@modelcontextprotocol/sdk": "^1.12.1",
39
+ "zod": "^3.23.0",
40
+ "zod-to-json-schema": "^3.24.0"
41
+ },
42
+ "devDependencies": {
43
+ "@types/node": "^22.0.0",
44
+ "tsx": "^4.19.0",
45
+ "typescript": "^5.7.0"
46
+ },
47
+ "keywords": [
48
+ "open-agent-sdk",
49
+ "codeany",
50
+ "agent",
51
+ "sdk",
52
+ "ai",
53
+ "llm",
54
+ "tools",
55
+ "agentic",
56
+ "coding-agent",
57
+ "mcp"
58
+ ],
59
+ "license": "MIT"
60
+ }
package/src/agent.ts ADDED
@@ -0,0 +1,425 @@
1
+ /**
2
+ * Agent - High-level API
3
+ *
4
+ * Provides createAgent() and query() interfaces compatible with
5
+ * open-agent-sdk.
6
+ *
7
+ * Usage:
8
+ * import { createAgent } from 'open-agent-sdk'
9
+ * const agent = createAgent({ model: 'claude-sonnet-4-6' })
10
+ * for await (const event of agent.query('Hello')) { ... }
11
+ */
12
+
13
+ import type {
14
+ AgentOptions,
15
+ QueryResult,
16
+ SDKMessage,
17
+ ToolDefinition,
18
+ CanUseToolFn,
19
+ Message,
20
+ TokenUsage,
21
+ PermissionMode,
22
+ } from './types.js'
23
+ import { QueryEngine } from './engine.js'
24
+ import { getAllBaseTools, filterTools } from './tools/index.js'
25
+ import { connectMCPServer, type MCPConnection } from './mcp/client.js'
26
+ import { isSdkServerConfig } from './sdk-mcp-server.js'
27
+ import { registerAgents } from './tools/agent-tool.js'
28
+ import {
29
+ saveSession,
30
+ loadSession,
31
+ } from './session.js'
32
+ import type Anthropic from '@anthropic-ai/sdk'
33
+
34
+ // --------------------------------------------------------------------------
35
+ // Agent class
36
+ // --------------------------------------------------------------------------
37
+
38
+ export class Agent {
39
+ private cfg: AgentOptions
40
+ private toolPool: ToolDefinition[]
41
+ private modelId: string
42
+ private apiCredentials: { key?: string; baseUrl?: string }
43
+ private mcpLinks: MCPConnection[] = []
44
+ private history: Anthropic.MessageParam[] = []
45
+ private messageLog: Message[] = []
46
+ private setupDone: Promise<void>
47
+ private sid: string
48
+ private abortCtrl: AbortController | null = null
49
+ private currentEngine: QueryEngine | null = null
50
+
51
+ constructor(options: AgentOptions = {}) {
52
+ // Shallow copy to avoid mutating caller's object
53
+ this.cfg = { ...options }
54
+
55
+ // Merge credentials from options.env map, direct options, and process.env
56
+ this.apiCredentials = this.pickCredentials()
57
+ this.modelId = this.cfg.model ?? this.readEnv('CODEANY_MODEL') ?? 'claude-sonnet-4-6'
58
+ this.sid = this.cfg.sessionId ?? crypto.randomUUID()
59
+
60
+ // The underlying @anthropic-ai/sdk reads ANTHROPIC_API_KEY from process.env,
61
+ // so we bridge our resolved credentials into it.
62
+ if (this.apiCredentials.key) {
63
+ process.env.ANTHROPIC_API_KEY = this.apiCredentials.key
64
+ }
65
+ if (this.apiCredentials.baseUrl) {
66
+ process.env.ANTHROPIC_BASE_URL = this.apiCredentials.baseUrl
67
+ }
68
+
69
+ // Build tool pool from options (supports ToolDefinition[], string[], or preset)
70
+ this.toolPool = this.buildToolPool()
71
+
72
+ // Kick off async setup (MCP connections, agent registration, session resume)
73
+ this.setupDone = this.setup()
74
+ }
75
+
76
+ /** Pick API key and base URL from options or CODEANY_* env vars. */
77
+ private pickCredentials(): { key?: string; baseUrl?: string } {
78
+ const envMap = this.cfg.env
79
+ return {
80
+ key:
81
+ this.cfg.apiKey ??
82
+ envMap?.CODEANY_API_KEY ??
83
+ envMap?.CODEANY_AUTH_TOKEN ??
84
+ this.readEnv('CODEANY_API_KEY') ??
85
+ this.readEnv('CODEANY_AUTH_TOKEN'),
86
+ baseUrl:
87
+ this.cfg.baseURL ??
88
+ envMap?.CODEANY_BASE_URL ??
89
+ this.readEnv('CODEANY_BASE_URL'),
90
+ }
91
+ }
92
+
93
+ /** Read a value from process.env (returns undefined if missing). */
94
+ private readEnv(key: string): string | undefined {
95
+ return process.env[key] || undefined
96
+ }
97
+
98
+ /** Assemble the available tool set based on options. */
99
+ private buildToolPool(): ToolDefinition[] {
100
+ const raw = this.cfg.tools
101
+ let pool: ToolDefinition[]
102
+
103
+ if (!raw || (typeof raw === 'object' && !Array.isArray(raw) && 'type' in raw)) {
104
+ pool = getAllBaseTools()
105
+ } else if (Array.isArray(raw) && raw.length > 0 && typeof raw[0] === 'string') {
106
+ pool = filterTools(getAllBaseTools(), raw as string[])
107
+ } else {
108
+ pool = raw as ToolDefinition[]
109
+ }
110
+
111
+ return filterTools(pool, this.cfg.allowedTools, this.cfg.disallowedTools)
112
+ }
113
+
114
+ /**
115
+ * Async initialization: connect MCP servers, register agents, resume sessions.
116
+ */
117
+ private async setup(): Promise<void> {
118
+ // Register custom agent definitions
119
+ if (this.cfg.agents) {
120
+ registerAgents(this.cfg.agents)
121
+ }
122
+
123
+ // Connect MCP servers (supports stdio, SSE, HTTP, and in-process SDK servers)
124
+ if (this.cfg.mcpServers) {
125
+ for (const [name, config] of Object.entries(this.cfg.mcpServers)) {
126
+ try {
127
+ if (isSdkServerConfig(config)) {
128
+ // In-process SDK MCP server - directly add tools
129
+ this.toolPool = [...this.toolPool, ...config.tools]
130
+ } else {
131
+ // External MCP server
132
+ const connection = await connectMCPServer(name, config)
133
+ this.mcpLinks.push(connection)
134
+
135
+ if (connection.status === 'connected' && connection.tools.length > 0) {
136
+ this.toolPool = [...this.toolPool, ...connection.tools]
137
+ }
138
+ }
139
+ } catch (err: any) {
140
+ console.error(`[MCP] Failed to connect to "${name}": ${err.message}`)
141
+ }
142
+ }
143
+ }
144
+
145
+ // Resume or continue session
146
+ if (this.cfg.resume) {
147
+ const sessionData = await loadSession(this.cfg.resume)
148
+ if (sessionData) {
149
+ this.history = sessionData.messages
150
+ this.sid = this.cfg.resume
151
+ }
152
+ }
153
+ }
154
+
155
+ /**
156
+ * Run a query with streaming events.
157
+ */
158
+ async *query(
159
+ prompt: string,
160
+ overrides?: Partial<AgentOptions>,
161
+ ): AsyncGenerator<SDKMessage, void> {
162
+ await this.setupDone
163
+
164
+ const opts = { ...this.cfg, ...overrides }
165
+ const cwd = opts.cwd || process.cwd()
166
+
167
+ // Create abort controller for this query
168
+ this.abortCtrl = opts.abortController || new AbortController()
169
+ if (opts.abortSignal) {
170
+ opts.abortSignal.addEventListener('abort', () => this.abortCtrl?.abort(), { once: true })
171
+ }
172
+
173
+ // Resolve systemPrompt (handle preset object)
174
+ let systemPrompt: string | undefined
175
+ let appendSystemPrompt = opts.appendSystemPrompt
176
+ if (typeof opts.systemPrompt === 'object' && opts.systemPrompt?.type === 'preset') {
177
+ systemPrompt = undefined // Use engine default (default style)
178
+ if (opts.systemPrompt.append) {
179
+ appendSystemPrompt = (appendSystemPrompt || '') + '\n' + opts.systemPrompt.append
180
+ }
181
+ } else {
182
+ systemPrompt = opts.systemPrompt as string | undefined
183
+ }
184
+
185
+ // Build canUseTool based on permission mode
186
+ const permMode = opts.permissionMode ?? 'bypassPermissions'
187
+ const canUseTool: CanUseToolFn = opts.canUseTool ?? (async (_tool, _input) => {
188
+ if (permMode === 'bypassPermissions' || permMode === 'dontAsk' || permMode === 'auto') {
189
+ return { behavior: 'allow' }
190
+ }
191
+ if (permMode === 'acceptEdits') {
192
+ return { behavior: 'allow' }
193
+ }
194
+ return { behavior: 'allow' }
195
+ })
196
+
197
+ // Resolve tools with overrides
198
+ let tools = this.toolPool
199
+ if (overrides?.allowedTools || overrides?.disallowedTools) {
200
+ tools = filterTools(tools, overrides.allowedTools, overrides.disallowedTools)
201
+ }
202
+ if (overrides?.tools) {
203
+ const ot = overrides.tools
204
+ if (Array.isArray(ot) && ot.length > 0 && typeof ot[0] === 'string') {
205
+ tools = filterTools(this.toolPool, ot as string[])
206
+ } else if (Array.isArray(ot)) {
207
+ tools = ot as ToolDefinition[]
208
+ }
209
+ }
210
+
211
+ // Create query engine with current conversation state
212
+ const engine = new QueryEngine({
213
+ cwd,
214
+ model: opts.model || this.modelId,
215
+ apiKey: this.apiCredentials.key,
216
+ baseURL: this.apiCredentials.baseUrl,
217
+ tools,
218
+ systemPrompt,
219
+ appendSystemPrompt,
220
+ maxTurns: opts.maxTurns ?? 10,
221
+ maxBudgetUsd: opts.maxBudgetUsd,
222
+ maxTokens: opts.maxTokens ?? 16384,
223
+ thinking: opts.thinking,
224
+ jsonSchema: opts.jsonSchema,
225
+ canUseTool,
226
+ includePartialMessages: opts.includePartialMessages ?? false,
227
+ abortSignal: this.abortCtrl.signal,
228
+ agents: opts.agents,
229
+ })
230
+ this.currentEngine = engine
231
+
232
+ // Inject existing conversation history
233
+ for (const msg of this.history) {
234
+ (engine as any).messages.push(msg)
235
+ }
236
+
237
+ // Run the engine
238
+ for await (const event of engine.submitMessage(prompt)) {
239
+ yield event
240
+
241
+ // Track assistant messages for multi-turn persistence
242
+ if (event.type === 'assistant') {
243
+ const uuid = crypto.randomUUID()
244
+ const timestamp = new Date().toISOString()
245
+ this.messageLog.push({
246
+ type: 'assistant',
247
+ message: event.message,
248
+ uuid,
249
+ timestamp,
250
+ })
251
+ }
252
+ }
253
+
254
+ // Persist conversation state for multi-turn
255
+ this.history = engine.getMessages()
256
+
257
+ // Add user message to tracked messages
258
+ const userUuid = crypto.randomUUID()
259
+ this.messageLog.push({
260
+ type: 'user',
261
+ message: { role: 'user', content: prompt },
262
+ uuid: userUuid,
263
+ timestamp: new Date().toISOString(),
264
+ })
265
+ }
266
+
267
+ /**
268
+ * Convenience method: send a prompt and collect the final answer as a single object.
269
+ * Internally iterates through the streaming query and aggregates the outcome.
270
+ */
271
+ async prompt(
272
+ text: string,
273
+ overrides?: Partial<AgentOptions>,
274
+ ): Promise<QueryResult> {
275
+ const t0 = performance.now()
276
+ const collected = { text: '', turns: 0, tokens: { in: 0, out: 0 } }
277
+
278
+ for await (const ev of this.query(text, overrides)) {
279
+ switch (ev.type) {
280
+ case 'assistant': {
281
+ // Extract the last assistant text (multi-turn: only final answer matters)
282
+ const fragments = ev.message.content
283
+ .filter((c): c is Anthropic.TextBlock => c.type === 'text')
284
+ .map((c) => c.text)
285
+ if (fragments.length) collected.text = fragments.join('')
286
+ break
287
+ }
288
+ case 'result':
289
+ collected.turns = ev.num_turns ?? 0
290
+ collected.tokens.in = ev.usage?.input_tokens ?? 0
291
+ collected.tokens.out = ev.usage?.output_tokens ?? 0
292
+ break
293
+ }
294
+ }
295
+
296
+ return {
297
+ text: collected.text,
298
+ usage: { input_tokens: collected.tokens.in, output_tokens: collected.tokens.out },
299
+ num_turns: collected.turns,
300
+ duration_ms: Math.round(performance.now() - t0),
301
+ messages: [...this.messageLog],
302
+ }
303
+ }
304
+
305
+ /**
306
+ * Get conversation messages.
307
+ */
308
+ getMessages(): Message[] {
309
+ return [...this.messageLog]
310
+ }
311
+
312
+ /**
313
+ * Reset conversation history.
314
+ */
315
+ clear(): void {
316
+ this.history = []
317
+ this.messageLog = []
318
+ }
319
+
320
+ /**
321
+ * Interrupt the current query.
322
+ */
323
+ async interrupt(): Promise<void> {
324
+ this.abortCtrl?.abort()
325
+ }
326
+
327
+ /**
328
+ * Change the model during a session.
329
+ */
330
+ async setModel(model?: string): Promise<void> {
331
+ if (model) {
332
+ this.modelId = model
333
+ this.cfg.model = model
334
+ }
335
+ }
336
+
337
+ /**
338
+ * Change the permission mode during a session.
339
+ */
340
+ async setPermissionMode(mode: PermissionMode): Promise<void> {
341
+ this.cfg.permissionMode = mode
342
+ }
343
+
344
+ /**
345
+ * Set maximum thinking tokens.
346
+ */
347
+ async setMaxThinkingTokens(maxThinkingTokens: number | null): Promise<void> {
348
+ if (maxThinkingTokens === null) {
349
+ this.cfg.thinking = { type: 'disabled' }
350
+ } else {
351
+ this.cfg.thinking = { type: 'enabled', budgetTokens: maxThinkingTokens }
352
+ }
353
+ }
354
+
355
+ /**
356
+ * Get the session ID.
357
+ */
358
+ getSessionId(): string {
359
+ return this.sid
360
+ }
361
+
362
+ /**
363
+ * Stop a background task.
364
+ */
365
+ async stopTask(taskId: string): Promise<void> {
366
+ const { getTask } = await import('./tools/task-tools.js')
367
+ const task = getTask(taskId)
368
+ if (task) {
369
+ task.status = 'cancelled'
370
+ }
371
+ }
372
+
373
+ /**
374
+ * Close MCP connections and clean up.
375
+ * Optionally persist session to disk.
376
+ */
377
+ async close(): Promise<void> {
378
+ // Persist session if enabled
379
+ if (this.cfg.persistSession !== false && this.history.length > 0) {
380
+ try {
381
+ await saveSession(this.sid, this.history, {
382
+ cwd: this.cfg.cwd || process.cwd(),
383
+ model: this.modelId,
384
+ summary: undefined,
385
+ })
386
+ } catch {
387
+ // Session persistence is best-effort
388
+ }
389
+ }
390
+
391
+ for (const conn of this.mcpLinks) {
392
+ await conn.close()
393
+ }
394
+ this.mcpLinks = []
395
+ }
396
+ }
397
+
398
+ // --------------------------------------------------------------------------
399
+ // Factory function
400
+ // --------------------------------------------------------------------------
401
+
402
+ /** Factory: shorthand for `new Agent(options)`. */
403
+ export function createAgent(options: AgentOptions = {}): Agent {
404
+ return new Agent(options)
405
+ }
406
+
407
+ // --------------------------------------------------------------------------
408
+ // Standalone query — one-shot convenience wrapper
409
+ // --------------------------------------------------------------------------
410
+
411
+ /**
412
+ * Execute a single agentic query without managing an Agent instance.
413
+ * The agent is created, used, and cleaned up automatically.
414
+ */
415
+ export async function* query(params: {
416
+ prompt: string
417
+ options?: AgentOptions
418
+ }): AsyncGenerator<SDKMessage, void> {
419
+ const ephemeral = createAgent(params.options)
420
+ try {
421
+ yield* ephemeral.query(params.prompt)
422
+ } finally {
423
+ await ephemeral.close()
424
+ }
425
+ }