hone-ai 0.2.0 → 0.9.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 +47 -2
- package/package.json +8 -5
- package/src/agent-client.integration.test.ts +57 -59
- package/src/agent-client.test.ts +27 -27
- package/src/agent-client.ts +109 -77
- package/src/agent.test.ts +16 -16
- package/src/agent.ts +103 -103
- package/src/agents-md-generator.test.ts +360 -0
- package/src/agents-md-generator.ts +900 -0
- package/src/config.test.ts +209 -224
- package/src/config.ts +84 -83
- package/src/errors.test.ts +211 -208
- package/src/errors.ts +107 -101
- package/src/index.integration.test.ts +327 -223
- package/src/index.ts +163 -100
- package/src/integration-test.ts +168 -137
- package/src/logger.test.ts +67 -67
- package/src/logger.ts +8 -8
- package/src/prd-generator.integration.test.ts +50 -50
- package/src/prd-generator.test.ts +66 -25
- package/src/prd-generator.ts +280 -194
- package/src/prds.test.ts +60 -65
- package/src/prds.ts +64 -62
- package/src/prompt.test.ts +154 -155
- package/src/prompt.ts +63 -65
- package/src/run.ts +147 -147
- package/src/status.test.ts +80 -80
- package/src/status.ts +40 -42
- package/src/task-generator.test.ts +93 -66
- package/src/task-generator.ts +125 -112
package/README.md
CHANGED
|
@@ -10,6 +10,8 @@ Transform feature ideas into working code through autonomous development with hu
|
|
|
10
10
|
|
|
11
11
|
```bash
|
|
12
12
|
npm install -g hone-ai
|
|
13
|
+
# or
|
|
14
|
+
bun add -g hone-ai
|
|
13
15
|
```
|
|
14
16
|
|
|
15
17
|
2. **Install an AI agent** ([OpenCode](https://opencode.ai) or [Claude Code](https://docs.anthropic.com/claude/docs/claude-code))
|
|
@@ -41,6 +43,8 @@ That's it! hone will implement the feature, run tests, and commit changes automa
|
|
|
41
43
|
|
|
42
44
|
```bash
|
|
43
45
|
npm install -g hone-ai
|
|
46
|
+
# or
|
|
47
|
+
bun add -g hone-ai
|
|
44
48
|
```
|
|
45
49
|
|
|
46
50
|
### From Source
|
|
@@ -55,11 +59,22 @@ Use `bun src/index.ts` instead of `hone` for all commands.
|
|
|
55
59
|
|
|
56
60
|
### Standalone Binary
|
|
57
61
|
|
|
58
|
-
|
|
62
|
+
Download pre-built binaries from [GitHub Releases](https://github.com/oskarhane/hone-ai/releases).
|
|
63
|
+
|
|
64
|
+
**macOS users**: Remove the quarantine attribute after downloading:
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
unzip hone-v*-macos.zip
|
|
68
|
+
xattr -d com.apple.quarantine hone-v*-macos/hone
|
|
69
|
+
cp hone-v*-macos/hone /usr/local/bin/
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Or build from source:
|
|
59
73
|
|
|
60
74
|
```bash
|
|
61
75
|
bun run build
|
|
62
|
-
cp hone /usr/local/bin/
|
|
76
|
+
cp hone-macos /usr/local/bin/hone # macOS
|
|
77
|
+
cp hone-linux /usr/local/bin/hone # Linux
|
|
63
78
|
```
|
|
64
79
|
|
|
65
80
|
## Common Commands
|
|
@@ -72,6 +87,20 @@ hone prd-to-tasks .plans/prd-feature.md # Generate tasks
|
|
|
72
87
|
hone run .plans/tasks-feature.yml -i 5 # Implement tasks
|
|
73
88
|
```
|
|
74
89
|
|
|
90
|
+
### Reference files and URLs in PRDs
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
# Reference local files in your PRD description
|
|
94
|
+
hone prd "Implement user authentication based on ./docs/auth-spec.md"
|
|
95
|
+
hone prd "Add dashboard following the component in ./src/components/Dashboard.tsx"
|
|
96
|
+
|
|
97
|
+
# Reference URLs for external specifications
|
|
98
|
+
hone prd "Create payment integration using https://stripe.com/docs/api"
|
|
99
|
+
hone prd "Build social login with https://developers.google.com/identity/protocols/oauth2"
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
hone automatically reads referenced files and fetches URL content to inform PRD generation.
|
|
103
|
+
|
|
75
104
|
### Check progress
|
|
76
105
|
|
|
77
106
|
```bash
|
|
@@ -114,6 +143,22 @@ hone breaks feature development into 3 phases:
|
|
|
114
143
|
|
|
115
144
|
Each `hone run` executes multiple iterations of this cycle automatically.
|
|
116
145
|
|
|
146
|
+
### File and URL References in PRDs
|
|
147
|
+
|
|
148
|
+
When creating PRDs, you can reference files and URLs directly in your feature description:
|
|
149
|
+
|
|
150
|
+
**Local files:**
|
|
151
|
+
- `./docs/api-spec.md` - Read project documentation
|
|
152
|
+
- `src/components/Button.tsx` - Analyze existing components
|
|
153
|
+
- `./database/schema.sql` - Review database structure
|
|
154
|
+
|
|
155
|
+
**URLs:**
|
|
156
|
+
- `https://docs.stripe.com/api` - External API documentation
|
|
157
|
+
- `https://www.figma.com/design/123/App` - Design specifications
|
|
158
|
+
- `http://localhost:3000/dashboard` - Reference existing pages
|
|
159
|
+
|
|
160
|
+
The AI agent automatically reads files and fetches web content to generate more accurate PRDs.
|
|
161
|
+
|
|
117
162
|
## File Structure
|
|
118
163
|
|
|
119
164
|
```
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "hone-ai",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
4
4
|
"description": "AI coding agent orchestrator - orchestrate AI agents to implement features based on PRDs",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ai",
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
"claude",
|
|
13
13
|
"opencode"
|
|
14
14
|
],
|
|
15
|
-
"author": "Oskar Hane",
|
|
15
|
+
"author": "Oskar Hane <oskar.hane@gmail.com>",
|
|
16
16
|
"license": "MIT",
|
|
17
17
|
"files": [
|
|
18
18
|
"src/",
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
],
|
|
22
22
|
"repository": {
|
|
23
23
|
"type": "git",
|
|
24
|
-
"url": "https://github.com/oskarhane/hone-ai.git"
|
|
24
|
+
"url": "git+https://github.com/oskarhane/hone-ai.git"
|
|
25
25
|
},
|
|
26
26
|
"bugs": {
|
|
27
27
|
"url": "https://github.com/oskarhane/hone-ai/issues"
|
|
@@ -30,10 +30,13 @@
|
|
|
30
30
|
"module": "src/index.ts",
|
|
31
31
|
"type": "module",
|
|
32
32
|
"bin": {
|
|
33
|
-
"hone": "
|
|
33
|
+
"hone": "src/index.ts"
|
|
34
34
|
},
|
|
35
35
|
"scripts": {
|
|
36
|
-
"build": "bun build
|
|
36
|
+
"build": "bun run build:linux && bun run build:macos",
|
|
37
|
+
"format": "prettier --write \"**/*.ts\"",
|
|
38
|
+
"build:linux": "bun build --compile --minify --sourcemap --target=bun-linux-x64 ./src/index.ts --outfile hone-linux",
|
|
39
|
+
"build:macos": "bun build --compile --minify --sourcemap --target=bun-darwin-arm64 ./src/index.ts --outfile hone-macos",
|
|
37
40
|
"format:yaml": "prettier --write \"**/*.yml\" \"**/*.yaml\"",
|
|
38
41
|
"lint:yaml": "yamllint -c .yamllint.yml **/*.yml **/*.yaml",
|
|
39
42
|
"check:yaml": "bun run lint:yaml && prettier --check \"**/*.yml\" \"**/*.yaml\""
|
|
@@ -1,97 +1,95 @@
|
|
|
1
|
-
import { describe, test, expect } from 'bun:test'
|
|
2
|
-
import { AgentClient, type AgentMessageRequest } from './agent-client'
|
|
1
|
+
import { describe, test, expect } from 'bun:test'
|
|
2
|
+
import { AgentClient, type AgentMessageRequest } from './agent-client'
|
|
3
3
|
|
|
4
4
|
describe('AgentClient Integration', () => {
|
|
5
5
|
// Note: These tests verify the API surface is correct.
|
|
6
6
|
// Actual agent subprocess spawning is tested manually or in CI
|
|
7
7
|
// where real agent binaries (opencode/claude) are available.
|
|
8
|
-
|
|
8
|
+
|
|
9
9
|
describe('API compatibility', () => {
|
|
10
10
|
test('mirrors Anthropic SDK interface', () => {
|
|
11
11
|
const client = new AgentClient({
|
|
12
12
|
agent: 'opencode',
|
|
13
|
-
model: 'claude-sonnet-4-20250514'
|
|
14
|
-
})
|
|
15
|
-
|
|
13
|
+
model: 'claude-sonnet-4-20250514',
|
|
14
|
+
})
|
|
15
|
+
|
|
16
16
|
// Verify the API surface matches Anthropic SDK
|
|
17
|
-
expect(client.messages).toBeDefined()
|
|
18
|
-
expect(typeof client.messages.create).toBe('function')
|
|
19
|
-
|
|
17
|
+
expect(client.messages).toBeDefined()
|
|
18
|
+
expect(typeof client.messages.create).toBe('function')
|
|
19
|
+
|
|
20
20
|
// Verify request structure matches Anthropic SDK
|
|
21
21
|
const validRequest: AgentMessageRequest = {
|
|
22
22
|
model: 'claude-sonnet-4-20250514',
|
|
23
23
|
max_tokens: 4000,
|
|
24
|
-
messages: [
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
};
|
|
29
|
-
|
|
24
|
+
messages: [{ role: 'user', content: 'Test message' }],
|
|
25
|
+
system: 'You are a helpful assistant',
|
|
26
|
+
}
|
|
27
|
+
|
|
30
28
|
// This shouldn't throw TypeScript errors
|
|
31
|
-
expect(validRequest.messages).toHaveLength(1)
|
|
32
|
-
})
|
|
33
|
-
|
|
29
|
+
expect(validRequest.messages).toHaveLength(1)
|
|
30
|
+
})
|
|
31
|
+
|
|
34
32
|
test('supports minimal request format', () => {
|
|
35
33
|
const client = new AgentClient({
|
|
36
34
|
agent: 'claude',
|
|
37
|
-
model: 'claude-sonnet-4-20250514'
|
|
38
|
-
})
|
|
39
|
-
|
|
35
|
+
model: 'claude-sonnet-4-20250514',
|
|
36
|
+
})
|
|
37
|
+
|
|
40
38
|
// Minimal valid request
|
|
41
39
|
const minimalRequest: AgentMessageRequest = {
|
|
42
|
-
messages: [{ role: 'user', content: 'Hello' }]
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
expect(minimalRequest.messages).toHaveLength(1)
|
|
46
|
-
})
|
|
47
|
-
|
|
40
|
+
messages: [{ role: 'user', content: 'Hello' }],
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
expect(minimalRequest.messages).toHaveLength(1)
|
|
44
|
+
})
|
|
45
|
+
|
|
48
46
|
test('supports multi-turn conversations', () => {
|
|
49
47
|
const client = new AgentClient({
|
|
50
48
|
agent: 'opencode',
|
|
51
|
-
model: 'claude-sonnet-4-20250514'
|
|
52
|
-
})
|
|
53
|
-
|
|
49
|
+
model: 'claude-sonnet-4-20250514',
|
|
50
|
+
})
|
|
51
|
+
|
|
54
52
|
const multiTurnRequest: AgentMessageRequest = {
|
|
55
53
|
messages: [
|
|
56
54
|
{ role: 'user', content: 'What is 2+2?' },
|
|
57
55
|
{ role: 'assistant', content: '4' },
|
|
58
|
-
{ role: 'user', content: 'What about 3+3?' }
|
|
59
|
-
]
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
expect(multiTurnRequest.messages).toHaveLength(3)
|
|
63
|
-
})
|
|
64
|
-
})
|
|
65
|
-
|
|
56
|
+
{ role: 'user', content: 'What about 3+3?' },
|
|
57
|
+
],
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
expect(multiTurnRequest.messages).toHaveLength(3)
|
|
61
|
+
})
|
|
62
|
+
})
|
|
63
|
+
|
|
66
64
|
describe('configuration', () => {
|
|
67
65
|
test('accepts opencode agent configuration', () => {
|
|
68
66
|
const client = new AgentClient({
|
|
69
67
|
agent: 'opencode',
|
|
70
68
|
model: 'claude-opus-4-20250514',
|
|
71
|
-
workingDir: '/custom/path'
|
|
72
|
-
})
|
|
73
|
-
|
|
74
|
-
expect(client).toBeDefined()
|
|
75
|
-
expect(client.messages.create).toBeDefined()
|
|
76
|
-
})
|
|
77
|
-
|
|
69
|
+
workingDir: '/custom/path',
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
expect(client).toBeDefined()
|
|
73
|
+
expect(client.messages.create).toBeDefined()
|
|
74
|
+
})
|
|
75
|
+
|
|
78
76
|
test('accepts claude agent configuration', () => {
|
|
79
77
|
const client = new AgentClient({
|
|
80
78
|
agent: 'claude',
|
|
81
|
-
model: 'claude-sonnet-4-20250514'
|
|
82
|
-
})
|
|
83
|
-
|
|
84
|
-
expect(client).toBeDefined()
|
|
85
|
-
expect(client.messages.create).toBeDefined()
|
|
86
|
-
})
|
|
87
|
-
|
|
79
|
+
model: 'claude-sonnet-4-20250514',
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
expect(client).toBeDefined()
|
|
83
|
+
expect(client.messages.create).toBeDefined()
|
|
84
|
+
})
|
|
85
|
+
|
|
88
86
|
test('accepts optional model in config', () => {
|
|
89
87
|
// Model can be omitted if it will be provided per-request
|
|
90
88
|
const client = new AgentClient({
|
|
91
|
-
agent: 'opencode'
|
|
92
|
-
})
|
|
93
|
-
|
|
94
|
-
expect(client).toBeDefined()
|
|
95
|
-
})
|
|
96
|
-
})
|
|
97
|
-
})
|
|
89
|
+
agent: 'opencode',
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
expect(client).toBeDefined()
|
|
93
|
+
})
|
|
94
|
+
})
|
|
95
|
+
})
|
package/src/agent-client.test.ts
CHANGED
|
@@ -1,43 +1,43 @@
|
|
|
1
|
-
import { describe, test, expect } from 'bun:test'
|
|
2
|
-
import { AgentClient } from './agent-client'
|
|
1
|
+
import { describe, test, expect } from 'bun:test'
|
|
2
|
+
import { AgentClient } from './agent-client'
|
|
3
3
|
|
|
4
4
|
describe('AgentClient', () => {
|
|
5
5
|
describe('constructor', () => {
|
|
6
6
|
test('creates client with minimal config', () => {
|
|
7
7
|
const client = new AgentClient({
|
|
8
8
|
agent: 'opencode',
|
|
9
|
-
model: 'claude-sonnet-4-20250514'
|
|
10
|
-
})
|
|
11
|
-
|
|
12
|
-
expect(client).toBeDefined()
|
|
13
|
-
expect(client.messages).toBeDefined()
|
|
14
|
-
expect(typeof client.messages.create).toBe('function')
|
|
15
|
-
})
|
|
16
|
-
|
|
9
|
+
model: 'claude-sonnet-4-20250514',
|
|
10
|
+
})
|
|
11
|
+
|
|
12
|
+
expect(client).toBeDefined()
|
|
13
|
+
expect(client.messages).toBeDefined()
|
|
14
|
+
expect(typeof client.messages.create).toBe('function')
|
|
15
|
+
})
|
|
16
|
+
|
|
17
17
|
test('creates client with full config', () => {
|
|
18
18
|
const client = new AgentClient({
|
|
19
19
|
agent: 'claude',
|
|
20
20
|
model: 'claude-opus-4-20250514',
|
|
21
|
-
workingDir: '/custom/path'
|
|
22
|
-
})
|
|
23
|
-
|
|
24
|
-
expect(client).toBeDefined()
|
|
25
|
-
expect(client.messages).toBeDefined()
|
|
26
|
-
})
|
|
27
|
-
})
|
|
28
|
-
|
|
21
|
+
workingDir: '/custom/path',
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
expect(client).toBeDefined()
|
|
25
|
+
expect(client.messages).toBeDefined()
|
|
26
|
+
})
|
|
27
|
+
})
|
|
28
|
+
|
|
29
29
|
describe('messages API', () => {
|
|
30
30
|
test('provides messages.create method', () => {
|
|
31
31
|
const client = new AgentClient({
|
|
32
32
|
agent: 'opencode',
|
|
33
|
-
model: 'claude-sonnet-4-20250514'
|
|
34
|
-
})
|
|
35
|
-
|
|
36
|
-
expect(client.messages.create).toBeDefined()
|
|
37
|
-
expect(typeof client.messages.create).toBe('function')
|
|
38
|
-
})
|
|
39
|
-
})
|
|
40
|
-
|
|
33
|
+
model: 'claude-sonnet-4-20250514',
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
expect(client.messages.create).toBeDefined()
|
|
37
|
+
expect(typeof client.messages.create).toBe('function')
|
|
38
|
+
})
|
|
39
|
+
})
|
|
40
|
+
|
|
41
41
|
// Note: Integration tests with actual agent spawning would test:
|
|
42
42
|
// - Prompt construction with system messages
|
|
43
43
|
// - Model parameter passing to spawnAgent
|
|
@@ -45,4 +45,4 @@ describe('AgentClient', () => {
|
|
|
45
45
|
// - Error handling for non-zero exit codes
|
|
46
46
|
// - Retry logic with retryWithBackoff
|
|
47
47
|
// These are covered in integration tests that mock child_process
|
|
48
|
-
})
|
|
48
|
+
})
|
package/src/agent-client.ts
CHANGED
|
@@ -3,26 +3,32 @@
|
|
|
3
3
|
* Mirrors Anthropic SDK API but routes through agent subprocess spawning
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import { spawnAgent } from './agent'
|
|
7
|
-
import {
|
|
8
|
-
|
|
9
|
-
|
|
6
|
+
import { spawnAgent } from './agent'
|
|
7
|
+
import {
|
|
8
|
+
retryWithBackoff,
|
|
9
|
+
parseAgentError,
|
|
10
|
+
ErrorMessages,
|
|
11
|
+
exitWithError,
|
|
12
|
+
isNetworkError,
|
|
13
|
+
} from './errors'
|
|
14
|
+
import type { AgentType } from './config'
|
|
15
|
+
import { logVerbose, logVerboseError } from './logger'
|
|
10
16
|
|
|
11
17
|
export interface AgentClientConfig {
|
|
12
|
-
agent: AgentType
|
|
13
|
-
model?: string
|
|
14
|
-
workingDir?: string
|
|
18
|
+
agent: AgentType
|
|
19
|
+
model?: string
|
|
20
|
+
workingDir?: string
|
|
15
21
|
}
|
|
16
22
|
|
|
17
23
|
export interface AgentMessageRequest {
|
|
18
|
-
model?: string
|
|
19
|
-
max_tokens?: number
|
|
20
|
-
messages: Array<{ role: 'user' | 'assistant'; content: string }
|
|
21
|
-
system?: string
|
|
24
|
+
model?: string
|
|
25
|
+
max_tokens?: number
|
|
26
|
+
messages: Array<{ role: 'user' | 'assistant'; content: string }>
|
|
27
|
+
system?: string
|
|
22
28
|
}
|
|
23
29
|
|
|
24
30
|
export interface AgentMessageResponse {
|
|
25
|
-
content: Array<{ type: 'text'; text: string }
|
|
31
|
+
content: Array<{ type: 'text'; text: string }>
|
|
26
32
|
}
|
|
27
33
|
|
|
28
34
|
/**
|
|
@@ -30,12 +36,12 @@ export interface AgentMessageResponse {
|
|
|
30
36
|
* Routes message creation through agent subprocess calls
|
|
31
37
|
*/
|
|
32
38
|
export class AgentClient {
|
|
33
|
-
private config: AgentClientConfig
|
|
34
|
-
|
|
39
|
+
private config: AgentClientConfig
|
|
40
|
+
|
|
35
41
|
constructor(config: AgentClientConfig) {
|
|
36
|
-
this.config = config
|
|
42
|
+
this.config = config
|
|
37
43
|
}
|
|
38
|
-
|
|
44
|
+
|
|
39
45
|
/**
|
|
40
46
|
* Messages API with create method
|
|
41
47
|
* Mirrors Anthropic client.messages.create() interface
|
|
@@ -44,19 +50,23 @@ export class AgentClient {
|
|
|
44
50
|
return {
|
|
45
51
|
create: async (request: AgentMessageRequest): Promise<AgentMessageResponse> => {
|
|
46
52
|
// Use model from request, fall back to client config
|
|
47
|
-
const model = request.model || this.config.model
|
|
48
|
-
|
|
53
|
+
const model = request.model || this.config.model
|
|
54
|
+
|
|
49
55
|
// Log agent request initiation
|
|
50
|
-
logVerbose(
|
|
51
|
-
|
|
56
|
+
logVerbose(
|
|
57
|
+
`[AgentClient] Initiating request to ${this.config.agent}${model ? ` with model ${model}` : ''}`
|
|
58
|
+
)
|
|
59
|
+
|
|
52
60
|
// Construct prompt from request
|
|
53
|
-
const prompt = constructPromptFromRequest(request)
|
|
54
|
-
|
|
61
|
+
const prompt = constructPromptFromRequest(request)
|
|
62
|
+
|
|
55
63
|
// Log request details
|
|
56
|
-
const messageCount = request.messages.length
|
|
57
|
-
const hasSystem = !!request.system
|
|
58
|
-
logVerbose(
|
|
59
|
-
|
|
64
|
+
const messageCount = request.messages.length
|
|
65
|
+
const hasSystem = !!request.system
|
|
66
|
+
logVerbose(
|
|
67
|
+
`[AgentClient] Request: ${messageCount} message(s)${hasSystem ? ' + system prompt' : ''}`
|
|
68
|
+
)
|
|
69
|
+
|
|
60
70
|
// Execute with retry logic for network errors only
|
|
61
71
|
try {
|
|
62
72
|
const result = await retryWithBackoff(async () => {
|
|
@@ -66,63 +76,83 @@ export class AgentClient {
|
|
|
66
76
|
workingDir: this.config.workingDir || process.cwd(),
|
|
67
77
|
model,
|
|
68
78
|
silent: true,
|
|
69
|
-
timeout: 120000 // 2 minute timeout for PRD questions
|
|
70
|
-
})
|
|
71
|
-
|
|
79
|
+
timeout: 120000, // 2 minute timeout for PRD questions
|
|
80
|
+
})
|
|
81
|
+
|
|
72
82
|
// Only retry network errors, throw immediately for other failures
|
|
73
83
|
if (spawnResult.exitCode !== 0) {
|
|
74
84
|
// Parse error type from stderr
|
|
75
|
-
const errorInfo = parseAgentError(spawnResult.stderr, spawnResult.exitCode)
|
|
76
|
-
logVerboseError(
|
|
77
|
-
|
|
85
|
+
const errorInfo = parseAgentError(spawnResult.stderr, spawnResult.exitCode)
|
|
86
|
+
logVerboseError(
|
|
87
|
+
`[AgentClient] Agent exited with code ${spawnResult.exitCode}, error type: ${errorInfo.type}`
|
|
88
|
+
)
|
|
89
|
+
|
|
78
90
|
// Handle specific error types with user-friendly messages
|
|
79
91
|
if (errorInfo.type === 'model_unavailable') {
|
|
80
|
-
const { message, details } = ErrorMessages.MODEL_UNAVAILABLE(
|
|
81
|
-
|
|
92
|
+
const { message, details } = ErrorMessages.MODEL_UNAVAILABLE(
|
|
93
|
+
model || 'unknown',
|
|
94
|
+
this.config.agent
|
|
95
|
+
)
|
|
96
|
+
exitWithError(message, details)
|
|
82
97
|
} else if (errorInfo.type === 'rate_limit') {
|
|
83
|
-
const { message, details } = ErrorMessages.RATE_LIMIT_ERROR(
|
|
84
|
-
|
|
98
|
+
const { message, details } = ErrorMessages.RATE_LIMIT_ERROR(
|
|
99
|
+
this.config.agent,
|
|
100
|
+
errorInfo.retryAfter
|
|
101
|
+
)
|
|
102
|
+
exitWithError(message, details)
|
|
85
103
|
} else if (errorInfo.type === 'spawn_failed') {
|
|
86
|
-
const { message, details } = ErrorMessages.AGENT_SPAWN_FAILED(
|
|
87
|
-
|
|
104
|
+
const { message, details } = ErrorMessages.AGENT_SPAWN_FAILED(
|
|
105
|
+
this.config.agent,
|
|
106
|
+
spawnResult.stderr
|
|
107
|
+
)
|
|
108
|
+
exitWithError(message, details)
|
|
88
109
|
} else if (errorInfo.type === 'timeout') {
|
|
89
|
-
const timeoutMs = 120000
|
|
90
|
-
const { message, details } = ErrorMessages.AGENT_TIMEOUT(
|
|
91
|
-
|
|
110
|
+
const timeoutMs = 120000 // Default timeout
|
|
111
|
+
const { message, details } = ErrorMessages.AGENT_TIMEOUT(
|
|
112
|
+
this.config.agent,
|
|
113
|
+
timeoutMs
|
|
114
|
+
)
|
|
115
|
+
exitWithError(message, details)
|
|
92
116
|
}
|
|
93
|
-
|
|
117
|
+
|
|
94
118
|
// For network errors, allow retry
|
|
95
119
|
if (errorInfo.retryable) {
|
|
96
|
-
logVerbose(`[AgentClient] Network error detected, will retry`)
|
|
97
|
-
throw new Error(
|
|
120
|
+
logVerbose(`[AgentClient] Network error detected, will retry`)
|
|
121
|
+
throw new Error(
|
|
122
|
+
`Agent exited with code ${spawnResult.exitCode}: ${spawnResult.stderr}`
|
|
123
|
+
)
|
|
98
124
|
}
|
|
99
|
-
|
|
125
|
+
|
|
100
126
|
// For other unknown errors, provide generic agent error message
|
|
101
|
-
const { message, details } = ErrorMessages.AGENT_ERROR(
|
|
102
|
-
|
|
127
|
+
const { message, details } = ErrorMessages.AGENT_ERROR(
|
|
128
|
+
this.config.agent,
|
|
129
|
+
spawnResult.exitCode,
|
|
130
|
+
spawnResult.stderr
|
|
131
|
+
)
|
|
132
|
+
exitWithError(message, details)
|
|
103
133
|
}
|
|
104
|
-
|
|
105
|
-
logVerbose(`[AgentClient] Request completed successfully`)
|
|
106
|
-
return spawnResult
|
|
107
|
-
})
|
|
108
|
-
|
|
134
|
+
|
|
135
|
+
logVerbose(`[AgentClient] Request completed successfully`)
|
|
136
|
+
return spawnResult
|
|
137
|
+
})
|
|
138
|
+
|
|
109
139
|
// Log response details
|
|
110
|
-
const responseLength = result.stdout.trim().length
|
|
111
|
-
logVerbose(`[AgentClient] Response: ${responseLength} characters`)
|
|
112
|
-
|
|
140
|
+
const responseLength = result.stdout.trim().length
|
|
141
|
+
logVerbose(`[AgentClient] Response: ${responseLength} characters`)
|
|
142
|
+
|
|
113
143
|
// Parse response into Anthropic-compatible format
|
|
114
|
-
return parseAgentResponse(result.stdout)
|
|
144
|
+
return parseAgentResponse(result.stdout)
|
|
115
145
|
} catch (error) {
|
|
116
146
|
// Network errors that exhausted retries
|
|
117
147
|
if (error instanceof Error && error.message.includes('Agent exited with code')) {
|
|
118
|
-
logVerboseError(`[AgentClient] All retry attempts exhausted`)
|
|
119
|
-
const { message, details } = ErrorMessages.NETWORK_ERROR_FINAL(error)
|
|
120
|
-
exitWithError(message, details)
|
|
148
|
+
logVerboseError(`[AgentClient] All retry attempts exhausted`)
|
|
149
|
+
const { message, details } = ErrorMessages.NETWORK_ERROR_FINAL(error)
|
|
150
|
+
exitWithError(message, details)
|
|
121
151
|
}
|
|
122
|
-
throw error
|
|
152
|
+
throw error
|
|
123
153
|
}
|
|
124
|
-
}
|
|
125
|
-
}
|
|
154
|
+
},
|
|
155
|
+
}
|
|
126
156
|
}
|
|
127
157
|
}
|
|
128
158
|
|
|
@@ -131,26 +161,26 @@ export class AgentClient {
|
|
|
131
161
|
* Converts Anthropic message format to agent prompt string
|
|
132
162
|
*/
|
|
133
163
|
function constructPromptFromRequest(request: AgentMessageRequest): string {
|
|
134
|
-
const parts: string[] = []
|
|
135
|
-
|
|
164
|
+
const parts: string[] = []
|
|
165
|
+
|
|
136
166
|
// Add system prompt if present
|
|
137
167
|
if (request.system) {
|
|
138
|
-
parts.push('# System')
|
|
139
|
-
parts.push(request.system)
|
|
140
|
-
parts.push('')
|
|
168
|
+
parts.push('# System')
|
|
169
|
+
parts.push(request.system)
|
|
170
|
+
parts.push('')
|
|
141
171
|
}
|
|
142
|
-
|
|
172
|
+
|
|
143
173
|
// Add conversation messages
|
|
144
174
|
for (const message of request.messages) {
|
|
145
175
|
if (message.role === 'user') {
|
|
146
|
-
parts.push(message.content)
|
|
176
|
+
parts.push(message.content)
|
|
147
177
|
} else {
|
|
148
178
|
// Handle assistant messages (for multi-turn conversations)
|
|
149
|
-
parts.push(`Previous response: ${message.content}`)
|
|
179
|
+
parts.push(`Previous response: ${message.content}`)
|
|
150
180
|
}
|
|
151
181
|
}
|
|
152
|
-
|
|
153
|
-
return parts.join('\n')
|
|
182
|
+
|
|
183
|
+
return parts.join('\n')
|
|
154
184
|
}
|
|
155
185
|
|
|
156
186
|
/**
|
|
@@ -158,9 +188,11 @@ function constructPromptFromRequest(request: AgentMessageRequest): string {
|
|
|
158
188
|
*/
|
|
159
189
|
function parseAgentResponse(stdout: string): AgentMessageResponse {
|
|
160
190
|
return {
|
|
161
|
-
content: [
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
191
|
+
content: [
|
|
192
|
+
{
|
|
193
|
+
type: 'text',
|
|
194
|
+
text: stdout.trim(),
|
|
195
|
+
},
|
|
196
|
+
],
|
|
197
|
+
}
|
|
166
198
|
}
|
package/src/agent.test.ts
CHANGED
|
@@ -1,26 +1,26 @@
|
|
|
1
|
-
import { describe, test, expect } from 'bun:test'
|
|
2
|
-
import { isAgentAvailable } from './agent'
|
|
1
|
+
import { describe, test, expect } from 'bun:test'
|
|
2
|
+
import { isAgentAvailable } from './agent'
|
|
3
3
|
|
|
4
4
|
describe('Agent utilities', () => {
|
|
5
5
|
describe('isAgentAvailable', () => {
|
|
6
6
|
test('should check if claude is available', async () => {
|
|
7
|
-
const available = await isAgentAvailable('claude')
|
|
8
|
-
expect(typeof available).toBe('boolean')
|
|
9
|
-
})
|
|
10
|
-
|
|
7
|
+
const available = await isAgentAvailable('claude')
|
|
8
|
+
expect(typeof available).toBe('boolean')
|
|
9
|
+
})
|
|
10
|
+
|
|
11
11
|
test('should check if opencode is available', async () => {
|
|
12
|
-
const available = await isAgentAvailable('opencode')
|
|
13
|
-
expect(typeof available).toBe('boolean')
|
|
14
|
-
})
|
|
15
|
-
})
|
|
16
|
-
|
|
12
|
+
const available = await isAgentAvailable('opencode')
|
|
13
|
+
expect(typeof available).toBe('boolean')
|
|
14
|
+
})
|
|
15
|
+
})
|
|
16
|
+
|
|
17
17
|
describe('spawnAgent', () => {
|
|
18
18
|
// Note: spawnAgent tests require actual agent binaries to be installed
|
|
19
19
|
// and would spawn interactive processes. Testing is done manually or via
|
|
20
20
|
// integration tests with mocked child_process.
|
|
21
21
|
test('should export spawnAgent function', () => {
|
|
22
|
-
const { spawnAgent } = require('./agent')
|
|
23
|
-
expect(typeof spawnAgent).toBe('function')
|
|
24
|
-
})
|
|
25
|
-
})
|
|
26
|
-
})
|
|
22
|
+
const { spawnAgent } = require('./agent')
|
|
23
|
+
expect(typeof spawnAgent).toBe('function')
|
|
24
|
+
})
|
|
25
|
+
})
|
|
26
|
+
})
|