@northflare/runner 0.0.1
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/DEBUG_LOGGING.md +60 -0
- package/LICENSE +21 -0
- package/MIGRATION_PLAN.md +52 -0
- package/README.md +220 -0
- package/SDK_IMPLEMENTATION_GUIDE.md +1036 -0
- package/bin/northflare-runner +367 -0
- package/coverage/base.css +224 -0
- package/coverage/block-navigation.js +87 -0
- package/coverage/coverage-final.json +12 -0
- package/coverage/favicon.png +0 -0
- package/coverage/index.html +176 -0
- package/coverage/lib/index.html +116 -0
- package/coverage/lib/preload-script.js.html +964 -0
- package/coverage/prettify.css +1 -0
- package/coverage/prettify.js +2 -0
- package/coverage/sort-arrow-sprite.png +0 -0
- package/coverage/sorter.js +196 -0
- package/coverage/src/collections/index.html +116 -0
- package/coverage/src/collections/runner-messages.ts.html +312 -0
- package/coverage/src/components/claude-manager.ts.html +1290 -0
- package/coverage/src/components/index.html +146 -0
- package/coverage/src/components/message-handler.ts.html +730 -0
- package/coverage/src/components/repository-manager.ts.html +841 -0
- package/coverage/src/index.html +131 -0
- package/coverage/src/index.ts.html +448 -0
- package/coverage/src/runner.ts.html +1239 -0
- package/coverage/src/utils/config.ts.html +780 -0
- package/coverage/src/utils/console.ts.html +121 -0
- package/coverage/src/utils/index.html +161 -0
- package/coverage/src/utils/logger.ts.html +475 -0
- package/coverage/src/utils/status-line.ts.html +445 -0
- package/dist/collections/runner-messages.d.ts +52 -0
- package/dist/collections/runner-messages.d.ts.map +1 -0
- package/dist/collections/runner-messages.js +161 -0
- package/dist/collections/runner-messages.js.map +1 -0
- package/dist/components/claude-manager.d.ts +39 -0
- package/dist/components/claude-manager.d.ts.map +1 -0
- package/dist/components/claude-manager.js +783 -0
- package/dist/components/claude-manager.js.map +1 -0
- package/dist/components/claude-sdk-manager.d.ts +47 -0
- package/dist/components/claude-sdk-manager.d.ts.map +1 -0
- package/dist/components/claude-sdk-manager.js +1088 -0
- package/dist/components/claude-sdk-manager.js.map +1 -0
- package/dist/components/enhanced-repository-manager.d.ts +134 -0
- package/dist/components/enhanced-repository-manager.d.ts.map +1 -0
- package/dist/components/enhanced-repository-manager.js +602 -0
- package/dist/components/enhanced-repository-manager.js.map +1 -0
- package/dist/components/message-handler-sse.d.ts +46 -0
- package/dist/components/message-handler-sse.d.ts.map +1 -0
- package/dist/components/message-handler-sse.js +734 -0
- package/dist/components/message-handler-sse.js.map +1 -0
- package/dist/components/message-handler.d.ts +35 -0
- package/dist/components/message-handler.d.ts.map +1 -0
- package/dist/components/message-handler.js +689 -0
- package/dist/components/message-handler.js.map +1 -0
- package/dist/components/repository-manager.d.ts +51 -0
- package/dist/components/repository-manager.d.ts.map +1 -0
- package/dist/components/repository-manager.js +295 -0
- package/dist/components/repository-manager.js.map +1 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +166 -0
- package/dist/index.js.map +1 -0
- package/dist/runner-sse.d.ts +57 -0
- package/dist/runner-sse.d.ts.map +1 -0
- package/dist/runner-sse.js +698 -0
- package/dist/runner-sse.js.map +1 -0
- package/dist/runner.d.ts +51 -0
- package/dist/runner.d.ts.map +1 -0
- package/dist/runner.js +530 -0
- package/dist/runner.js.map +1 -0
- package/dist/services/RunnerAPIClient.d.ts +30 -0
- package/dist/services/RunnerAPIClient.d.ts.map +1 -0
- package/dist/services/RunnerAPIClient.js +112 -0
- package/dist/services/RunnerAPIClient.js.map +1 -0
- package/dist/services/SSEClient.d.ts +60 -0
- package/dist/services/SSEClient.d.ts.map +1 -0
- package/dist/services/SSEClient.js +204 -0
- package/dist/services/SSEClient.js.map +1 -0
- package/dist/types/claude.d.ts +45 -0
- package/dist/types/claude.d.ts.map +1 -0
- package/dist/types/claude.js +6 -0
- package/dist/types/claude.js.map +1 -0
- package/dist/types/index.d.ts +47 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +23 -0
- package/dist/types/index.js.map +1 -0
- package/dist/types/messages.d.ts +31 -0
- package/dist/types/messages.d.ts.map +1 -0
- package/dist/types/messages.js +6 -0
- package/dist/types/messages.js.map +1 -0
- package/dist/types/runner-interface.d.ts +24 -0
- package/dist/types/runner-interface.d.ts.map +1 -0
- package/dist/types/runner-interface.js +6 -0
- package/dist/types/runner-interface.js.map +1 -0
- package/dist/utils/StateManager.d.ts +52 -0
- package/dist/utils/StateManager.d.ts.map +1 -0
- package/dist/utils/StateManager.js +162 -0
- package/dist/utils/StateManager.js.map +1 -0
- package/dist/utils/config.d.ts +41 -0
- package/dist/utils/config.d.ts.map +1 -0
- package/dist/utils/config.js +250 -0
- package/dist/utils/config.js.map +1 -0
- package/dist/utils/console.d.ts +11 -0
- package/dist/utils/console.d.ts.map +1 -0
- package/dist/utils/console.js +15 -0
- package/dist/utils/console.js.map +1 -0
- package/dist/utils/expand-env.d.ts +2 -0
- package/dist/utils/expand-env.d.ts.map +1 -0
- package/dist/utils/expand-env.js +20 -0
- package/dist/utils/expand-env.js.map +1 -0
- package/dist/utils/logger.d.ts +9 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +108 -0
- package/dist/utils/logger.js.map +1 -0
- package/dist/utils/status-line.d.ts +37 -0
- package/dist/utils/status-line.d.ts.map +1 -0
- package/dist/utils/status-line.js +113 -0
- package/dist/utils/status-line.js.map +1 -0
- package/docs/claude-manager.md +91 -0
- package/exceptions.log +22 -0
- package/lib/preload-script.js +293 -0
- package/package.json +55 -0
- package/rejections.log +63 -0
- package/runner.log +488 -0
- package/src/components/claude-sdk-manager.ts +1354 -0
- package/src/components/enhanced-repository-manager.ts +823 -0
- package/src/components/message-handler-sse.ts +1011 -0
- package/src/components/repository-manager.ts +337 -0
- package/src/index.ts +166 -0
- package/src/runner-sse.ts +847 -0
- package/src/services/RunnerAPIClient.ts +135 -0
- package/src/services/SSEClient.ts +258 -0
- package/src/types/claude.ts +55 -0
- package/src/types/computer-name.d.ts +4 -0
- package/src/types/index.ts +63 -0
- package/src/types/messages.ts +39 -0
- package/src/types/runner-interface.ts +34 -0
- package/src/utils/StateManager.ts +187 -0
- package/src/utils/codex-sdk.js +448 -0
- package/src/utils/config.ts +315 -0
- package/src/utils/console.ts +13 -0
- package/src/utils/expand-env.ts +22 -0
- package/src/utils/logger.ts +131 -0
- package/src/utils/sdk-demo.js +34 -0
- package/src/utils/status-line.ts +121 -0
- package/test-debug.sh +26 -0
- package/tests/retry-strategies.test.ts +410 -0
- package/tests/sdk-integration.test.ts +329 -0
- package/tests/sdk-streaming.test.ts +1180 -0
- package/tests/setup.ts +5 -0
- package/tests/test-claude-manager.ts +120 -0
- package/tsconfig.json +36 -0
- package/vitest.config.ts +27 -0
|
@@ -0,0 +1,1036 @@
|
|
|
1
|
+
# SDK-Native Implementation Guide
|
|
2
|
+
|
|
3
|
+
This guide provides comprehensive documentation for implementing and maintaining the SDK-native patterns in the Northflare Runner with practical examples, best practices, and troubleshooting guidance.
|
|
4
|
+
|
|
5
|
+
## Table of Contents
|
|
6
|
+
|
|
7
|
+
1. [Overview](#overview)
|
|
8
|
+
2. [Core Concepts](#core-concepts)
|
|
9
|
+
3. [Implementation Patterns](#implementation-patterns)
|
|
10
|
+
4. [Best Practices](#best-practices)
|
|
11
|
+
5. [Common Use Cases](#common-use-cases)
|
|
12
|
+
6. [Error Handling](#error-handling)
|
|
13
|
+
7. [Performance Optimization](#performance-optimization)
|
|
14
|
+
8. [Troubleshooting](#troubleshooting)
|
|
15
|
+
9. [Testing Patterns](#testing-patterns)
|
|
16
|
+
|
|
17
|
+
## Overview
|
|
18
|
+
|
|
19
|
+
The SDK-native implementation provides:
|
|
20
|
+
|
|
21
|
+
- **40% reduction in configuration complexity**
|
|
22
|
+
- **30-50% improvement in message handling performance**
|
|
23
|
+
- **40% reduction in custom error handling code**
|
|
24
|
+
- **Native session management** eliminating custom session handling
|
|
25
|
+
- **Built-in resource management** with automatic cleanup
|
|
26
|
+
- **Better API compatibility** and future-proofing
|
|
27
|
+
|
|
28
|
+
### SDK Builder Pattern
|
|
29
|
+
|
|
30
|
+
```typescript
|
|
31
|
+
// Clean builder chain for conversation creation
|
|
32
|
+
const conversation = claude()
|
|
33
|
+
.withExecutable(cliPath)
|
|
34
|
+
.withEnv({ ANTHROPIC_API_KEY: apiKey })
|
|
35
|
+
.withModel("sonnet")
|
|
36
|
+
.inDirectory(workspacePath)
|
|
37
|
+
.withSessionId(sessionId) // Native session resuming
|
|
38
|
+
.appendSystemPrompt(instructions)
|
|
39
|
+
.denyTools("Write", "Edit")
|
|
40
|
+
.withMCP(servers)
|
|
41
|
+
.skipPermissions()
|
|
42
|
+
.onProcessComplete(handleCompletion)
|
|
43
|
+
.asConversation();
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Core Concepts
|
|
47
|
+
|
|
48
|
+
### Builder Pattern Architecture
|
|
49
|
+
|
|
50
|
+
The SDK-native approach uses a fluent builder pattern that provides:
|
|
51
|
+
|
|
52
|
+
1. **Method Chaining**: Each configuration method returns the builder for chaining
|
|
53
|
+
2. **Type Safety**: TypeScript ensures correct configuration at compile time
|
|
54
|
+
3. **Readability**: Clear, declarative configuration style
|
|
55
|
+
4. **Flexibility**: Easy to extend and modify configurations
|
|
56
|
+
|
|
57
|
+
### Session Management
|
|
58
|
+
|
|
59
|
+
#### Native Session Handling
|
|
60
|
+
|
|
61
|
+
```typescript
|
|
62
|
+
// Resume existing session
|
|
63
|
+
const conversation = claude()
|
|
64
|
+
.withExecutable("/path/to/cli.js")
|
|
65
|
+
.withSessionId("session_abc123") // Native resume
|
|
66
|
+
.asConversation();
|
|
67
|
+
|
|
68
|
+
// Session ID callback for new sessions
|
|
69
|
+
conversation.onSessionId((sessionId) => {
|
|
70
|
+
console.log("New session created:", sessionId);
|
|
71
|
+
// Store sessionId for future resume operations
|
|
72
|
+
});
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### Message Handling
|
|
76
|
+
|
|
77
|
+
#### Direct Message Sending
|
|
78
|
+
|
|
79
|
+
```typescript
|
|
80
|
+
// No custom queuing required
|
|
81
|
+
conversation.send({ type: "text", text: "Hello Claude!" });
|
|
82
|
+
|
|
83
|
+
// Direct streaming without wrappers
|
|
84
|
+
conversation.stream(async (message, sessionId) => {
|
|
85
|
+
await processMessage(message);
|
|
86
|
+
});
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## Implementation Patterns
|
|
90
|
+
|
|
91
|
+
### 1. Basic Conversation Creation
|
|
92
|
+
|
|
93
|
+
```typescript
|
|
94
|
+
async function createBasicConversation(
|
|
95
|
+
workspacePath: string,
|
|
96
|
+
apiKey: string
|
|
97
|
+
): Promise<ConversationType> {
|
|
98
|
+
const conversation = claude()
|
|
99
|
+
.withExecutable(require.resolve("@anthropic-ai/claude-code/cli.js"))
|
|
100
|
+
.withEnv({ ANTHROPIC_API_KEY: apiKey })
|
|
101
|
+
.withModel("sonnet")
|
|
102
|
+
.inDirectory(workspacePath)
|
|
103
|
+
.skipPermissions()
|
|
104
|
+
.asConversation();
|
|
105
|
+
|
|
106
|
+
return conversation;
|
|
107
|
+
}
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
### 2. Advanced Configuration with MCP Servers
|
|
111
|
+
|
|
112
|
+
```typescript
|
|
113
|
+
async function createAdvancedConversation(config: {
|
|
114
|
+
workspacePath: string;
|
|
115
|
+
apiKey: string;
|
|
116
|
+
sessionId?: string;
|
|
117
|
+
mcpServers?: Record<string, any>;
|
|
118
|
+
systemPrompt?: string;
|
|
119
|
+
restrictedTools?: string[];
|
|
120
|
+
}): Promise<ConversationType> {
|
|
121
|
+
let builder = claude()
|
|
122
|
+
.withExecutable(require.resolve("@anthropic-ai/claude-code/cli.js"))
|
|
123
|
+
.withEnv({
|
|
124
|
+
ANTHROPIC_API_KEY: config.apiKey,
|
|
125
|
+
GITHUB_TOKEN: process.env.GITHUB_TOKEN || "",
|
|
126
|
+
})
|
|
127
|
+
.withModel("sonnet")
|
|
128
|
+
.inDirectory(config.workspacePath)
|
|
129
|
+
.skipPermissions();
|
|
130
|
+
|
|
131
|
+
// Conditional configuration
|
|
132
|
+
if (config.sessionId) {
|
|
133
|
+
builder = builder.withSessionId(config.sessionId);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (config.systemPrompt) {
|
|
137
|
+
builder = builder.appendSystemPrompt(config.systemPrompt);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (config.mcpServers) {
|
|
141
|
+
builder = builder.withMCP(config.mcpServers);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (config.restrictedTools?.length) {
|
|
145
|
+
builder = builder.denyTools(...config.restrictedTools);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Error handling
|
|
149
|
+
builder = builder.onProcessComplete((code, error) => {
|
|
150
|
+
if (error || code !== 0) {
|
|
151
|
+
console.error("Conversation process error:", {
|
|
152
|
+
code,
|
|
153
|
+
error: error?.message,
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
return builder.asConversation();
|
|
159
|
+
}
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
### 3. Session Management Pattern
|
|
163
|
+
|
|
164
|
+
```typescript
|
|
165
|
+
class SessionManager {
|
|
166
|
+
private activeSessions = new Map<string, string>(); // conversationId -> sessionId
|
|
167
|
+
|
|
168
|
+
async createOrResumeConversation(
|
|
169
|
+
conversationId: string,
|
|
170
|
+
config: ConversationConfig
|
|
171
|
+
): Promise<ConversationType> {
|
|
172
|
+
const existingSessionId = this.activeSessions.get(conversationId);
|
|
173
|
+
|
|
174
|
+
let builder = claude()
|
|
175
|
+
.withExecutable("/path/to/cli.js")
|
|
176
|
+
.withModel("sonnet")
|
|
177
|
+
.inDirectory(config.workspacePath);
|
|
178
|
+
|
|
179
|
+
// Resume existing session if available
|
|
180
|
+
if (existingSessionId) {
|
|
181
|
+
builder = builder.withSessionId(existingSessionId);
|
|
182
|
+
console.log(
|
|
183
|
+
`Resuming session ${existingSessionId} for conversation ${conversationId}`
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const conversation = builder.asConversation();
|
|
188
|
+
|
|
189
|
+
// Track new session IDs
|
|
190
|
+
conversation.onSessionId((sessionId) => {
|
|
191
|
+
this.activeSessions.set(conversationId, sessionId);
|
|
192
|
+
console.log(
|
|
193
|
+
`Session ${sessionId} created for conversation ${conversationId}`
|
|
194
|
+
);
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
return conversation;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
async endConversation(conversationId: string): Promise<void> {
|
|
201
|
+
this.activeSessions.delete(conversationId);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
### 4. Error Classification and Recovery
|
|
207
|
+
|
|
208
|
+
```typescript
|
|
209
|
+
interface ErrorClassification {
|
|
210
|
+
type: "recoverable" | "fatal" | "expected";
|
|
211
|
+
shouldRetry: boolean;
|
|
212
|
+
maxRetries: number;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function classifyError(code: number, error?: Error): ErrorClassification {
|
|
216
|
+
// Expected termination
|
|
217
|
+
if (code === 143) {
|
|
218
|
+
// SIGTERM
|
|
219
|
+
return { type: "expected", shouldRetry: false, maxRetries: 0 };
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Recoverable errors
|
|
223
|
+
const recoverablePatterns = [
|
|
224
|
+
/timeout/i,
|
|
225
|
+
/network/i,
|
|
226
|
+
/ECONNREFUSED/i,
|
|
227
|
+
/temporary/i,
|
|
228
|
+
];
|
|
229
|
+
|
|
230
|
+
if (
|
|
231
|
+
error &&
|
|
232
|
+
recoverablePatterns.some((pattern) => pattern.test(error.message))
|
|
233
|
+
) {
|
|
234
|
+
return { type: "recoverable", shouldRetry: true, maxRetries: 3 };
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// Fatal errors
|
|
238
|
+
const fatalPatterns = [
|
|
239
|
+
/authentication/i,
|
|
240
|
+
/unauthorized/i,
|
|
241
|
+
/permission/i,
|
|
242
|
+
/not found/i,
|
|
243
|
+
];
|
|
244
|
+
|
|
245
|
+
if (error && fatalPatterns.some((pattern) => pattern.test(error.message))) {
|
|
246
|
+
return { type: "fatal", shouldRetry: false, maxRetries: 0 };
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Unknown errors - treat as potentially recoverable with limited retries
|
|
250
|
+
return { type: "recoverable", shouldRetry: true, maxRetries: 1 };
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
async function createResilientConversation(
|
|
254
|
+
config: ConversationConfig,
|
|
255
|
+
retryCount = 0
|
|
256
|
+
): Promise<ConversationType> {
|
|
257
|
+
const conversation = claude()
|
|
258
|
+
.withExecutable("/path/to/cli.js")
|
|
259
|
+
.inDirectory(config.workspacePath)
|
|
260
|
+
.onProcessComplete(async (code, error) => {
|
|
261
|
+
const classification = classifyError(code, error);
|
|
262
|
+
|
|
263
|
+
if (
|
|
264
|
+
classification.shouldRetry &&
|
|
265
|
+
retryCount < classification.maxRetries
|
|
266
|
+
) {
|
|
267
|
+
console.log(
|
|
268
|
+
`Retrying conversation (attempt ${retryCount + 1}/${
|
|
269
|
+
classification.maxRetries
|
|
270
|
+
})`
|
|
271
|
+
);
|
|
272
|
+
|
|
273
|
+
// Exponential backoff
|
|
274
|
+
const delay = Math.pow(2, retryCount) * 1000;
|
|
275
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
276
|
+
|
|
277
|
+
// Retry with incremented count
|
|
278
|
+
return createResilientConversation(config, retryCount + 1);
|
|
279
|
+
} else if (classification.type === "fatal") {
|
|
280
|
+
console.error("Fatal error, cannot retry:", error?.message);
|
|
281
|
+
throw error || new Error(`Process exited with code ${code}`);
|
|
282
|
+
}
|
|
283
|
+
})
|
|
284
|
+
.asConversation();
|
|
285
|
+
|
|
286
|
+
return conversation;
|
|
287
|
+
}
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
## Best Practices
|
|
291
|
+
|
|
292
|
+
### 1. Configuration Management
|
|
293
|
+
|
|
294
|
+
- **Use environment variables** for sensitive data like API keys
|
|
295
|
+
- **Validate configuration** before creating conversations
|
|
296
|
+
- **Use conditional chaining** for optional configurations
|
|
297
|
+
- **Centralize common configurations** in reusable functions
|
|
298
|
+
|
|
299
|
+
```typescript
|
|
300
|
+
// Good: Centralized configuration
|
|
301
|
+
function createStandardBuilder(): ClaudeBuilder {
|
|
302
|
+
return claude()
|
|
303
|
+
.withExecutable(require.resolve("@anthropic-ai/claude-code/cli.js"))
|
|
304
|
+
.withEnv({ ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY })
|
|
305
|
+
.skipPermissions();
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// Good: Conditional configuration
|
|
309
|
+
let builder = createStandardBuilder().inDirectory(workspacePath);
|
|
310
|
+
|
|
311
|
+
if (sessionId) {
|
|
312
|
+
builder = builder.withSessionId(sessionId);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
if (isReadOnlyMode) {
|
|
316
|
+
builder = builder.denyTools("Write", "Edit", "MultiEdit");
|
|
317
|
+
}
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
### 2. Error Handling
|
|
321
|
+
|
|
322
|
+
- **Use unified error handling** through `onProcessComplete()`
|
|
323
|
+
- **Classify errors** for appropriate recovery strategies
|
|
324
|
+
- **Implement retry logic** for recoverable errors
|
|
325
|
+
- **Log errors with context** for debugging
|
|
326
|
+
|
|
327
|
+
```typescript
|
|
328
|
+
// Good: Unified error handling
|
|
329
|
+
const conversation = claude()
|
|
330
|
+
.withExecutable("/path/to/cli.js")
|
|
331
|
+
.onProcessComplete((code, error) => {
|
|
332
|
+
const context = {
|
|
333
|
+
conversationId,
|
|
334
|
+
sessionId: currentSessionId,
|
|
335
|
+
exitCode: code,
|
|
336
|
+
error: error?.message,
|
|
337
|
+
timestamp: new Date().toISOString(),
|
|
338
|
+
};
|
|
339
|
+
|
|
340
|
+
if (error || code !== 0) {
|
|
341
|
+
logger.error("Conversation process error", context);
|
|
342
|
+
handleConversationError(context);
|
|
343
|
+
} else {
|
|
344
|
+
logger.info("Conversation completed successfully", context);
|
|
345
|
+
}
|
|
346
|
+
})
|
|
347
|
+
.asConversation();
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
### 3. Resource Management
|
|
351
|
+
|
|
352
|
+
- **Always call `conversation.end()`** when done
|
|
353
|
+
- **Clean up session tracking** after conversations end
|
|
354
|
+
- **Use try-finally blocks** to ensure cleanup
|
|
355
|
+
- **Monitor memory usage** in long-running applications
|
|
356
|
+
|
|
357
|
+
```typescript
|
|
358
|
+
// Good: Resource cleanup pattern
|
|
359
|
+
async function runConversation(config: ConversationConfig): Promise<void> {
|
|
360
|
+
let conversation: ConversationType | null = null;
|
|
361
|
+
|
|
362
|
+
try {
|
|
363
|
+
conversation = claude()
|
|
364
|
+
.withExecutable("/path/to/cli.js")
|
|
365
|
+
.inDirectory(config.workspacePath)
|
|
366
|
+
.asConversation();
|
|
367
|
+
|
|
368
|
+
// Use conversation
|
|
369
|
+
await doConversationWork(conversation);
|
|
370
|
+
} finally {
|
|
371
|
+
// Ensure cleanup
|
|
372
|
+
if (conversation) {
|
|
373
|
+
await conversation.end();
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
```
|
|
378
|
+
|
|
379
|
+
### 4. Message Handling
|
|
380
|
+
|
|
381
|
+
- **Use async message handlers** for processing
|
|
382
|
+
- **Handle different message types** appropriately
|
|
383
|
+
- **Avoid blocking operations** in stream handlers
|
|
384
|
+
- **Implement backpressure** for high-volume streams
|
|
385
|
+
|
|
386
|
+
```typescript
|
|
387
|
+
// Good: Async message handling with type checking
|
|
388
|
+
conversation.stream(async (message, sessionId) => {
|
|
389
|
+
try {
|
|
390
|
+
switch (message.type) {
|
|
391
|
+
case "assistant":
|
|
392
|
+
await handleAssistantMessage(message, sessionId);
|
|
393
|
+
break;
|
|
394
|
+
case "tool_result":
|
|
395
|
+
await handleToolResult(message, sessionId);
|
|
396
|
+
break;
|
|
397
|
+
case "system":
|
|
398
|
+
await handleSystemMessage(message, sessionId);
|
|
399
|
+
break;
|
|
400
|
+
default:
|
|
401
|
+
console.warn("Unknown message type:", message.type);
|
|
402
|
+
}
|
|
403
|
+
} catch (error) {
|
|
404
|
+
console.error("Error processing message:", error);
|
|
405
|
+
// Don't re-throw - let stream continue
|
|
406
|
+
}
|
|
407
|
+
});
|
|
408
|
+
```
|
|
409
|
+
|
|
410
|
+
## Common Use Cases
|
|
411
|
+
|
|
412
|
+
### 1. Task Execution with Resume
|
|
413
|
+
|
|
414
|
+
```typescript
|
|
415
|
+
async function executeTask(
|
|
416
|
+
taskId: string,
|
|
417
|
+
config: TaskConfig,
|
|
418
|
+
existingSessionId?: string
|
|
419
|
+
): Promise<void> {
|
|
420
|
+
const conversation = claude()
|
|
421
|
+
.withExecutable("/path/to/cli.js")
|
|
422
|
+
.withModel("sonnet")
|
|
423
|
+
.inDirectory(config.workspacePath)
|
|
424
|
+
.withEnv({
|
|
425
|
+
ANTHROPIC_API_KEY: config.apiKey,
|
|
426
|
+
GITHUB_TOKEN: config.githubToken,
|
|
427
|
+
})
|
|
428
|
+
.appendSystemPrompt(config.instructions);
|
|
429
|
+
|
|
430
|
+
// Resume if session exists
|
|
431
|
+
if (existingSessionId) {
|
|
432
|
+
conversation = conversation.withSessionId(existingSessionId);
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
const conv = conversation.asConversation();
|
|
436
|
+
|
|
437
|
+
// Track session for future resume
|
|
438
|
+
conv.onSessionId((sessionId) => {
|
|
439
|
+
updateTaskSession(taskId, sessionId);
|
|
440
|
+
});
|
|
441
|
+
|
|
442
|
+
// Handle task messages
|
|
443
|
+
conv.stream(async (message, sessionId) => {
|
|
444
|
+
await processTaskMessage(taskId, message, sessionId);
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
// Send initial task message
|
|
448
|
+
conv.send({
|
|
449
|
+
type: "text",
|
|
450
|
+
text: existingSessionId ? "Continue with the task" : config.initialPrompt,
|
|
451
|
+
});
|
|
452
|
+
}
|
|
453
|
+
```
|
|
454
|
+
|
|
455
|
+
### 2. Read-Only Mode Implementation
|
|
456
|
+
|
|
457
|
+
```typescript
|
|
458
|
+
function createReadOnlyConversation(
|
|
459
|
+
workspacePath: string,
|
|
460
|
+
apiKey: string
|
|
461
|
+
): ConversationType {
|
|
462
|
+
return claude()
|
|
463
|
+
.withExecutable("/path/to/cli.js")
|
|
464
|
+
.withEnv({ ANTHROPIC_API_KEY: apiKey })
|
|
465
|
+
.withModel("sonnet")
|
|
466
|
+
.inDirectory(workspacePath)
|
|
467
|
+
.denyTools("Write", "Edit", "MultiEdit", "Bash", "KillBash", "NotebookEdit")
|
|
468
|
+
.appendSystemPrompt(
|
|
469
|
+
"You are in read-only mode. You can analyze code and provide suggestions but cannot modify files or execute commands."
|
|
470
|
+
)
|
|
471
|
+
.asConversation();
|
|
472
|
+
}
|
|
473
|
+
```
|
|
474
|
+
|
|
475
|
+
### 3. MCP Server Integration
|
|
476
|
+
|
|
477
|
+
```typescript
|
|
478
|
+
async function createConversationWithMCP(config: {
|
|
479
|
+
workspacePath: string;
|
|
480
|
+
apiKey: string;
|
|
481
|
+
mcpServers: Record<string, any>;
|
|
482
|
+
toolToken: string;
|
|
483
|
+
}): Promise<ConversationType> {
|
|
484
|
+
// Expand environment variables in MCP server configurations
|
|
485
|
+
const expandedServers = expandEnv(config.mcpServers, {
|
|
486
|
+
TOOL_TOKEN: config.toolToken,
|
|
487
|
+
});
|
|
488
|
+
|
|
489
|
+
return claude()
|
|
490
|
+
.withExecutable("/path/to/cli.js")
|
|
491
|
+
.withEnv({ ANTHROPIC_API_KEY: config.apiKey })
|
|
492
|
+
.withModel("sonnet")
|
|
493
|
+
.inDirectory(config.workspacePath)
|
|
494
|
+
.withMCP(expandedServers)
|
|
495
|
+
.skipPermissions()
|
|
496
|
+
.onProcessComplete((code, error) => {
|
|
497
|
+
if (error?.message?.includes("mcp")) {
|
|
498
|
+
console.error("MCP server error:", error.message);
|
|
499
|
+
}
|
|
500
|
+
})
|
|
501
|
+
.asConversation();
|
|
502
|
+
}
|
|
503
|
+
```
|
|
504
|
+
|
|
505
|
+
## Error Handling
|
|
506
|
+
|
|
507
|
+
### Error Types and Classification
|
|
508
|
+
|
|
509
|
+
#### Process Exit Codes
|
|
510
|
+
|
|
511
|
+
- **0**: Successful completion
|
|
512
|
+
- **1**: General error
|
|
513
|
+
- **127**: Command not found (fatal)
|
|
514
|
+
- **143**: SIGTERM (expected termination)
|
|
515
|
+
|
|
516
|
+
#### Common Error Patterns
|
|
517
|
+
|
|
518
|
+
- **Authentication**: `authentication`, `401`, `unauthorized`
|
|
519
|
+
- **Network**: `timeout`, `ECONNREFUSED`, `network`
|
|
520
|
+
- **Session**: `session`, `Invalid session`, `expired`
|
|
521
|
+
- **Tool**: `tool`, `permission denied`, `command failed`
|
|
522
|
+
|
|
523
|
+
### Error Handling Implementation
|
|
524
|
+
|
|
525
|
+
```typescript
|
|
526
|
+
interface ConversationError {
|
|
527
|
+
conversationId: string;
|
|
528
|
+
sessionId?: string;
|
|
529
|
+
errorType: string;
|
|
530
|
+
message: string;
|
|
531
|
+
code?: number;
|
|
532
|
+
timestamp: Date;
|
|
533
|
+
recoverable: boolean;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
function handleConversationError(
|
|
537
|
+
code: number,
|
|
538
|
+
error: Error | undefined,
|
|
539
|
+
context: { conversationId: string; sessionId?: string }
|
|
540
|
+
): ConversationError {
|
|
541
|
+
const errorInfo: ConversationError = {
|
|
542
|
+
conversationId: context.conversationId,
|
|
543
|
+
sessionId: context.sessionId,
|
|
544
|
+
errorType: "unknown",
|
|
545
|
+
message: error?.message || `Process exited with code ${code}`,
|
|
546
|
+
code,
|
|
547
|
+
timestamp: new Date(),
|
|
548
|
+
recoverable: false,
|
|
549
|
+
};
|
|
550
|
+
|
|
551
|
+
// Classify error
|
|
552
|
+
if (code === 143) {
|
|
553
|
+
errorInfo.errorType = "termination";
|
|
554
|
+
errorInfo.recoverable = false; // Expected
|
|
555
|
+
} else if (error?.message.includes("authentication")) {
|
|
556
|
+
errorInfo.errorType = "authentication";
|
|
557
|
+
errorInfo.recoverable = false; // Fatal
|
|
558
|
+
} else if (
|
|
559
|
+
error?.message.includes("timeout") ||
|
|
560
|
+
error?.message.includes("network")
|
|
561
|
+
) {
|
|
562
|
+
errorInfo.errorType = "network";
|
|
563
|
+
errorInfo.recoverable = true; // Retry possible
|
|
564
|
+
} else if (error?.message.includes("session")) {
|
|
565
|
+
errorInfo.errorType = "session";
|
|
566
|
+
errorInfo.recoverable = true; // Can recreate session
|
|
567
|
+
} else if (code === 127) {
|
|
568
|
+
errorInfo.errorType = "command_not_found";
|
|
569
|
+
errorInfo.recoverable = false; // Fatal
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
// Log error
|
|
573
|
+
console.error("Conversation error:", errorInfo);
|
|
574
|
+
|
|
575
|
+
// Notify error handlers
|
|
576
|
+
notifyErrorHandlers(errorInfo);
|
|
577
|
+
|
|
578
|
+
return errorInfo;
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
async function notifyErrorHandlers(error: ConversationError): Promise<void> {
|
|
582
|
+
// Example: Report to monitoring system
|
|
583
|
+
if (error.errorType === "authentication") {
|
|
584
|
+
await reportCriticalError(error);
|
|
585
|
+
} else if (error.recoverable) {
|
|
586
|
+
await scheduleRetry(error);
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
```
|
|
590
|
+
|
|
591
|
+
## Performance Optimization
|
|
592
|
+
|
|
593
|
+
### 1. Builder Reuse
|
|
594
|
+
|
|
595
|
+
```typescript
|
|
596
|
+
// Cache common builder configurations
|
|
597
|
+
const builderCache = new Map<string, ClaudeBuilder>();
|
|
598
|
+
|
|
599
|
+
function getOptimizedBuilder(workspacePath: string): ClaudeBuilder {
|
|
600
|
+
const cacheKey = `standard_${workspacePath}`;
|
|
601
|
+
|
|
602
|
+
if (!builderCache.has(cacheKey)) {
|
|
603
|
+
const builder = claude()
|
|
604
|
+
.withExecutable("/path/to/cli.js")
|
|
605
|
+
.withModel("sonnet")
|
|
606
|
+
.inDirectory(workspacePath)
|
|
607
|
+
.skipPermissions();
|
|
608
|
+
|
|
609
|
+
builderCache.set(cacheKey, builder);
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
return builderCache.get(cacheKey)!;
|
|
613
|
+
}
|
|
614
|
+
```
|
|
615
|
+
|
|
616
|
+
### 2. Efficient Message Handling
|
|
617
|
+
|
|
618
|
+
```typescript
|
|
619
|
+
// Batch message processing for high throughput
|
|
620
|
+
class MessageProcessor {
|
|
621
|
+
private messageQueue: Array<{ message: any; sessionId: string | null }> = [];
|
|
622
|
+
private processing = false;
|
|
623
|
+
|
|
624
|
+
async handleMessage(message: any, sessionId: string | null): Promise<void> {
|
|
625
|
+
this.messageQueue.push({ message, sessionId });
|
|
626
|
+
|
|
627
|
+
if (!this.processing) {
|
|
628
|
+
await this.processQueue();
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
private async processQueue(): Promise<void> {
|
|
633
|
+
this.processing = true;
|
|
634
|
+
|
|
635
|
+
while (this.messageQueue.length > 0) {
|
|
636
|
+
const batch = this.messageQueue.splice(0, 10); // Process in batches
|
|
637
|
+
|
|
638
|
+
await Promise.all(
|
|
639
|
+
batch.map(({ message, sessionId }) =>
|
|
640
|
+
this.processSingleMessage(message, sessionId)
|
|
641
|
+
)
|
|
642
|
+
);
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
this.processing = false;
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
private async processSingleMessage(
|
|
649
|
+
message: any,
|
|
650
|
+
sessionId: string | null
|
|
651
|
+
): Promise<void> {
|
|
652
|
+
// Individual message processing logic
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
```
|
|
656
|
+
|
|
657
|
+
### 3. Memory Management
|
|
658
|
+
|
|
659
|
+
```typescript
|
|
660
|
+
// Monitor and cleanup resources
|
|
661
|
+
class ConversationManager {
|
|
662
|
+
private conversations = new Map<string, ConversationType>();
|
|
663
|
+
private sessionCleanupTimer: NodeJS.Timeout;
|
|
664
|
+
|
|
665
|
+
constructor() {
|
|
666
|
+
// Periodic cleanup
|
|
667
|
+
this.sessionCleanupTimer = setInterval(() => {
|
|
668
|
+
this.cleanupInactiveConversations();
|
|
669
|
+
}, 60000); // Every minute
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
async createConversation(
|
|
673
|
+
id: string,
|
|
674
|
+
config: ConversationConfig
|
|
675
|
+
): Promise<ConversationType> {
|
|
676
|
+
const conversation = claude()
|
|
677
|
+
.withExecutable("/path/to/cli.js")
|
|
678
|
+
.inDirectory(config.workspacePath)
|
|
679
|
+
.onProcessComplete(async (code, error) => {
|
|
680
|
+
// Auto-cleanup on completion
|
|
681
|
+
await this.endConversation(id);
|
|
682
|
+
})
|
|
683
|
+
.asConversation();
|
|
684
|
+
|
|
685
|
+
this.conversations.set(id, conversation);
|
|
686
|
+
return conversation;
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
async endConversation(id: string): Promise<void> {
|
|
690
|
+
const conversation = this.conversations.get(id);
|
|
691
|
+
if (conversation) {
|
|
692
|
+
await conversation.end();
|
|
693
|
+
this.conversations.delete(id);
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
private async cleanupInactiveConversations(): Promise<void> {
|
|
698
|
+
// Implementation for cleanup based on inactivity
|
|
699
|
+
console.log(`Active conversations: ${this.conversations.size}`);
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
destroy(): void {
|
|
703
|
+
clearInterval(this.sessionCleanupTimer);
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
```
|
|
707
|
+
|
|
708
|
+
## Troubleshooting
|
|
709
|
+
|
|
710
|
+
### Common Issues and Solutions
|
|
711
|
+
|
|
712
|
+
#### 1. Session Resume Failures
|
|
713
|
+
|
|
714
|
+
**Problem**: Conversation fails to resume with existing session ID
|
|
715
|
+
|
|
716
|
+
```
|
|
717
|
+
Error: Invalid session ID provided
|
|
718
|
+
```
|
|
719
|
+
|
|
720
|
+
**Solution**:
|
|
721
|
+
|
|
722
|
+
```typescript
|
|
723
|
+
// Validate session before resuming
|
|
724
|
+
async function safeResumeConversation(
|
|
725
|
+
sessionId: string,
|
|
726
|
+
config: ConversationConfig
|
|
727
|
+
): Promise<ConversationType> {
|
|
728
|
+
try {
|
|
729
|
+
return claude()
|
|
730
|
+
.withExecutable("/path/to/cli.js")
|
|
731
|
+
.withSessionId(sessionId)
|
|
732
|
+
.inDirectory(config.workspacePath)
|
|
733
|
+
.asConversation();
|
|
734
|
+
} catch (error) {
|
|
735
|
+
if (error.message.includes("Invalid session")) {
|
|
736
|
+
console.warn(
|
|
737
|
+
`Session ${sessionId} is invalid, creating new conversation`
|
|
738
|
+
);
|
|
739
|
+
return claude()
|
|
740
|
+
.withExecutable("/path/to/cli.js")
|
|
741
|
+
.inDirectory(config.workspacePath)
|
|
742
|
+
.asConversation();
|
|
743
|
+
}
|
|
744
|
+
throw error;
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
```
|
|
748
|
+
|
|
749
|
+
#### 2. MCP Server Connection Issues
|
|
750
|
+
|
|
751
|
+
**Problem**: MCP servers fail to connect
|
|
752
|
+
|
|
753
|
+
```
|
|
754
|
+
Error: Failed to connect to MCP server 'filesystem-server'
|
|
755
|
+
```
|
|
756
|
+
|
|
757
|
+
**Solution**:
|
|
758
|
+
|
|
759
|
+
```typescript
|
|
760
|
+
// Validate MCP server configuration
|
|
761
|
+
function validateMCPServers(servers: Record<string, any>): Record<string, any> {
|
|
762
|
+
const validatedServers: Record<string, any> = {};
|
|
763
|
+
|
|
764
|
+
for (const [name, config] of Object.entries(servers)) {
|
|
765
|
+
try {
|
|
766
|
+
// Validate server configuration
|
|
767
|
+
if (!config.command) {
|
|
768
|
+
throw new Error(`MCP server '${name}' missing command`);
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
validatedServers[name] = config;
|
|
772
|
+
} catch (error) {
|
|
773
|
+
console.warn(`Skipping invalid MCP server '${name}':`, error.message);
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
return validatedServers;
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
const conversation = claude()
|
|
781
|
+
.withExecutable("/path/to/cli.js")
|
|
782
|
+
.withMCP(validateMCPServers(config.mcpServers))
|
|
783
|
+
.asConversation();
|
|
784
|
+
```
|
|
785
|
+
|
|
786
|
+
#### 3. Memory Leaks
|
|
787
|
+
|
|
788
|
+
**Problem**: Memory usage increases over time
|
|
789
|
+
|
|
790
|
+
**Solution**:
|
|
791
|
+
|
|
792
|
+
```typescript
|
|
793
|
+
// Proper cleanup and monitoring
|
|
794
|
+
class MemoryAwareConversationManager {
|
|
795
|
+
private conversations = new Map<
|
|
796
|
+
string,
|
|
797
|
+
{
|
|
798
|
+
conversation: ConversationType;
|
|
799
|
+
createdAt: Date;
|
|
800
|
+
lastActivity: Date;
|
|
801
|
+
}
|
|
802
|
+
>();
|
|
803
|
+
|
|
804
|
+
async createConversation(
|
|
805
|
+
id: string,
|
|
806
|
+
config: ConversationConfig
|
|
807
|
+
): Promise<ConversationType> {
|
|
808
|
+
// Check memory before creating new conversation
|
|
809
|
+
const memoryUsage = process.memoryUsage();
|
|
810
|
+
if (memoryUsage.heapUsed > 500 * 1024 * 1024) {
|
|
811
|
+
// 500MB limit
|
|
812
|
+
await this.forceCleanup();
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
const conversation = claude()
|
|
816
|
+
.withExecutable("/path/to/cli.js")
|
|
817
|
+
.inDirectory(config.workspacePath)
|
|
818
|
+
.asConversation();
|
|
819
|
+
|
|
820
|
+
this.conversations.set(id, {
|
|
821
|
+
conversation,
|
|
822
|
+
createdAt: new Date(),
|
|
823
|
+
lastActivity: new Date(),
|
|
824
|
+
});
|
|
825
|
+
|
|
826
|
+
return conversation;
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
private async forceCleanup(): Promise<void> {
|
|
830
|
+
const cutoff = new Date(Date.now() - 30 * 60 * 1000); // 30 minutes ago
|
|
831
|
+
|
|
832
|
+
for (const [
|
|
833
|
+
id,
|
|
834
|
+
{ conversation, lastActivity },
|
|
835
|
+
] of this.conversations.entries()) {
|
|
836
|
+
if (lastActivity < cutoff) {
|
|
837
|
+
await conversation.end();
|
|
838
|
+
this.conversations.delete(id);
|
|
839
|
+
console.log(`Cleaned up inactive conversation ${id}`);
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
// Force garbage collection if available
|
|
844
|
+
if (global.gc) {
|
|
845
|
+
global.gc();
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
```
|
|
850
|
+
|
|
851
|
+
#### 4. Process Exit Issues
|
|
852
|
+
|
|
853
|
+
**Problem**: Conversations don't terminate properly
|
|
854
|
+
|
|
855
|
+
**Solution**:
|
|
856
|
+
|
|
857
|
+
```typescript
|
|
858
|
+
// Implement timeout and forced termination
|
|
859
|
+
async function createConversationWithTimeout(
|
|
860
|
+
config: ConversationConfig,
|
|
861
|
+
timeoutMs: number = 300000 // 5 minutes default
|
|
862
|
+
): Promise<ConversationType> {
|
|
863
|
+
const conversation = claude()
|
|
864
|
+
.withExecutable("/path/to/cli.js")
|
|
865
|
+
.inDirectory(config.workspacePath)
|
|
866
|
+
.asConversation();
|
|
867
|
+
|
|
868
|
+
// Set up timeout
|
|
869
|
+
const timeoutId = setTimeout(async () => {
|
|
870
|
+
console.warn("Conversation timeout, forcing termination");
|
|
871
|
+
await conversation.end();
|
|
872
|
+
}, timeoutMs);
|
|
873
|
+
|
|
874
|
+
// Clear timeout on normal completion
|
|
875
|
+
conversation.stream(async (message, sessionId) => {
|
|
876
|
+
if (
|
|
877
|
+
message.type === "result" ||
|
|
878
|
+
(message.type === "system" && message.subtype === "exit")
|
|
879
|
+
) {
|
|
880
|
+
clearTimeout(timeoutId);
|
|
881
|
+
}
|
|
882
|
+
});
|
|
883
|
+
|
|
884
|
+
return conversation;
|
|
885
|
+
}
|
|
886
|
+
```
|
|
887
|
+
|
|
888
|
+
### Debug Mode
|
|
889
|
+
|
|
890
|
+
Enable debug logging for troubleshooting:
|
|
891
|
+
|
|
892
|
+
```typescript
|
|
893
|
+
// Enable SDK debug logging
|
|
894
|
+
process.env.DEBUG = "true";
|
|
895
|
+
|
|
896
|
+
const conversation = claude()
|
|
897
|
+
.withExecutable("/path/to/cli.js")
|
|
898
|
+
.withEnv({ DEBUG: "1" }) // Enable CLI debug output
|
|
899
|
+
.inDirectory(config.workspacePath)
|
|
900
|
+
.onProcessComplete((code, error) => {
|
|
901
|
+
console.debug("Process completion debug:", {
|
|
902
|
+
code,
|
|
903
|
+
error: error?.message,
|
|
904
|
+
stack: error?.stack,
|
|
905
|
+
});
|
|
906
|
+
})
|
|
907
|
+
.asConversation();
|
|
908
|
+
```
|
|
909
|
+
|
|
910
|
+
## Testing Patterns
|
|
911
|
+
|
|
912
|
+
### Unit Testing Builder Configuration
|
|
913
|
+
|
|
914
|
+
```typescript
|
|
915
|
+
import { describe, it, expect, vi } from "vitest";
|
|
916
|
+
import { claude } from "@anthropic-ai/claude-code";
|
|
917
|
+
|
|
918
|
+
describe("Builder Configuration", () => {
|
|
919
|
+
it("should create correct builder chain", () => {
|
|
920
|
+
const mockBuilder = {
|
|
921
|
+
withExecutable: vi.fn().mockReturnThis(),
|
|
922
|
+
withModel: vi.fn().mockReturnThis(),
|
|
923
|
+
inDirectory: vi.fn().mockReturnThis(),
|
|
924
|
+
withSessionId: vi.fn().mockReturnThis(),
|
|
925
|
+
asConversation: vi.fn().mockReturnValue({}),
|
|
926
|
+
};
|
|
927
|
+
|
|
928
|
+
vi.mocked(claude).mockReturnValue(mockBuilder);
|
|
929
|
+
|
|
930
|
+
const conversation = claude()
|
|
931
|
+
.withExecutable("/path/to/cli.js")
|
|
932
|
+
.withModel("sonnet")
|
|
933
|
+
.inDirectory("/workspace")
|
|
934
|
+
.withSessionId("session123")
|
|
935
|
+
.asConversation();
|
|
936
|
+
|
|
937
|
+
expect(mockBuilder.withExecutable).toHaveBeenCalledWith("/path/to/cli.js");
|
|
938
|
+
expect(mockBuilder.withModel).toHaveBeenCalledWith("sonnet");
|
|
939
|
+
expect(mockBuilder.inDirectory).toHaveBeenCalledWith("/workspace");
|
|
940
|
+
expect(mockBuilder.withSessionId).toHaveBeenCalledWith("session123");
|
|
941
|
+
expect(mockBuilder.asConversation).toHaveBeenCalled();
|
|
942
|
+
});
|
|
943
|
+
});
|
|
944
|
+
```
|
|
945
|
+
|
|
946
|
+
### Integration Testing
|
|
947
|
+
|
|
948
|
+
```typescript
|
|
949
|
+
describe("Conversation Integration", () => {
|
|
950
|
+
it("should handle complete conversation lifecycle", async () => {
|
|
951
|
+
const mockConversation = {
|
|
952
|
+
send: vi.fn(),
|
|
953
|
+
end: vi.fn().mockResolvedValue(undefined),
|
|
954
|
+
onSessionId: vi.fn(),
|
|
955
|
+
stream: vi.fn(),
|
|
956
|
+
};
|
|
957
|
+
|
|
958
|
+
const conversation = await createTestConversation(mockConversation);
|
|
959
|
+
|
|
960
|
+
// Test session ID callback
|
|
961
|
+
const sessionCallback = vi.fn();
|
|
962
|
+
conversation.onSessionId(sessionCallback);
|
|
963
|
+
|
|
964
|
+
// Simulate session ID
|
|
965
|
+
const onSessionIdCall = vi.mocked(conversation.onSessionId).mock
|
|
966
|
+
.calls[0][0];
|
|
967
|
+
onSessionIdCall("test-session-123");
|
|
968
|
+
|
|
969
|
+
expect(sessionCallback).toHaveBeenCalledWith("test-session-123");
|
|
970
|
+
|
|
971
|
+
// Test message sending
|
|
972
|
+
conversation.send({ type: "text", text: "Test message" });
|
|
973
|
+
expect(mockConversation.send).toHaveBeenCalledWith({
|
|
974
|
+
type: "text",
|
|
975
|
+
text: "Test message",
|
|
976
|
+
});
|
|
977
|
+
|
|
978
|
+
// Test cleanup
|
|
979
|
+
await conversation.end();
|
|
980
|
+
expect(mockConversation.end).toHaveBeenCalled();
|
|
981
|
+
});
|
|
982
|
+
});
|
|
983
|
+
```
|
|
984
|
+
|
|
985
|
+
### Performance Testing
|
|
986
|
+
|
|
987
|
+
```typescript
|
|
988
|
+
describe("Performance", () => {
|
|
989
|
+
it("should create conversations efficiently", () => {
|
|
990
|
+
const startTime = Date.now();
|
|
991
|
+
|
|
992
|
+
for (let i = 0; i < 100; i++) {
|
|
993
|
+
claude()
|
|
994
|
+
.withExecutable("/path/to/cli.js")
|
|
995
|
+
.withModel("sonnet")
|
|
996
|
+
.inDirectory(`/workspace${i}`)
|
|
997
|
+
.asConversation();
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
const duration = Date.now() - startTime;
|
|
1001
|
+
expect(duration).toBeLessThan(1000); // Should be very fast
|
|
1002
|
+
});
|
|
1003
|
+
|
|
1004
|
+
it("should handle concurrent conversations", async () => {
|
|
1005
|
+
const conversations = await Promise.all(
|
|
1006
|
+
Array.from({ length: 10 }, (_, i) =>
|
|
1007
|
+
createTestConversation({
|
|
1008
|
+
workspacePath: `/workspace${i}`,
|
|
1009
|
+
sessionId: `session${i}`,
|
|
1010
|
+
})
|
|
1011
|
+
)
|
|
1012
|
+
);
|
|
1013
|
+
|
|
1014
|
+
expect(conversations).toHaveLength(10);
|
|
1015
|
+
|
|
1016
|
+
// Clean up
|
|
1017
|
+
await Promise.all(conversations.map((conv) => conv.end()));
|
|
1018
|
+
});
|
|
1019
|
+
});
|
|
1020
|
+
```
|
|
1021
|
+
|
|
1022
|
+
---
|
|
1023
|
+
|
|
1024
|
+
## Summary
|
|
1025
|
+
|
|
1026
|
+
The SDK-native implementation provides significant improvements over the legacy query-based approach:
|
|
1027
|
+
|
|
1028
|
+
- **Simplified Configuration**: Clean builder pattern vs complex query options
|
|
1029
|
+
- **Native Session Management**: Built-in `withSessionId()` vs custom session handling
|
|
1030
|
+
- **Better Error Handling**: Unified `onProcessComplete()` vs multiple error points
|
|
1031
|
+
- **Improved Performance**: Direct streaming and messaging vs custom wrappers
|
|
1032
|
+
- **Enhanced Maintainability**: Clear patterns and better type safety
|
|
1033
|
+
|
|
1034
|
+
Follow the patterns and best practices in this guide to implement robust, performant conversation management in your applications. The comprehensive test suite validates these patterns and ensures reliability during migration and ongoing development.
|
|
1035
|
+
|
|
1036
|
+
For additional support, refer to the test files in `src/__tests__/` which provide working examples of all patterns described in this guide.
|