@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/README.md +144 -31
- package/examples/12-skills.ts +88 -0
- package/examples/13-hooks.ts +88 -0
- package/examples/14-openai-compat.ts +71 -0
- package/package.json +1 -1
- package/src/agent.ts +106 -15
- package/src/engine.ts +169 -59
- package/src/index.ts +51 -1
- package/src/providers/anthropic.ts +60 -0
- package/src/providers/index.ts +34 -0
- package/src/providers/openai.ts +315 -0
- package/src/providers/types.ts +85 -0
- package/src/session.ts +5 -5
- package/src/skills/bundled/commit.ts +38 -0
- package/src/skills/bundled/debug.ts +48 -0
- package/src/skills/bundled/index.ts +28 -0
- package/src/skills/bundled/review.ts +41 -0
- package/src/skills/bundled/simplify.ts +51 -0
- package/src/skills/bundled/test.ts +43 -0
- package/src/skills/index.ts +25 -0
- package/src/skills/registry.ts +133 -0
- package/src/skills/types.ts +99 -0
- package/src/tools/agent-tool.ts +13 -2
- package/src/tools/index.ts +8 -0
- package/src/tools/skill-tool.ts +133 -0
- package/src/tools/types.ts +7 -3
- package/src/types.ts +35 -8
- package/src/utils/compact.ts +18 -17
- package/src/utils/messages.ts +12 -13
- package/src/utils/tokens.ts +29 -6
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,
|
|
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
|
|
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:
|
|
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:
|
|
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<
|
|
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:
|
|
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
|
+
}
|