mirror of
https://github.com/codeany-ai/open-agent-sdk-typescript.git
synced 2026-04-27 07:16:21 +09:00
feat: add skills system, hooks integration, and OpenAI-compatible provider
- Add skill system with types, registry, SkillTool, and 5 bundled skills
(simplify, commit, review, debug, test)
- Integrate hooks into QueryEngine at 9 lifecycle points (SessionStart,
UserPromptSubmit, PreToolUse, PostToolUse, PostToolUseFailure,
PreCompact, PostCompact, Stop, SessionEnd)
- Add LLM provider abstraction supporting both Anthropic Messages API
and OpenAI Chat Completions API (works with GPT, DeepSeek, Qwen, etc.)
- Add CODEANY_API_TYPE env var ('anthropic-messages' | 'openai-completions')
with auto-detection from model name
- Remove all ANTHROPIC_* env var references, only support CODEANY_* prefix
- Add model pricing and context windows for OpenAI/DeepSeek models
- Remove direct @anthropic-ai/sdk dependency from all files except the
Anthropic provider (types.ts, engine.ts, etc. are now provider-agnostic)
- Add PermissionBehavior type export
- Bump version to 0.2.0
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,88 @@
|
||||
/**
|
||||
* Example 12: Skills
|
||||
*
|
||||
* Shows how to use the skill system: bundled skills, custom skills,
|
||||
* and invoking skills programmatically.
|
||||
*
|
||||
* Run: npx tsx examples/12-skills.ts
|
||||
*/
|
||||
import {
|
||||
createAgent,
|
||||
registerSkill,
|
||||
getAllSkills,
|
||||
getUserInvocableSkills,
|
||||
getSkill,
|
||||
initBundledSkills,
|
||||
} from '../src/index.js'
|
||||
import type { SkillContentBlock } from '../src/index.js'
|
||||
|
||||
async function main() {
|
||||
console.log('--- Example 12: Skills ---\n')
|
||||
|
||||
// Bundled skills are auto-initialized when creating an Agent,
|
||||
// but you can also init them explicitly:
|
||||
initBundledSkills()
|
||||
|
||||
// List all registered skills
|
||||
const all = getAllSkills()
|
||||
console.log(`Registered skills (${all.length}):`)
|
||||
for (const skill of all) {
|
||||
console.log(` - ${skill.name}: ${skill.description.slice(0, 80)}...`)
|
||||
}
|
||||
|
||||
// Register a custom skill
|
||||
registerSkill({
|
||||
name: 'explain',
|
||||
description: 'Explain a concept or piece of code in simple terms.',
|
||||
aliases: ['eli5'],
|
||||
userInvocable: true,
|
||||
async getPrompt(args): Promise<SkillContentBlock[]> {
|
||||
return [{
|
||||
type: 'text',
|
||||
text: `Explain the following in simple, clear terms that a beginner could understand. Use analogies where helpful.\n\nTopic: ${args || 'Ask the user what they want explained.'}`,
|
||||
}]
|
||||
},
|
||||
})
|
||||
|
||||
console.log(`\nAfter registering custom skill: ${getAllSkills().length} total`)
|
||||
console.log(`User-invocable: ${getUserInvocableSkills().length}`)
|
||||
|
||||
// Get a specific skill
|
||||
const commitSkill = getSkill('commit')
|
||||
if (commitSkill) {
|
||||
const blocks = await commitSkill.getPrompt('', { cwd: process.cwd() })
|
||||
console.log(`\nCommit skill prompt (first 200 chars):`)
|
||||
console.log(blocks[0]?.type === 'text' ? blocks[0].text.slice(0, 200) + '...' : '(non-text)')
|
||||
}
|
||||
|
||||
// Use skills with an agent - the model can invoke them via the Skill tool
|
||||
console.log('\n--- Using skills with an agent ---\n')
|
||||
|
||||
const agent = createAgent({
|
||||
model: process.env.CODEANY_MODEL || 'claude-sonnet-4-6',
|
||||
maxTurns: 5,
|
||||
})
|
||||
|
||||
for await (const event of agent.query(
|
||||
'Use the "explain" skill to explain what git rebase does.',
|
||||
)) {
|
||||
const msg = event as any
|
||||
if (msg.type === 'assistant') {
|
||||
for (const block of msg.message?.content || []) {
|
||||
if (block.type === 'tool_use') {
|
||||
console.log(`[Tool: ${block.name}] ${JSON.stringify(block.input)}`)
|
||||
}
|
||||
if (block.type === 'text' && block.text.trim()) {
|
||||
console.log(block.text)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (msg.type === 'result') {
|
||||
console.log(`\n--- ${msg.subtype} ---`)
|
||||
}
|
||||
}
|
||||
|
||||
await agent.close()
|
||||
}
|
||||
|
||||
main().catch(console.error)
|
||||
@@ -0,0 +1,88 @@
|
||||
/**
|
||||
* Example 13: Hooks
|
||||
*
|
||||
* Shows how to use lifecycle hooks to intercept agent behavior.
|
||||
* Hooks fire at key points: session start/end, before/after tool use,
|
||||
* compaction, etc.
|
||||
*
|
||||
* Run: npx tsx examples/13-hooks.ts
|
||||
*/
|
||||
import { createAgent, createHookRegistry } from '../src/index.js'
|
||||
import type { HookInput } from '../src/index.js'
|
||||
|
||||
async function main() {
|
||||
console.log('--- Example 13: Hooks ---\n')
|
||||
|
||||
// Create a hook registry with custom handlers
|
||||
const registry = createHookRegistry({
|
||||
SessionStart: [{
|
||||
handler: async (input: HookInput) => {
|
||||
console.log(`[Hook] Session started: ${input.sessionId}`)
|
||||
},
|
||||
}],
|
||||
PreToolUse: [{
|
||||
handler: async (input: HookInput) => {
|
||||
console.log(`[Hook] About to use tool: ${input.toolName}`)
|
||||
// You can block a tool by returning { block: true }
|
||||
// return { block: true, message: 'Tool blocked by hook' }
|
||||
},
|
||||
}],
|
||||
PostToolUse: [{
|
||||
handler: async (input: HookInput) => {
|
||||
const output = typeof input.toolOutput === 'string'
|
||||
? input.toolOutput.slice(0, 100)
|
||||
: JSON.stringify(input.toolOutput).slice(0, 100)
|
||||
console.log(`[Hook] Tool ${input.toolName} completed: ${output}...`)
|
||||
},
|
||||
}],
|
||||
PostToolUseFailure: [{
|
||||
handler: async (input: HookInput) => {
|
||||
console.log(`[Hook] Tool ${input.toolName} FAILED: ${input.error}`)
|
||||
},
|
||||
}],
|
||||
Stop: [{
|
||||
handler: async () => {
|
||||
console.log('[Hook] Agent loop completed')
|
||||
},
|
||||
}],
|
||||
SessionEnd: [{
|
||||
handler: async () => {
|
||||
console.log('[Hook] Session ended')
|
||||
},
|
||||
}],
|
||||
})
|
||||
|
||||
// Create agent with hook registry
|
||||
// Note: For direct HookRegistry usage, we pass hooks via the engine config.
|
||||
// The AgentOptions.hooks format also works (see below).
|
||||
const agent = createAgent({
|
||||
model: process.env.CODEANY_MODEL || 'claude-sonnet-4-6',
|
||||
maxTurns: 5,
|
||||
// Alternative: use AgentOptions.hooks format
|
||||
hooks: {
|
||||
PreToolUse: [{
|
||||
hooks: [async (input: any, toolUseId: string) => {
|
||||
console.log(`[AgentHook] PreToolUse: ${input.toolName} (${toolUseId})`)
|
||||
}],
|
||||
}],
|
||||
},
|
||||
})
|
||||
|
||||
for await (const event of agent.query('What files are in the current directory? Be brief.')) {
|
||||
const msg = event as any
|
||||
if (msg.type === 'assistant') {
|
||||
for (const block of msg.message?.content || []) {
|
||||
if (block.type === 'text' && block.text.trim()) {
|
||||
console.log(`\nAssistant: ${block.text.slice(0, 200)}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (msg.type === 'result') {
|
||||
console.log(`\n--- ${msg.subtype} ---`)
|
||||
}
|
||||
}
|
||||
|
||||
await agent.close()
|
||||
}
|
||||
|
||||
main().catch(console.error)
|
||||
@@ -0,0 +1,71 @@
|
||||
/**
|
||||
* Example 14: OpenAI-Compatible Models
|
||||
*
|
||||
* Shows how to use the SDK with OpenAI's API or any OpenAI-compatible
|
||||
* endpoint (e.g., DeepSeek, Qwen, vLLM, Ollama).
|
||||
*
|
||||
* Environment variables:
|
||||
* CODEANY_API_KEY=sk-... # Your OpenAI API key
|
||||
* CODEANY_BASE_URL=https://api.openai.com/v1 # Optional, defaults to OpenAI
|
||||
* CODEANY_API_TYPE=openai-completions # Optional, auto-detected from model name
|
||||
*
|
||||
* Run: npx tsx examples/14-openai-compat.ts
|
||||
*/
|
||||
import { createAgent } from '../src/index.js'
|
||||
|
||||
async function main() {
|
||||
console.log('--- Example 14: OpenAI-Compatible Models ---\n')
|
||||
|
||||
// Option 1: Explicit apiType
|
||||
const agent = createAgent({
|
||||
apiType: 'openai-completions',
|
||||
model: process.env.CODEANY_MODEL || 'gpt-4o',
|
||||
apiKey: process.env.CODEANY_API_KEY,
|
||||
baseURL: process.env.CODEANY_BASE_URL || 'https://api.openai.com/v1',
|
||||
maxTurns: 5,
|
||||
})
|
||||
|
||||
console.log(`API Type: ${agent.getApiType()}`)
|
||||
console.log(`Model: ${process.env.CODEANY_MODEL || 'gpt-4o'}\n`)
|
||||
|
||||
// Option 2: Auto-detected from model name (uncomment to try)
|
||||
// const agent = createAgent({
|
||||
// model: 'gpt-4o', // Auto-detects 'openai-completions'
|
||||
// apiKey: process.env.CODEANY_API_KEY,
|
||||
// })
|
||||
|
||||
// Option 3: DeepSeek example (uncomment to try)
|
||||
// const agent = createAgent({
|
||||
// model: 'deepseek-chat',
|
||||
// apiKey: process.env.CODEANY_API_KEY,
|
||||
// baseURL: 'https://api.deepseek.com/v1',
|
||||
// })
|
||||
|
||||
// Option 4: Via environment variables only
|
||||
// CODEANY_API_TYPE=openai-completions
|
||||
// CODEANY_MODEL=gpt-4o
|
||||
// CODEANY_API_KEY=sk-...
|
||||
// CODEANY_BASE_URL=https://api.openai.com/v1
|
||||
// const agent = createAgent()
|
||||
|
||||
for await (const event of agent.query('What is 2+2? Reply in one sentence.')) {
|
||||
const msg = event as any
|
||||
if (msg.type === 'assistant') {
|
||||
for (const block of msg.message?.content || []) {
|
||||
if (block.type === 'text' && block.text.trim()) {
|
||||
console.log(`Assistant: ${block.text}`)
|
||||
}
|
||||
if (block.type === 'tool_use') {
|
||||
console.log(`[Tool: ${block.name}] ${JSON.stringify(block.input)}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (msg.type === 'result') {
|
||||
console.log(`\n--- ${msg.subtype} (${msg.usage?.input_tokens}+${msg.usage?.output_tokens} tokens) ---`)
|
||||
}
|
||||
}
|
||||
|
||||
await agent.close()
|
||||
}
|
||||
|
||||
main().catch(console.error)
|
||||
Reference in New Issue
Block a user