@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,150 @@
1
+ /**
2
+ * MCP Client - Connect to Model Context Protocol servers
3
+ */
4
+
5
+ import type { ToolDefinition, McpServerConfig, ToolContext, ToolResult } from '../types.js'
6
+
7
+ export interface MCPConnection {
8
+ name: string
9
+ status: 'connected' | 'disconnected' | 'error'
10
+ tools: ToolDefinition[]
11
+ close: () => Promise<void>
12
+ }
13
+
14
+ /**
15
+ * Connect to an MCP server and fetch its tools.
16
+ */
17
+ export async function connectMCPServer(
18
+ name: string,
19
+ config: McpServerConfig,
20
+ ): Promise<MCPConnection> {
21
+ try {
22
+ const { Client } = await import('@modelcontextprotocol/sdk/client/index.js')
23
+
24
+ let transport: any
25
+
26
+ if (!config.type || config.type === 'stdio') {
27
+ const stdioConfig = config as { command: string; args?: string[]; env?: Record<string, string> }
28
+ const { StdioClientTransport } = await import('@modelcontextprotocol/sdk/client/stdio.js')
29
+ transport = new StdioClientTransport({
30
+ command: stdioConfig.command,
31
+ args: stdioConfig.args || [],
32
+ env: { ...process.env, ...stdioConfig.env } as Record<string, string>,
33
+ })
34
+ } else if (config.type === 'sse') {
35
+ const sseConfig = config as { url: string; headers?: Record<string, string> }
36
+ const { SSEClientTransport } = await import('@modelcontextprotocol/sdk/client/sse.js')
37
+ transport = new SSEClientTransport(new URL(sseConfig.url), {
38
+ requestInit: sseConfig.headers ? { headers: sseConfig.headers } : undefined,
39
+ } as any)
40
+ } else if (config.type === 'http') {
41
+ const httpConfig = config as { url: string; headers?: Record<string, string> }
42
+ const { StreamableHTTPClientTransport } = await import('@modelcontextprotocol/sdk/client/streamableHttp.js')
43
+ transport = new StreamableHTTPClientTransport(new URL(httpConfig.url), {
44
+ requestInit: httpConfig.headers ? { headers: httpConfig.headers } : undefined,
45
+ } as any)
46
+ } else {
47
+ throw new Error(`Unsupported MCP transport type: ${(config as any).type}`)
48
+ }
49
+
50
+ const client = new Client(
51
+ { name: `agent-sdk-${name}`, version: '1.0.0' },
52
+ { capabilities: {} },
53
+ )
54
+
55
+ await client.connect(transport)
56
+
57
+ // Fetch available tools
58
+ const toolList = await client.listTools()
59
+ const tools: ToolDefinition[] = (toolList.tools || []).map((mcpTool: any) =>
60
+ createMCPToolDefinition(name, mcpTool, client),
61
+ )
62
+
63
+ return {
64
+ name,
65
+ status: 'connected',
66
+ tools,
67
+ async close() {
68
+ try {
69
+ await client.close()
70
+ } catch {
71
+ // ignore close errors
72
+ }
73
+ },
74
+ }
75
+ } catch (err: any) {
76
+ console.error(`[MCP] Failed to connect to "${name}": ${err.message}`)
77
+ return {
78
+ name,
79
+ status: 'error',
80
+ tools: [],
81
+ async close() {},
82
+ }
83
+ }
84
+ }
85
+
86
+ /**
87
+ * Create a ToolDefinition wrapping an MCP server tool.
88
+ */
89
+ function createMCPToolDefinition(
90
+ serverName: string,
91
+ mcpTool: { name: string; description?: string; inputSchema?: any },
92
+ client: any,
93
+ ): ToolDefinition {
94
+ const toolName = `mcp__${serverName}__${mcpTool.name}`
95
+
96
+ return {
97
+ name: toolName,
98
+ description: mcpTool.description || `MCP tool: ${mcpTool.name} from ${serverName}`,
99
+ inputSchema: mcpTool.inputSchema || { type: 'object', properties: {} },
100
+ isReadOnly: () => false,
101
+ isConcurrencySafe: () => false,
102
+ isEnabled: () => true,
103
+ async prompt() {
104
+ return mcpTool.description || ''
105
+ },
106
+ async call(input: any): Promise<ToolResult> {
107
+ try {
108
+ const result = await client.callTool({
109
+ name: mcpTool.name,
110
+ arguments: input,
111
+ })
112
+
113
+ // Extract text content from MCP result
114
+ let output = ''
115
+ if (result.content) {
116
+ for (const block of result.content) {
117
+ if (block.type === 'text') {
118
+ output += block.text
119
+ } else {
120
+ output += JSON.stringify(block)
121
+ }
122
+ }
123
+ } else {
124
+ output = JSON.stringify(result)
125
+ }
126
+
127
+ return {
128
+ type: 'tool_result',
129
+ tool_use_id: '',
130
+ content: output,
131
+ is_error: result.isError || false,
132
+ }
133
+ } catch (err: any) {
134
+ return {
135
+ type: 'tool_result',
136
+ tool_use_id: '',
137
+ content: `MCP tool error: ${err.message}`,
138
+ is_error: true,
139
+ }
140
+ }
141
+ },
142
+ }
143
+ }
144
+
145
+ /**
146
+ * Close all MCP connections.
147
+ */
148
+ export async function closeAllConnections(connections: MCPConnection[]): Promise<void> {
149
+ await Promise.allSettled(connections.map((c) => c.close()))
150
+ }
@@ -0,0 +1,78 @@
1
+ /**
2
+ * In-Process MCP Server
3
+ *
4
+ * createSdkMcpServer() creates an in-process MCP server from tool() definitions.
5
+ * Compatible with open-agent-sdk's createSdkMcpServer().
6
+ *
7
+ * Usage:
8
+ * import { tool, createSdkMcpServer } from 'open-agent-sdk'
9
+ * import { z } from 'zod'
10
+ *
11
+ * const weatherTool = tool('get_weather', 'Get weather', { city: z.string() },
12
+ * async ({ city }) => ({ content: [{ type: 'text', text: `22°C in ${city}` }] })
13
+ * )
14
+ *
15
+ * const server = createSdkMcpServer({
16
+ * name: 'weather',
17
+ * tools: [weatherTool],
18
+ * })
19
+ *
20
+ * // Use as MCP server config:
21
+ * const agent = createAgent({
22
+ * mcpServers: { weather: server },
23
+ * })
24
+ */
25
+
26
+ import type { SdkMcpToolDefinition } from './tool-helper.js'
27
+ import { sdkToolToToolDefinition } from './tool-helper.js'
28
+ import type { ToolDefinition, McpServerConfig } from './types.js'
29
+
30
+ /**
31
+ * SDK MCP server config that includes the in-process server instance.
32
+ */
33
+ export interface McpSdkServerConfig {
34
+ type: 'sdk'
35
+ name: string
36
+ version: string
37
+ tools: ToolDefinition[]
38
+ _sdkTools: SdkMcpToolDefinition<any>[]
39
+ }
40
+
41
+ /**
42
+ * Create an in-process MCP server from tool definitions.
43
+ *
44
+ * The server runs in the same process as the agent, avoiding
45
+ * subprocess overhead. Tools are directly callable.
46
+ */
47
+ export function createSdkMcpServer(options: {
48
+ name: string
49
+ version?: string
50
+ tools?: SdkMcpToolDefinition<any>[]
51
+ }): McpSdkServerConfig {
52
+ const sdkTools = options.tools || []
53
+
54
+ // Convert SDK tools to engine-compatible tool definitions
55
+ // Prefix tool names with mcp__{server_name}__ for namespace isolation
56
+ const toolDefinitions: ToolDefinition[] = sdkTools.map((sdkTool) => {
57
+ const toolDef = sdkToolToToolDefinition(sdkTool)
58
+ return {
59
+ ...toolDef,
60
+ name: `mcp__${options.name}__${sdkTool.name}`,
61
+ }
62
+ })
63
+
64
+ return {
65
+ type: 'sdk',
66
+ name: options.name,
67
+ version: options.version || '1.0.0',
68
+ tools: toolDefinitions,
69
+ _sdkTools: sdkTools,
70
+ }
71
+ }
72
+
73
+ /**
74
+ * Check if a server config is an in-process SDK server.
75
+ */
76
+ export function isSdkServerConfig(config: any): config is McpSdkServerConfig {
77
+ return config?.type === 'sdk' && Array.isArray(config.tools)
78
+ }
package/src/session.ts ADDED
@@ -0,0 +1,227 @@
1
+ /**
2
+ * Session Storage & Management
3
+ *
4
+ * Persists conversation transcripts to disk for resumption.
5
+ * Manages session lifecycle (create, resume, list, fork).
6
+ */
7
+
8
+ import { readFile, writeFile, mkdir, readdir, stat } from 'fs/promises'
9
+ import { join } from 'path'
10
+ import type { Message } from './types.js'
11
+ import type Anthropic from '@anthropic-ai/sdk'
12
+
13
+ /**
14
+ * Session metadata.
15
+ */
16
+ export interface SessionMetadata {
17
+ id: string
18
+ cwd: string
19
+ model: string
20
+ createdAt: string
21
+ updatedAt: string
22
+ messageCount: number
23
+ summary?: string
24
+ }
25
+
26
+ /**
27
+ * Session data on disk.
28
+ */
29
+ export interface SessionData {
30
+ metadata: SessionMetadata
31
+ messages: Anthropic.MessageParam[]
32
+ }
33
+
34
+ /**
35
+ * Get the sessions directory path.
36
+ */
37
+ function getSessionsDir(): string {
38
+ const home = process.env.HOME || process.env.USERPROFILE || '/tmp'
39
+ return join(home, '.open-agent-sdk', 'sessions')
40
+ }
41
+
42
+ /**
43
+ * Get the path for a specific session.
44
+ */
45
+ function getSessionPath(sessionId: string): string {
46
+ return join(getSessionsDir(), sessionId)
47
+ }
48
+
49
+ /**
50
+ * Save session to disk.
51
+ */
52
+ export async function saveSession(
53
+ sessionId: string,
54
+ messages: Anthropic.MessageParam[],
55
+ metadata: Partial<SessionMetadata>,
56
+ ): Promise<void> {
57
+ const dir = getSessionPath(sessionId)
58
+ await mkdir(dir, { recursive: true })
59
+
60
+ const data: SessionData = {
61
+ metadata: {
62
+ id: sessionId,
63
+ cwd: metadata.cwd || process.cwd(),
64
+ model: metadata.model || 'claude-sonnet-4-6',
65
+ createdAt: metadata.createdAt || new Date().toISOString(),
66
+ updatedAt: new Date().toISOString(),
67
+ messageCount: messages.length,
68
+ summary: metadata.summary,
69
+ },
70
+ messages,
71
+ }
72
+
73
+ await writeFile(
74
+ join(dir, 'transcript.json'),
75
+ JSON.stringify(data, null, 2),
76
+ 'utf-8',
77
+ )
78
+ }
79
+
80
+ /**
81
+ * Load session from disk.
82
+ */
83
+ export async function loadSession(sessionId: string): Promise<SessionData | null> {
84
+ try {
85
+ const filePath = join(getSessionPath(sessionId), 'transcript.json')
86
+ const content = await readFile(filePath, 'utf-8')
87
+ return JSON.parse(content) as SessionData
88
+ } catch {
89
+ return null
90
+ }
91
+ }
92
+
93
+ /**
94
+ * List all sessions.
95
+ */
96
+ export async function listSessions(): Promise<SessionMetadata[]> {
97
+ try {
98
+ const dir = getSessionsDir()
99
+ const entries = await readdir(dir)
100
+ const sessions: SessionMetadata[] = []
101
+
102
+ for (const entry of entries) {
103
+ try {
104
+ const data = await loadSession(entry)
105
+ if (data?.metadata) {
106
+ sessions.push(data.metadata)
107
+ }
108
+ } catch {
109
+ // Skip invalid sessions
110
+ }
111
+ }
112
+
113
+ // Sort by updatedAt descending
114
+ sessions.sort((a, b) => b.updatedAt.localeCompare(a.updatedAt))
115
+
116
+ return sessions
117
+ } catch {
118
+ return []
119
+ }
120
+ }
121
+
122
+ /**
123
+ * Fork a session (create a copy with a new ID).
124
+ */
125
+ export async function forkSession(
126
+ sourceSessionId: string,
127
+ newSessionId?: string,
128
+ ): Promise<string | null> {
129
+ const data = await loadSession(sourceSessionId)
130
+ if (!data) return null
131
+
132
+ const forkId = newSessionId || crypto.randomUUID()
133
+
134
+ await saveSession(forkId, data.messages, {
135
+ ...data.metadata,
136
+ id: forkId,
137
+ createdAt: new Date().toISOString(),
138
+ summary: `Forked from session ${sourceSessionId}`,
139
+ })
140
+
141
+ return forkId
142
+ }
143
+
144
+ /**
145
+ * Get session messages.
146
+ */
147
+ export async function getSessionMessages(
148
+ sessionId: string,
149
+ ): Promise<Anthropic.MessageParam[]> {
150
+ const data = await loadSession(sessionId)
151
+ return data?.messages || []
152
+ }
153
+
154
+ /**
155
+ * Append a message to a session transcript.
156
+ */
157
+ export async function appendToSession(
158
+ sessionId: string,
159
+ message: Anthropic.MessageParam,
160
+ ): Promise<void> {
161
+ const data = await loadSession(sessionId)
162
+ if (!data) return
163
+
164
+ data.messages.push(message)
165
+ data.metadata.updatedAt = new Date().toISOString()
166
+ data.metadata.messageCount = data.messages.length
167
+
168
+ await saveSession(sessionId, data.messages, data.metadata)
169
+ }
170
+
171
+ /**
172
+ * Delete a session.
173
+ */
174
+ export async function deleteSession(sessionId: string): Promise<boolean> {
175
+ try {
176
+ const { rm } = await import('fs/promises')
177
+ await rm(getSessionPath(sessionId), { recursive: true, force: true })
178
+ return true
179
+ } catch {
180
+ return false
181
+ }
182
+ }
183
+
184
+ /**
185
+ * Get info about a specific session.
186
+ */
187
+ export async function getSessionInfo(
188
+ sessionId: string,
189
+ options?: { dir?: string },
190
+ ): Promise<SessionMetadata | null> {
191
+ const data = await loadSession(sessionId)
192
+ return data?.metadata || null
193
+ }
194
+
195
+ /**
196
+ * Rename a session.
197
+ */
198
+ export async function renameSession(
199
+ sessionId: string,
200
+ title: string,
201
+ options?: { dir?: string },
202
+ ): Promise<void> {
203
+ const data = await loadSession(sessionId)
204
+ if (!data) return
205
+
206
+ data.metadata.summary = title
207
+ data.metadata.updatedAt = new Date().toISOString()
208
+
209
+ await saveSession(sessionId, data.messages, data.metadata)
210
+ }
211
+
212
+ /**
213
+ * Tag a session.
214
+ */
215
+ export async function tagSession(
216
+ sessionId: string,
217
+ tag: string | null,
218
+ options?: { dir?: string },
219
+ ): Promise<void> {
220
+ const data = await loadSession(sessionId)
221
+ if (!data) return
222
+
223
+ ;(data.metadata as any).tag = tag
224
+ data.metadata.updatedAt = new Date().toISOString()
225
+
226
+ await saveSession(sessionId, data.messages, data.metadata)
227
+ }
@@ -0,0 +1,127 @@
1
+ /**
2
+ * tool() helper - Create tools using Zod schemas
3
+ *
4
+ * Compatible with open-agent-sdk's tool() function.
5
+ *
6
+ * Usage:
7
+ * import { tool } from 'open-agent-sdk'
8
+ * import { z } from 'zod'
9
+ *
10
+ * const weatherTool = tool(
11
+ * 'get_weather',
12
+ * 'Get weather for a city',
13
+ * { city: z.string().describe('City name') },
14
+ * async ({ city }) => {
15
+ * return { content: [{ type: 'text', text: `Weather in ${city}: 22°C` }] }
16
+ * }
17
+ * )
18
+ */
19
+
20
+ import { z, type ZodRawShape, type ZodObject } from 'zod'
21
+ import { zodToJsonSchema } from 'zod-to-json-schema'
22
+ import type { ToolDefinition, ToolResult, ToolContext } from './types.js'
23
+
24
+ /**
25
+ * Tool annotations (MCP standard).
26
+ */
27
+ export interface ToolAnnotations {
28
+ readOnlyHint?: boolean
29
+ destructiveHint?: boolean
30
+ idempotentHint?: boolean
31
+ openWorldHint?: boolean
32
+ }
33
+
34
+ /**
35
+ * Tool call result (MCP-compatible).
36
+ */
37
+ export interface CallToolResult {
38
+ content: Array<
39
+ | { type: 'text'; text: string }
40
+ | { type: 'image'; data: string; mimeType: string }
41
+ | { type: 'resource'; resource: { uri: string; text?: string; blob?: string } }
42
+ >
43
+ isError?: boolean
44
+ }
45
+
46
+ /**
47
+ * SDK MCP tool definition.
48
+ */
49
+ export interface SdkMcpToolDefinition<T extends ZodRawShape = ZodRawShape> {
50
+ name: string
51
+ description: string
52
+ inputSchema: ZodObject<T>
53
+ handler: (args: z.infer<ZodObject<T>>, extra: unknown) => Promise<CallToolResult>
54
+ annotations?: ToolAnnotations
55
+ }
56
+
57
+ /**
58
+ * Create a tool using Zod schema.
59
+ *
60
+ * Compatible with open-agent-sdk's tool() function.
61
+ */
62
+ export function tool<T extends ZodRawShape>(
63
+ name: string,
64
+ description: string,
65
+ inputSchema: T,
66
+ handler: (args: z.infer<ZodObject<T>>, extra: unknown) => Promise<CallToolResult>,
67
+ extras?: { annotations?: ToolAnnotations },
68
+ ): SdkMcpToolDefinition<T> {
69
+ return {
70
+ name,
71
+ description,
72
+ inputSchema: z.object(inputSchema),
73
+ handler,
74
+ annotations: extras?.annotations,
75
+ }
76
+ }
77
+
78
+ /**
79
+ * Convert an SdkMcpToolDefinition to a ToolDefinition for the engine.
80
+ */
81
+ export function sdkToolToToolDefinition(sdkTool: SdkMcpToolDefinition<any>): ToolDefinition {
82
+ const jsonSchema = zodToJsonSchema(sdkTool.inputSchema, { target: 'openApi3' }) as any
83
+
84
+ return {
85
+ name: sdkTool.name,
86
+ description: sdkTool.description,
87
+ inputSchema: {
88
+ type: 'object',
89
+ properties: jsonSchema.properties || {},
90
+ required: jsonSchema.required || [],
91
+ },
92
+ isReadOnly: () => sdkTool.annotations?.readOnlyHint ?? false,
93
+ isConcurrencySafe: () => sdkTool.annotations?.readOnlyHint ?? false,
94
+ isEnabled: () => true,
95
+ async prompt() { return sdkTool.description },
96
+ async call(input: any, _context: ToolContext): Promise<ToolResult> {
97
+ try {
98
+ const parsed = sdkTool.inputSchema.parse(input)
99
+ const result = await sdkTool.handler(parsed, {})
100
+
101
+ // Convert MCP content blocks to string
102
+ const text = result.content
103
+ .map((block) => {
104
+ if (block.type === 'text') return block.text
105
+ if (block.type === 'image') return `[Image: ${block.mimeType}]`
106
+ if (block.type === 'resource') return block.resource.text || `[Resource: ${block.resource.uri}]`
107
+ return JSON.stringify(block)
108
+ })
109
+ .join('\n')
110
+
111
+ return {
112
+ type: 'tool_result',
113
+ tool_use_id: '',
114
+ content: text,
115
+ is_error: result.isError || false,
116
+ }
117
+ } catch (err: any) {
118
+ return {
119
+ type: 'tool_result',
120
+ tool_use_id: '',
121
+ content: `Error: ${err.message}`,
122
+ is_error: true,
123
+ }
124
+ }
125
+ },
126
+ }
127
+ }