@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,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
|
+
}
|