@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.
- package/.env.example +8 -0
- package/LICENSE +21 -0
- package/README.md +388 -0
- package/examples/01-simple-query.ts +43 -0
- package/examples/02-multi-tool.ts +44 -0
- package/examples/03-multi-turn.ts +39 -0
- package/examples/04-prompt-api.ts +29 -0
- package/examples/05-custom-system-prompt.ts +26 -0
- package/examples/06-mcp-server.ts +49 -0
- package/examples/07-custom-tools.ts +87 -0
- package/examples/08-official-api-compat.ts +38 -0
- package/examples/09-subagents.ts +48 -0
- package/examples/10-permissions.ts +40 -0
- package/examples/11-custom-mcp-tools.ts +101 -0
- package/examples/web/index.html +365 -0
- package/examples/web/server.ts +157 -0
- package/package.json +60 -0
- package/src/agent.ts +425 -0
- package/src/engine.ts +520 -0
- package/src/hooks.ts +261 -0
- package/src/index.ts +376 -0
- package/src/mcp/client.ts +150 -0
- package/src/sdk-mcp-server.ts +78 -0
- package/src/session.ts +227 -0
- package/src/tool-helper.ts +127 -0
- package/src/tools/agent-tool.ts +153 -0
- package/src/tools/ask-user.ts +79 -0
- package/src/tools/bash.ts +75 -0
- package/src/tools/config-tool.ts +89 -0
- package/src/tools/cron-tools.ts +153 -0
- package/src/tools/edit.ts +74 -0
- package/src/tools/glob.ts +77 -0
- package/src/tools/grep.ts +168 -0
- package/src/tools/index.ts +232 -0
- package/src/tools/lsp-tool.ts +163 -0
- package/src/tools/mcp-resource-tools.ts +125 -0
- package/src/tools/notebook-edit.ts +93 -0
- package/src/tools/plan-tools.ts +88 -0
- package/src/tools/read.ts +73 -0
- package/src/tools/send-message.ts +96 -0
- package/src/tools/task-tools.ts +290 -0
- package/src/tools/team-tools.ts +128 -0
- package/src/tools/todo-tool.ts +112 -0
- package/src/tools/tool-search.ts +87 -0
- package/src/tools/types.ts +62 -0
- package/src/tools/web-fetch.ts +66 -0
- package/src/tools/web-search.ts +86 -0
- package/src/tools/worktree-tools.ts +140 -0
- package/src/tools/write.ts +42 -0
- package/src/types.ts +459 -0
- package/src/utils/compact.ts +206 -0
- package/src/utils/context.ts +191 -0
- package/src/utils/fileCache.ts +148 -0
- package/src/utils/messages.ts +196 -0
- package/src/utils/retry.ts +140 -0
- package/src/utils/tokens.ts +122 -0
- 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
|
+
}
|