@codeany/open-agent-sdk 0.1.0 → 0.2.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/src/index.ts CHANGED
@@ -6,6 +6,7 @@
6
6
  *
7
7
  * Features:
8
8
  * - 30+ built-in tools (file I/O, shell, web, agents, tasks, teams, etc.)
9
+ * - Skill system (reusable prompt templates with bundled skills)
9
10
  * - MCP server integration (stdio, SSE, HTTP)
10
11
  * - Context compression (auto-compact, micro-compact)
11
12
  * - Retry with exponential backoff
@@ -14,7 +15,7 @@
14
15
  * - Permission system (allow/deny/bypass modes)
15
16
  * - Subagent spawning & team coordination
16
17
  * - Task management & scheduling
17
- * - Hook system (pre/post tool use, lifecycle events)
18
+ * - Hook system with lifecycle integration (pre/post tool use, session, compact)
18
19
  * - Token estimation & cost tracking
19
20
  * - File state LRU caching
20
21
  * - Plan mode for structured workflows
@@ -50,6 +51,26 @@ export type { McpSdkServerConfig } from './sdk-mcp-server.js'
50
51
 
51
52
  export { QueryEngine } from './engine.js'
52
53
 
54
+ // --------------------------------------------------------------------------
55
+ // LLM Providers (Anthropic + OpenAI)
56
+ // --------------------------------------------------------------------------
57
+
58
+ export {
59
+ createProvider,
60
+ AnthropicProvider,
61
+ OpenAIProvider,
62
+ } from './providers/index.js'
63
+ export type {
64
+ ApiType,
65
+ LLMProvider,
66
+ CreateMessageParams,
67
+ CreateMessageResponse,
68
+ NormalizedMessageParam,
69
+ NormalizedContentBlock,
70
+ NormalizedTool,
71
+ NormalizedResponseBlock,
72
+ } from './providers/index.js'
73
+
53
74
  // --------------------------------------------------------------------------
54
75
  // Tool System (30+ tools)
55
76
  // --------------------------------------------------------------------------
@@ -123,6 +144,9 @@ export {
123
144
 
124
145
  // Todo
125
146
  TodoWriteTool,
147
+
148
+ // Skill
149
+ SkillTool,
126
150
  } from './tools/index.js'
127
151
 
128
152
  // --------------------------------------------------------------------------
@@ -132,6 +156,27 @@ export {
132
156
  export { connectMCPServer, closeAllConnections } from './mcp/client.js'
133
157
  export type { MCPConnection } from './mcp/client.js'
134
158
 
159
+ // --------------------------------------------------------------------------
160
+ // Skill System
161
+ // --------------------------------------------------------------------------
162
+
163
+ export {
164
+ registerSkill,
165
+ getSkill,
166
+ getAllSkills,
167
+ getUserInvocableSkills,
168
+ hasSkill,
169
+ unregisterSkill,
170
+ clearSkills,
171
+ formatSkillsForPrompt,
172
+ initBundledSkills,
173
+ } from './skills/index.js'
174
+ export type {
175
+ SkillDefinition,
176
+ SkillContentBlock,
177
+ SkillResult,
178
+ } from './skills/index.js'
179
+
135
180
  // --------------------------------------------------------------------------
136
181
  // Hook System
137
182
  // --------------------------------------------------------------------------
@@ -341,6 +386,7 @@ export type {
341
386
 
342
387
  // Permission types
343
388
  PermissionMode,
389
+ PermissionBehavior,
344
390
  CanUseToolFn,
345
391
  CanUseToolResult,
346
392
 
@@ -360,6 +406,10 @@ export type {
360
406
  // Engine types
361
407
  QueryEngineConfig,
362
408
 
409
+ // Content block types
410
+ ContentBlockParam,
411
+ ContentBlock,
412
+
363
413
  // Sandbox types
364
414
  SandboxSettings,
365
415
  SandboxNetworkConfig,
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Anthropic Messages API Provider
3
+ *
4
+ * Wraps the @anthropic-ai/sdk client. Since our internal format is
5
+ * Anthropic-like, this is mostly a thin pass-through.
6
+ */
7
+
8
+ import Anthropic from '@anthropic-ai/sdk'
9
+ import type {
10
+ LLMProvider,
11
+ CreateMessageParams,
12
+ CreateMessageResponse,
13
+ } from './types.js'
14
+
15
+ export class AnthropicProvider implements LLMProvider {
16
+ readonly apiType = 'anthropic-messages' as const
17
+ private client: Anthropic
18
+
19
+ constructor(opts: { apiKey?: string; baseURL?: string }) {
20
+ this.client = new Anthropic({
21
+ apiKey: opts.apiKey,
22
+ baseURL: opts.baseURL,
23
+ })
24
+ }
25
+
26
+ async createMessage(params: CreateMessageParams): Promise<CreateMessageResponse> {
27
+ const requestParams: Anthropic.MessageCreateParamsNonStreaming = {
28
+ model: params.model,
29
+ max_tokens: params.maxTokens,
30
+ system: params.system,
31
+ messages: params.messages as Anthropic.MessageParam[],
32
+ tools: params.tools
33
+ ? (params.tools as Anthropic.Tool[])
34
+ : undefined,
35
+ }
36
+
37
+ // Add extended thinking if configured
38
+ if (params.thinking?.type === 'enabled' && params.thinking.budget_tokens) {
39
+ (requestParams as any).thinking = {
40
+ type: 'enabled',
41
+ budget_tokens: params.thinking.budget_tokens,
42
+ }
43
+ }
44
+
45
+ const response = await this.client.messages.create(requestParams)
46
+
47
+ return {
48
+ content: response.content as CreateMessageResponse['content'],
49
+ stopReason: response.stop_reason || 'end_turn',
50
+ usage: {
51
+ input_tokens: response.usage.input_tokens,
52
+ output_tokens: response.usage.output_tokens,
53
+ cache_creation_input_tokens:
54
+ (response.usage as any).cache_creation_input_tokens,
55
+ cache_read_input_tokens:
56
+ (response.usage as any).cache_read_input_tokens,
57
+ },
58
+ }
59
+ }
60
+ }
@@ -0,0 +1,34 @@
1
+ /**
2
+ * LLM Provider Factory
3
+ *
4
+ * Creates the appropriate provider based on API type configuration.
5
+ */
6
+
7
+ export type { ApiType, LLMProvider, CreateMessageParams, CreateMessageResponse, NormalizedMessageParam, NormalizedContentBlock, NormalizedTool, NormalizedResponseBlock } from './types.js'
8
+
9
+ export { AnthropicProvider } from './anthropic.js'
10
+ export { OpenAIProvider } from './openai.js'
11
+
12
+ import type { ApiType, LLMProvider } from './types.js'
13
+ import { AnthropicProvider } from './anthropic.js'
14
+ import { OpenAIProvider } from './openai.js'
15
+
16
+ /**
17
+ * Create an LLM provider based on the API type.
18
+ *
19
+ * @param apiType - 'anthropic-messages' or 'openai-completions'
20
+ * @param opts - API credentials
21
+ */
22
+ export function createProvider(
23
+ apiType: ApiType,
24
+ opts: { apiKey?: string; baseURL?: string },
25
+ ): LLMProvider {
26
+ switch (apiType) {
27
+ case 'anthropic-messages':
28
+ return new AnthropicProvider(opts)
29
+ case 'openai-completions':
30
+ return new OpenAIProvider(opts)
31
+ default:
32
+ throw new Error(`Unsupported API type: ${apiType}. Use 'anthropic-messages' or 'openai-completions'.`)
33
+ }
34
+ }
@@ -0,0 +1,315 @@
1
+ /**
2
+ * OpenAI Chat Completions API Provider
3
+ *
4
+ * Converts between the SDK's internal Anthropic-like message format
5
+ * and OpenAI's Chat Completions API format.
6
+ *
7
+ * Uses native fetch (no openai SDK dependency required).
8
+ */
9
+
10
+ import type {
11
+ LLMProvider,
12
+ CreateMessageParams,
13
+ CreateMessageResponse,
14
+ NormalizedMessageParam,
15
+ NormalizedContentBlock,
16
+ NormalizedTool,
17
+ NormalizedResponseBlock,
18
+ } from './types.js'
19
+
20
+ // --------------------------------------------------------------------------
21
+ // OpenAI-specific types (minimal, just what we need)
22
+ // --------------------------------------------------------------------------
23
+
24
+ interface OpenAIChatMessage {
25
+ role: 'system' | 'user' | 'assistant' | 'tool'
26
+ content?: string | null
27
+ tool_calls?: OpenAIToolCall[]
28
+ tool_call_id?: string
29
+ }
30
+
31
+ interface OpenAIToolCall {
32
+ id: string
33
+ type: 'function'
34
+ function: {
35
+ name: string
36
+ arguments: string
37
+ }
38
+ }
39
+
40
+ interface OpenAITool {
41
+ type: 'function'
42
+ function: {
43
+ name: string
44
+ description: string
45
+ parameters: Record<string, any>
46
+ }
47
+ }
48
+
49
+ interface OpenAIChatResponse {
50
+ id: string
51
+ choices: Array<{
52
+ index: number
53
+ message: {
54
+ role: 'assistant'
55
+ content: string | null
56
+ tool_calls?: OpenAIToolCall[]
57
+ }
58
+ finish_reason: 'stop' | 'length' | 'tool_calls' | 'content_filter' | string
59
+ }>
60
+ usage?: {
61
+ prompt_tokens: number
62
+ completion_tokens: number
63
+ total_tokens: number
64
+ }
65
+ }
66
+
67
+ // --------------------------------------------------------------------------
68
+ // Provider
69
+ // --------------------------------------------------------------------------
70
+
71
+ export class OpenAIProvider implements LLMProvider {
72
+ readonly apiType = 'openai-completions' as const
73
+ private apiKey: string
74
+ private baseURL: string
75
+
76
+ constructor(opts: { apiKey?: string; baseURL?: string }) {
77
+ this.apiKey = opts.apiKey || ''
78
+ this.baseURL = (opts.baseURL || 'https://api.openai.com/v1').replace(/\/$/, '')
79
+ }
80
+
81
+ async createMessage(params: CreateMessageParams): Promise<CreateMessageResponse> {
82
+ // Convert to OpenAI format
83
+ const messages = this.convertMessages(params.system, params.messages)
84
+ const tools = params.tools ? this.convertTools(params.tools) : undefined
85
+
86
+ const body: Record<string, any> = {
87
+ model: params.model,
88
+ max_tokens: params.maxTokens,
89
+ messages,
90
+ }
91
+
92
+ if (tools && tools.length > 0) {
93
+ body.tools = tools
94
+ }
95
+
96
+ // Make API call
97
+ const response = await fetch(`${this.baseURL}/chat/completions`, {
98
+ method: 'POST',
99
+ headers: {
100
+ 'Content-Type': 'application/json',
101
+ Authorization: `Bearer ${this.apiKey}`,
102
+ },
103
+ body: JSON.stringify(body),
104
+ })
105
+
106
+ if (!response.ok) {
107
+ const errBody = await response.text().catch(() => '')
108
+ const err: any = new Error(
109
+ `OpenAI API error: ${response.status} ${response.statusText}: ${errBody}`,
110
+ )
111
+ err.status = response.status
112
+ throw err
113
+ }
114
+
115
+ const data = (await response.json()) as OpenAIChatResponse
116
+
117
+ // Convert response back to normalized format
118
+ return this.convertResponse(data)
119
+ }
120
+
121
+ // --------------------------------------------------------------------------
122
+ // Message Conversion: Internal → OpenAI
123
+ // --------------------------------------------------------------------------
124
+
125
+ private convertMessages(
126
+ system: string,
127
+ messages: NormalizedMessageParam[],
128
+ ): OpenAIChatMessage[] {
129
+ const result: OpenAIChatMessage[] = []
130
+
131
+ // System prompt as first message
132
+ if (system) {
133
+ result.push({ role: 'system', content: system })
134
+ }
135
+
136
+ for (const msg of messages) {
137
+ if (msg.role === 'user') {
138
+ this.convertUserMessage(msg, result)
139
+ } else if (msg.role === 'assistant') {
140
+ this.convertAssistantMessage(msg, result)
141
+ }
142
+ }
143
+
144
+ return result
145
+ }
146
+
147
+ private convertUserMessage(
148
+ msg: NormalizedMessageParam,
149
+ result: OpenAIChatMessage[],
150
+ ): void {
151
+ if (typeof msg.content === 'string') {
152
+ result.push({ role: 'user', content: msg.content })
153
+ return
154
+ }
155
+
156
+ // Content blocks may contain text and/or tool_result blocks
157
+ const textParts: string[] = []
158
+ const toolResults: Array<{ tool_use_id: string; content: string }> = []
159
+
160
+ for (const block of msg.content) {
161
+ if (block.type === 'text') {
162
+ textParts.push(block.text)
163
+ } else if (block.type === 'tool_result') {
164
+ toolResults.push({
165
+ tool_use_id: block.tool_use_id,
166
+ content: block.content,
167
+ })
168
+ }
169
+ }
170
+
171
+ // Tool results become separate tool messages
172
+ for (const tr of toolResults) {
173
+ result.push({
174
+ role: 'tool',
175
+ tool_call_id: tr.tool_use_id,
176
+ content: tr.content,
177
+ })
178
+ }
179
+
180
+ // Text parts become a user message
181
+ if (textParts.length > 0) {
182
+ result.push({ role: 'user', content: textParts.join('\n') })
183
+ }
184
+ }
185
+
186
+ private convertAssistantMessage(
187
+ msg: NormalizedMessageParam,
188
+ result: OpenAIChatMessage[],
189
+ ): void {
190
+ if (typeof msg.content === 'string') {
191
+ result.push({ role: 'assistant', content: msg.content })
192
+ return
193
+ }
194
+
195
+ // Extract text and tool_use blocks
196
+ const textParts: string[] = []
197
+ const toolCalls: OpenAIToolCall[] = []
198
+
199
+ for (const block of msg.content) {
200
+ if (block.type === 'text') {
201
+ textParts.push(block.text)
202
+ } else if (block.type === 'tool_use') {
203
+ toolCalls.push({
204
+ id: block.id,
205
+ type: 'function',
206
+ function: {
207
+ name: block.name,
208
+ arguments: typeof block.input === 'string'
209
+ ? block.input
210
+ : JSON.stringify(block.input),
211
+ },
212
+ })
213
+ }
214
+ }
215
+
216
+ const assistantMsg: OpenAIChatMessage = {
217
+ role: 'assistant',
218
+ content: textParts.length > 0 ? textParts.join('\n') : null,
219
+ }
220
+
221
+ if (toolCalls.length > 0) {
222
+ assistantMsg.tool_calls = toolCalls
223
+ }
224
+
225
+ result.push(assistantMsg)
226
+ }
227
+
228
+ // --------------------------------------------------------------------------
229
+ // Tool Conversion: Internal → OpenAI
230
+ // --------------------------------------------------------------------------
231
+
232
+ private convertTools(tools: NormalizedTool[]): OpenAITool[] {
233
+ return tools.map((t) => ({
234
+ type: 'function' as const,
235
+ function: {
236
+ name: t.name,
237
+ description: t.description,
238
+ parameters: t.input_schema,
239
+ },
240
+ }))
241
+ }
242
+
243
+ // --------------------------------------------------------------------------
244
+ // Response Conversion: OpenAI → Internal
245
+ // --------------------------------------------------------------------------
246
+
247
+ private convertResponse(data: OpenAIChatResponse): CreateMessageResponse {
248
+ const choice = data.choices[0]
249
+ if (!choice) {
250
+ return {
251
+ content: [{ type: 'text', text: '' }],
252
+ stopReason: 'end_turn',
253
+ usage: { input_tokens: 0, output_tokens: 0 },
254
+ }
255
+ }
256
+
257
+ const content: NormalizedResponseBlock[] = []
258
+
259
+ // Add text content
260
+ if (choice.message.content) {
261
+ content.push({ type: 'text', text: choice.message.content })
262
+ }
263
+
264
+ // Add tool calls
265
+ if (choice.message.tool_calls) {
266
+ for (const tc of choice.message.tool_calls) {
267
+ let input: any
268
+ try {
269
+ input = JSON.parse(tc.function.arguments)
270
+ } catch {
271
+ input = tc.function.arguments
272
+ }
273
+
274
+ content.push({
275
+ type: 'tool_use',
276
+ id: tc.id,
277
+ name: tc.function.name,
278
+ input,
279
+ })
280
+ }
281
+ }
282
+
283
+ // If no content at all, add empty text
284
+ if (content.length === 0) {
285
+ content.push({ type: 'text', text: '' })
286
+ }
287
+
288
+ // Map finish_reason to our normalized stop reasons
289
+ const stopReason = this.mapFinishReason(choice.finish_reason)
290
+
291
+ return {
292
+ content,
293
+ stopReason,
294
+ usage: {
295
+ input_tokens: data.usage?.prompt_tokens || 0,
296
+ output_tokens: data.usage?.completion_tokens || 0,
297
+ },
298
+ }
299
+ }
300
+
301
+ private mapFinishReason(
302
+ reason: string,
303
+ ): 'end_turn' | 'max_tokens' | 'tool_use' | string {
304
+ switch (reason) {
305
+ case 'stop':
306
+ return 'end_turn'
307
+ case 'length':
308
+ return 'max_tokens'
309
+ case 'tool_calls':
310
+ return 'tool_use'
311
+ default:
312
+ return reason
313
+ }
314
+ }
315
+ }
@@ -0,0 +1,85 @@
1
+ /**
2
+ * LLM Provider Abstraction Types
3
+ *
4
+ * Defines a provider interface that normalizes API differences between
5
+ * Anthropic Messages API and OpenAI Chat Completions API.
6
+ *
7
+ * Internally the SDK uses Anthropic-like message format as the canonical
8
+ * representation. Providers convert to/from their native API format.
9
+ */
10
+
11
+ // --------------------------------------------------------------------------
12
+ // API Type
13
+ // --------------------------------------------------------------------------
14
+
15
+ export type ApiType = 'anthropic-messages' | 'openai-completions'
16
+
17
+ // --------------------------------------------------------------------------
18
+ // Normalized Request
19
+ // --------------------------------------------------------------------------
20
+
21
+ export interface CreateMessageParams {
22
+ model: string
23
+ maxTokens: number
24
+ system: string
25
+ messages: NormalizedMessageParam[]
26
+ tools?: NormalizedTool[]
27
+ thinking?: { type: string; budget_tokens?: number }
28
+ }
29
+
30
+ /**
31
+ * Normalized message format (Anthropic-like).
32
+ * This is the internal representation used throughout the SDK.
33
+ */
34
+ export interface NormalizedMessageParam {
35
+ role: 'user' | 'assistant'
36
+ content: string | NormalizedContentBlock[]
37
+ }
38
+
39
+ export type NormalizedContentBlock =
40
+ | { type: 'text'; text: string }
41
+ | { type: 'tool_use'; id: string; name: string; input: any }
42
+ | { type: 'tool_result'; tool_use_id: string; content: string; is_error?: boolean }
43
+ | { type: 'image'; source: any }
44
+ | { type: 'thinking'; thinking: string }
45
+
46
+ export interface NormalizedTool {
47
+ name: string
48
+ description: string
49
+ input_schema: {
50
+ type: 'object'
51
+ properties: Record<string, any>
52
+ required?: string[]
53
+ }
54
+ }
55
+
56
+ // --------------------------------------------------------------------------
57
+ // Normalized Response
58
+ // --------------------------------------------------------------------------
59
+
60
+ export interface CreateMessageResponse {
61
+ content: NormalizedResponseBlock[]
62
+ stopReason: 'end_turn' | 'max_tokens' | 'tool_use' | string
63
+ usage: {
64
+ input_tokens: number
65
+ output_tokens: number
66
+ cache_creation_input_tokens?: number
67
+ cache_read_input_tokens?: number
68
+ }
69
+ }
70
+
71
+ export type NormalizedResponseBlock =
72
+ | { type: 'text'; text: string }
73
+ | { type: 'tool_use'; id: string; name: string; input: any }
74
+
75
+ // --------------------------------------------------------------------------
76
+ // Provider Interface
77
+ // --------------------------------------------------------------------------
78
+
79
+ export interface LLMProvider {
80
+ /** The API type this provider implements. */
81
+ readonly apiType: ApiType
82
+
83
+ /** Send a message and get a response. */
84
+ createMessage(params: CreateMessageParams): Promise<CreateMessageResponse>
85
+ }
package/src/session.ts CHANGED
@@ -8,7 +8,7 @@
8
8
  import { readFile, writeFile, mkdir, readdir, stat } from 'fs/promises'
9
9
  import { join } from 'path'
10
10
  import type { Message } from './types.js'
11
- import type Anthropic from '@anthropic-ai/sdk'
11
+ import type { NormalizedMessageParam } from './providers/types.js'
12
12
 
13
13
  /**
14
14
  * Session metadata.
@@ -28,7 +28,7 @@ export interface SessionMetadata {
28
28
  */
29
29
  export interface SessionData {
30
30
  metadata: SessionMetadata
31
- messages: Anthropic.MessageParam[]
31
+ messages: NormalizedMessageParam[]
32
32
  }
33
33
 
34
34
  /**
@@ -51,7 +51,7 @@ function getSessionPath(sessionId: string): string {
51
51
  */
52
52
  export async function saveSession(
53
53
  sessionId: string,
54
- messages: Anthropic.MessageParam[],
54
+ messages: NormalizedMessageParam[],
55
55
  metadata: Partial<SessionMetadata>,
56
56
  ): Promise<void> {
57
57
  const dir = getSessionPath(sessionId)
@@ -146,7 +146,7 @@ export async function forkSession(
146
146
  */
147
147
  export async function getSessionMessages(
148
148
  sessionId: string,
149
- ): Promise<Anthropic.MessageParam[]> {
149
+ ): Promise<NormalizedMessageParam[]> {
150
150
  const data = await loadSession(sessionId)
151
151
  return data?.messages || []
152
152
  }
@@ -156,7 +156,7 @@ export async function getSessionMessages(
156
156
  */
157
157
  export async function appendToSession(
158
158
  sessionId: string,
159
- message: Anthropic.MessageParam,
159
+ message: NormalizedMessageParam,
160
160
  ): Promise<void> {
161
161
  const data = await loadSession(sessionId)
162
162
  if (!data) return
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Bundled Skill: commit
3
+ *
4
+ * Create a git commit with a well-crafted message based on staged changes.
5
+ */
6
+
7
+ import { registerSkill } from '../registry.js'
8
+ import type { SkillContentBlock } from '../types.js'
9
+
10
+ const COMMIT_PROMPT = `Create a git commit for the current changes. Follow these steps:
11
+
12
+ 1. Run \`git status\` and \`git diff --cached\` to understand what's staged
13
+ 2. If nothing is staged, run \`git diff\` to see unstaged changes and suggest what to stage
14
+ 3. Analyze the changes and draft a concise commit message that:
15
+ - Uses imperative mood ("Add feature" not "Added feature")
16
+ - Summarizes the "why" not just the "what"
17
+ - Keeps the first line under 72 characters
18
+ - Adds a body with details if the change is complex
19
+ 4. Create the commit
20
+
21
+ Do NOT push to remote unless explicitly asked.`
22
+
23
+ export function registerCommitSkill(): void {
24
+ registerSkill({
25
+ name: 'commit',
26
+ description: 'Create a git commit with a well-crafted message based on staged changes.',
27
+ aliases: ['ci'],
28
+ allowedTools: ['Bash', 'Read', 'Glob', 'Grep'],
29
+ userInvocable: true,
30
+ async getPrompt(args): Promise<SkillContentBlock[]> {
31
+ let prompt = COMMIT_PROMPT
32
+ if (args.trim()) {
33
+ prompt += `\n\nAdditional instructions: ${args}`
34
+ }
35
+ return [{ type: 'text', text: prompt }]
36
+ },
37
+ })
38
+ }