@plannotator/pi-extension 0.15.0 → 0.15.2
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 +1 -1
- package/generated/ai/base-session.ts +95 -0
- package/generated/ai/context.ts +212 -0
- package/generated/ai/endpoints.ts +309 -0
- package/generated/ai/index.ts +106 -0
- package/generated/ai/provider.ts +104 -0
- package/generated/ai/providers/claude-agent-sdk.ts +441 -0
- package/generated/ai/providers/codex-sdk.ts +430 -0
- package/generated/ai/providers/opencode-sdk.ts +491 -0
- package/generated/ai/providers/pi-events.ts +111 -0
- package/generated/ai/providers/pi-sdk-node.ts +377 -0
- package/generated/ai/providers/pi-sdk.ts +442 -0
- package/generated/ai/session-manager.ts +196 -0
- package/generated/ai/types.ts +370 -0
- package/generated/resolve-file.ts +28 -0
- package/index.ts +74 -45
- package/package.json +2 -2
- package/plannotator.html +70 -70
- package/review-editor.html +2 -2
- package/server/serverAnnotate.ts +2 -1
- package/server/serverReview.ts +5 -5
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
// @generated — DO NOT EDIT. Source: packages/ai/index.ts
|
|
2
|
+
/**
|
|
3
|
+
* @plannotator/ai — AI provider layer for Plannotator.
|
|
4
|
+
*
|
|
5
|
+
* This package provides the backbone for AI-powered features (inline chat,
|
|
6
|
+
* plan Q&A, code review assistance) across all Plannotator surfaces.
|
|
7
|
+
*
|
|
8
|
+
* Architecture:
|
|
9
|
+
*
|
|
10
|
+
* ┌─────────────────┐ ┌──────────────┐
|
|
11
|
+
* │ Plan Review UI │────▶│ │
|
|
12
|
+
* ├─────────────────┤ │ AI Endpoints │──▶ SSE stream
|
|
13
|
+
* │ Code Review UI │────▶│ (HTTP) │
|
|
14
|
+
* ├─────────────────┤ │ │
|
|
15
|
+
* │ Annotate UI │────▶└──────┬───────┘
|
|
16
|
+
* └─────────────────┘ │
|
|
17
|
+
* ▼
|
|
18
|
+
* ┌────────────────┐
|
|
19
|
+
* │ Session Manager │
|
|
20
|
+
* └────────┬───────┘
|
|
21
|
+
* │
|
|
22
|
+
* ┌────────▼───────┐
|
|
23
|
+
* │ AIProvider │ (abstract)
|
|
24
|
+
* └────────┬───────┘
|
|
25
|
+
* │
|
|
26
|
+
* ┌─────────────┼──────────────┐
|
|
27
|
+
* ▼ ▼ ▼
|
|
28
|
+
* ┌──────────────┐ ┌──────────┐ ┌───────────┐
|
|
29
|
+
* │ Claude Agent │ │ OpenCode │ │ Future │
|
|
30
|
+
* │ SDK Provider │ │ Provider │ │ Providers │
|
|
31
|
+
* └──────────────┘ └──────────┘ └───────────┘
|
|
32
|
+
*
|
|
33
|
+
* Quick start:
|
|
34
|
+
*
|
|
35
|
+
* ```ts
|
|
36
|
+
* import "@plannotator/ai/providers/claude-agent-sdk";
|
|
37
|
+
* import { ProviderRegistry, createProvider, createAIEndpoints, SessionManager } from "@plannotator/ai";
|
|
38
|
+
*
|
|
39
|
+
* // 1. Create a registry and provider
|
|
40
|
+
* const registry = new ProviderRegistry();
|
|
41
|
+
* const provider = await createProvider({ type: "claude-agent-sdk", cwd: process.cwd() });
|
|
42
|
+
* registry.register(provider);
|
|
43
|
+
*
|
|
44
|
+
* // 2. Create endpoints and session manager
|
|
45
|
+
* const sessionManager = new SessionManager();
|
|
46
|
+
* const aiEndpoints = createAIEndpoints({ registry, sessionManager });
|
|
47
|
+
*
|
|
48
|
+
* // 3. Mount endpoints in your Bun server
|
|
49
|
+
* // aiEndpoints["/api/ai/query"](request) → SSE Response
|
|
50
|
+
* ```
|
|
51
|
+
*/
|
|
52
|
+
|
|
53
|
+
// Types
|
|
54
|
+
export type {
|
|
55
|
+
AIProvider,
|
|
56
|
+
AIProviderCapabilities,
|
|
57
|
+
AIProviderConfig,
|
|
58
|
+
AISession,
|
|
59
|
+
AIMessage,
|
|
60
|
+
AITextMessage,
|
|
61
|
+
AITextDeltaMessage,
|
|
62
|
+
AIToolUseMessage,
|
|
63
|
+
AIToolResultMessage,
|
|
64
|
+
AIErrorMessage,
|
|
65
|
+
AIResultMessage,
|
|
66
|
+
AIPermissionRequestMessage,
|
|
67
|
+
AIUnknownMessage,
|
|
68
|
+
AIContext,
|
|
69
|
+
AIContextMode,
|
|
70
|
+
PlanContext,
|
|
71
|
+
CodeReviewContext,
|
|
72
|
+
AnnotateContext,
|
|
73
|
+
ParentSession,
|
|
74
|
+
CreateSessionOptions,
|
|
75
|
+
ClaudeAgentSDKConfig,
|
|
76
|
+
CodexSDKConfig,
|
|
77
|
+
PiSDKConfig,
|
|
78
|
+
OpenCodeConfig,
|
|
79
|
+
} from "./types.ts";
|
|
80
|
+
|
|
81
|
+
// Provider registry
|
|
82
|
+
export {
|
|
83
|
+
ProviderRegistry,
|
|
84
|
+
registerProviderFactory,
|
|
85
|
+
createProvider,
|
|
86
|
+
} from "./provider.ts";
|
|
87
|
+
|
|
88
|
+
// Context builders
|
|
89
|
+
export { buildSystemPrompt, buildForkPreamble, buildEffectivePrompt } from "./context.ts";
|
|
90
|
+
|
|
91
|
+
// Base session
|
|
92
|
+
export { BaseSession } from "./base-session.ts";
|
|
93
|
+
|
|
94
|
+
// Session manager
|
|
95
|
+
export { SessionManager } from "./session-manager.ts";
|
|
96
|
+
export type { SessionEntry, SessionManagerOptions } from "./session-manager.ts";
|
|
97
|
+
|
|
98
|
+
// HTTP endpoints
|
|
99
|
+
export { createAIEndpoints } from "./endpoints.ts";
|
|
100
|
+
export type {
|
|
101
|
+
AIEndpoints,
|
|
102
|
+
AIEndpointDeps,
|
|
103
|
+
CreateSessionRequest,
|
|
104
|
+
QueryRequest,
|
|
105
|
+
AbortRequest,
|
|
106
|
+
} from "./endpoints.ts";
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
// @generated — DO NOT EDIT. Source: packages/ai/provider.ts
|
|
2
|
+
/**
|
|
3
|
+
* Provider registry — manages AI provider instances.
|
|
4
|
+
*
|
|
5
|
+
* Supports multiple instances of the same provider type (e.g., two Claude
|
|
6
|
+
* Agent SDK providers with different configs) keyed by instance ID.
|
|
7
|
+
*
|
|
8
|
+
* Each server (plan review, code review, annotate) should create its own
|
|
9
|
+
* ProviderRegistry or share one — no module-level global state.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import type { AIProvider, AIProviderConfig } from "./types.ts";
|
|
13
|
+
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
// Factory registry (global — factories are stateless type→constructor maps)
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
|
|
18
|
+
type ProviderFactory = (config: AIProviderConfig) => Promise<AIProvider>;
|
|
19
|
+
const factories = new Map<string, ProviderFactory>();
|
|
20
|
+
|
|
21
|
+
/** Register a factory function for a provider type. */
|
|
22
|
+
export function registerProviderFactory(
|
|
23
|
+
type: string,
|
|
24
|
+
factory: ProviderFactory
|
|
25
|
+
): void {
|
|
26
|
+
factories.set(type, factory);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Create a provider from config using a registered factory. Does NOT auto-register. */
|
|
30
|
+
export async function createProvider(
|
|
31
|
+
config: AIProviderConfig
|
|
32
|
+
): Promise<AIProvider> {
|
|
33
|
+
const factory = factories.get(config.type);
|
|
34
|
+
if (!factory) {
|
|
35
|
+
throw new Error(
|
|
36
|
+
`No AI provider factory registered for type "${config.type}". ` +
|
|
37
|
+
`Available: ${[...factories.keys()].join(", ") || "(none)"}`
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
return factory(config);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// ---------------------------------------------------------------------------
|
|
44
|
+
// Registry
|
|
45
|
+
// ---------------------------------------------------------------------------
|
|
46
|
+
|
|
47
|
+
export class ProviderRegistry {
|
|
48
|
+
private instances = new Map<string, AIProvider>();
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Register a provider instance under an ID.
|
|
52
|
+
* If no instanceId is provided, uses `provider.name`.
|
|
53
|
+
* Returns the instanceId used.
|
|
54
|
+
*/
|
|
55
|
+
register(provider: AIProvider, instanceId?: string): string {
|
|
56
|
+
const id = instanceId ?? provider.name;
|
|
57
|
+
this.instances.set(id, provider);
|
|
58
|
+
return id;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Get a provider by instance ID. */
|
|
62
|
+
get(instanceId: string): AIProvider | undefined {
|
|
63
|
+
return this.instances.get(instanceId);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Get the first registered provider (convenience for single-provider setups). */
|
|
67
|
+
getDefault(): { id: string; provider: AIProvider } | undefined {
|
|
68
|
+
const first = this.instances.entries().next();
|
|
69
|
+
if (first.done) return undefined;
|
|
70
|
+
return { id: first.value[0], provider: first.value[1] };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Get all instances of a given provider type (by provider.name). */
|
|
74
|
+
getByType(typeName: string): AIProvider[] {
|
|
75
|
+
return [...this.instances.values()].filter((p) => p.name === typeName);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** List all instance IDs. */
|
|
79
|
+
list(): string[] {
|
|
80
|
+
return [...this.instances.keys()];
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Dispose and remove a single instance. No-op if not found. */
|
|
84
|
+
dispose(instanceId: string): void {
|
|
85
|
+
const provider = this.instances.get(instanceId);
|
|
86
|
+
if (provider) {
|
|
87
|
+
provider.dispose();
|
|
88
|
+
this.instances.delete(instanceId);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Dispose all providers and clear the registry. */
|
|
93
|
+
disposeAll(): void {
|
|
94
|
+
for (const provider of this.instances.values()) {
|
|
95
|
+
provider.dispose();
|
|
96
|
+
}
|
|
97
|
+
this.instances.clear();
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** Number of registered instances. */
|
|
101
|
+
get size(): number {
|
|
102
|
+
return this.instances.size;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
@@ -0,0 +1,441 @@
|
|
|
1
|
+
// @generated — DO NOT EDIT. Source: packages/ai/providers/claude-agent-sdk.ts
|
|
2
|
+
/**
|
|
3
|
+
* Claude Agent SDK provider — the first concrete AIProvider implementation.
|
|
4
|
+
*
|
|
5
|
+
* Uses @anthropic-ai/claude-agent-sdk to create sessions that can:
|
|
6
|
+
* - Start fresh with Plannotator context as the system prompt
|
|
7
|
+
* - Fork from a parent Claude Code session (preserving full history)
|
|
8
|
+
* - Resume a previous Plannotator inline chat session
|
|
9
|
+
* - Stream text deltas back to the UI in real time
|
|
10
|
+
*
|
|
11
|
+
* Sessions are read-only by default (tools limited to Read, Glob, Grep)
|
|
12
|
+
* to keep inline chat safe and cost-bounded.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { buildSystemPrompt, buildForkPreamble, buildEffectivePrompt } from "../context.ts";
|
|
16
|
+
import { BaseSession } from "../base-session.ts";
|
|
17
|
+
import type {
|
|
18
|
+
AIProvider,
|
|
19
|
+
AIProviderCapabilities,
|
|
20
|
+
AISession,
|
|
21
|
+
AIMessage,
|
|
22
|
+
CreateSessionOptions,
|
|
23
|
+
ClaudeAgentSDKConfig,
|
|
24
|
+
} from "../types.ts";
|
|
25
|
+
|
|
26
|
+
// ---------------------------------------------------------------------------
|
|
27
|
+
// Constants
|
|
28
|
+
// ---------------------------------------------------------------------------
|
|
29
|
+
|
|
30
|
+
const PROVIDER_NAME = "claude-agent-sdk";
|
|
31
|
+
|
|
32
|
+
/** Default read-only tools for inline chat. */
|
|
33
|
+
const DEFAULT_ALLOWED_TOOLS = ["Read", "Glob", "Grep", "WebSearch"];
|
|
34
|
+
|
|
35
|
+
const DEFAULT_MAX_TURNS = 99;
|
|
36
|
+
const DEFAULT_MODEL = "claude-sonnet-4-6";
|
|
37
|
+
|
|
38
|
+
// ---------------------------------------------------------------------------
|
|
39
|
+
// SDK query options — typed to catch typos at compile time
|
|
40
|
+
// ---------------------------------------------------------------------------
|
|
41
|
+
|
|
42
|
+
interface ClaudeSDKQueryOptions {
|
|
43
|
+
model: string;
|
|
44
|
+
maxTurns: number;
|
|
45
|
+
allowedTools: string[];
|
|
46
|
+
cwd: string;
|
|
47
|
+
abortController: AbortController;
|
|
48
|
+
includePartialMessages: boolean;
|
|
49
|
+
persistSession: boolean;
|
|
50
|
+
maxBudgetUsd?: number;
|
|
51
|
+
systemPrompt?: string | { type: "preset"; preset: string; append?: string };
|
|
52
|
+
resume?: string;
|
|
53
|
+
forkSession?: boolean;
|
|
54
|
+
permissionMode?: ClaudeAgentSDKConfig['permissionMode'];
|
|
55
|
+
allowDangerouslySkipPermissions?: boolean;
|
|
56
|
+
pathToClaudeCodeExecutable?: string;
|
|
57
|
+
settingSources?: string[];
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// ---------------------------------------------------------------------------
|
|
61
|
+
// Provider
|
|
62
|
+
// ---------------------------------------------------------------------------
|
|
63
|
+
|
|
64
|
+
export class ClaudeAgentSDKProvider implements AIProvider {
|
|
65
|
+
readonly name = PROVIDER_NAME;
|
|
66
|
+
readonly capabilities: AIProviderCapabilities = {
|
|
67
|
+
fork: true,
|
|
68
|
+
resume: true,
|
|
69
|
+
streaming: true,
|
|
70
|
+
tools: true,
|
|
71
|
+
};
|
|
72
|
+
readonly models = [
|
|
73
|
+
{ id: 'claude-sonnet-4-6', label: 'Sonnet 4.6', default: true },
|
|
74
|
+
{ id: 'claude-opus-4-6', label: 'Opus 4.6' },
|
|
75
|
+
{ id: 'claude-haiku-4-5', label: 'Haiku 4.5' },
|
|
76
|
+
] as const;
|
|
77
|
+
|
|
78
|
+
private config: ClaudeAgentSDKConfig;
|
|
79
|
+
|
|
80
|
+
constructor(config: ClaudeAgentSDKConfig) {
|
|
81
|
+
this.config = config;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async createSession(options: CreateSessionOptions): Promise<AISession> {
|
|
85
|
+
return new ClaudeAgentSDKSession({
|
|
86
|
+
...this.baseConfig(options),
|
|
87
|
+
systemPrompt: buildSystemPrompt(options.context),
|
|
88
|
+
cwd: options.cwd ?? this.config.cwd ?? process.cwd(),
|
|
89
|
+
parentSessionId: null,
|
|
90
|
+
forkFromSession: null,
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async forkSession(options: CreateSessionOptions): Promise<AISession> {
|
|
95
|
+
const parent = options.context.parent;
|
|
96
|
+
if (!parent) {
|
|
97
|
+
throw new Error(
|
|
98
|
+
"Cannot fork: no parent session provided in context. " +
|
|
99
|
+
"Use createSession() for standalone sessions."
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return new ClaudeAgentSDKSession({
|
|
104
|
+
...this.baseConfig(options),
|
|
105
|
+
systemPrompt: null,
|
|
106
|
+
forkPreamble: buildForkPreamble(options.context),
|
|
107
|
+
cwd: parent.cwd,
|
|
108
|
+
parentSessionId: parent.sessionId,
|
|
109
|
+
forkFromSession: parent.sessionId,
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async resumeSession(sessionId: string): Promise<AISession> {
|
|
114
|
+
return new ClaudeAgentSDKSession({
|
|
115
|
+
...this.baseConfig(),
|
|
116
|
+
systemPrompt: null,
|
|
117
|
+
cwd: this.config.cwd ?? process.cwd(),
|
|
118
|
+
parentSessionId: null,
|
|
119
|
+
forkFromSession: null,
|
|
120
|
+
resumeSessionId: sessionId,
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
dispose(): void {
|
|
125
|
+
// No persistent resources to clean up
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
private baseConfig(options?: CreateSessionOptions) {
|
|
129
|
+
return {
|
|
130
|
+
model: options?.model ?? this.config.model ?? DEFAULT_MODEL,
|
|
131
|
+
maxTurns: options?.maxTurns ?? DEFAULT_MAX_TURNS,
|
|
132
|
+
maxBudgetUsd: options?.maxBudgetUsd,
|
|
133
|
+
allowedTools: this.config.allowedTools ?? DEFAULT_ALLOWED_TOOLS,
|
|
134
|
+
permissionMode: this.config.permissionMode ?? "default",
|
|
135
|
+
claudeExecutablePath: this.config.claudeExecutablePath,
|
|
136
|
+
settingSources: this.config.settingSources ?? ['user', 'project'],
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// ---------------------------------------------------------------------------
|
|
142
|
+
// SDK import cache — resolve once, reuse across all queries
|
|
143
|
+
// ---------------------------------------------------------------------------
|
|
144
|
+
|
|
145
|
+
// biome-ignore lint/suspicious/noExplicitAny: SDK types resolved at runtime via dynamic import
|
|
146
|
+
let sdkQueryFn: ((...args: any[]) => any) | null = null;
|
|
147
|
+
|
|
148
|
+
async function getSDKQuery() {
|
|
149
|
+
if (!sdkQueryFn) {
|
|
150
|
+
const sdk = await import("@anthropic-ai/claude-agent-sdk");
|
|
151
|
+
sdkQueryFn = sdk.query;
|
|
152
|
+
}
|
|
153
|
+
return sdkQueryFn!;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// ---------------------------------------------------------------------------
|
|
157
|
+
// Session
|
|
158
|
+
// ---------------------------------------------------------------------------
|
|
159
|
+
|
|
160
|
+
interface SessionConfig {
|
|
161
|
+
systemPrompt: string | null;
|
|
162
|
+
forkPreamble?: string;
|
|
163
|
+
model: string;
|
|
164
|
+
maxTurns: number;
|
|
165
|
+
maxBudgetUsd?: number;
|
|
166
|
+
allowedTools: string[];
|
|
167
|
+
permissionMode: ClaudeAgentSDKConfig['permissionMode'];
|
|
168
|
+
cwd: string;
|
|
169
|
+
parentSessionId: string | null;
|
|
170
|
+
forkFromSession: string | null;
|
|
171
|
+
resumeSessionId?: string;
|
|
172
|
+
claudeExecutablePath?: string;
|
|
173
|
+
settingSources?: string[];
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
class ClaudeAgentSDKSession extends BaseSession {
|
|
177
|
+
private config: SessionConfig;
|
|
178
|
+
/** Active Query object — needed to send control responses (permission decisions) */
|
|
179
|
+
private _activeQuery: { streamInput: (iter: AsyncIterable<unknown>) => Promise<void> } | null = null;
|
|
180
|
+
|
|
181
|
+
constructor(config: SessionConfig) {
|
|
182
|
+
super({
|
|
183
|
+
parentSessionId: config.parentSessionId,
|
|
184
|
+
initialId: config.resumeSessionId,
|
|
185
|
+
});
|
|
186
|
+
this.config = config;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
async *query(prompt: string): AsyncIterable<AIMessage> {
|
|
190
|
+
const started = this.startQuery();
|
|
191
|
+
if (!started) { yield BaseSession.BUSY_ERROR; return; }
|
|
192
|
+
const { gen } = started;
|
|
193
|
+
|
|
194
|
+
try {
|
|
195
|
+
const queryFn = await getSDKQuery();
|
|
196
|
+
|
|
197
|
+
const queryPrompt = buildEffectivePrompt(
|
|
198
|
+
prompt,
|
|
199
|
+
this.config.forkPreamble ?? null,
|
|
200
|
+
this._firstQuerySent,
|
|
201
|
+
);
|
|
202
|
+
const options = this.buildQueryOptions();
|
|
203
|
+
|
|
204
|
+
const stream = queryFn({ prompt: queryPrompt, options }) as
|
|
205
|
+
AsyncIterable<Record<string, unknown>> & { streamInput: (iter: AsyncIterable<unknown>) => Promise<void> };
|
|
206
|
+
this._activeQuery = stream;
|
|
207
|
+
|
|
208
|
+
this._firstQuerySent = true;
|
|
209
|
+
|
|
210
|
+
for await (const message of stream) {
|
|
211
|
+
const mapped = mapSDKMessage(message);
|
|
212
|
+
|
|
213
|
+
// Capture the real session ID from the init message
|
|
214
|
+
if (
|
|
215
|
+
!this._resolvedId &&
|
|
216
|
+
"session_id" in message &&
|
|
217
|
+
typeof message.session_id === "string" &&
|
|
218
|
+
message.session_id
|
|
219
|
+
) {
|
|
220
|
+
this.resolveId(message.session_id);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
for (const msg of mapped) {
|
|
224
|
+
yield msg;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
} catch (err) {
|
|
228
|
+
yield {
|
|
229
|
+
type: "error",
|
|
230
|
+
error: err instanceof Error ? err.message : String(err),
|
|
231
|
+
code: "provider_error",
|
|
232
|
+
};
|
|
233
|
+
} finally {
|
|
234
|
+
this.endQuery(gen);
|
|
235
|
+
this._activeQuery = null;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
abort(): void {
|
|
240
|
+
this._activeQuery = null;
|
|
241
|
+
super.abort();
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
respondToPermission(requestId: string, allow: boolean, message?: string): void {
|
|
245
|
+
if (!this._activeQuery || !this._activeQuery.streamInput) return;
|
|
246
|
+
|
|
247
|
+
const response = allow
|
|
248
|
+
? { type: 'control_response', response: { subtype: 'success', request_id: requestId, response: { behavior: 'allow' } } }
|
|
249
|
+
: { type: 'control_response', response: { subtype: 'success', request_id: requestId, response: { behavior: 'deny', message: message ?? 'User denied this action' } } };
|
|
250
|
+
|
|
251
|
+
this._activeQuery.streamInput(
|
|
252
|
+
(async function* () { yield response; })()
|
|
253
|
+
).catch(() => {});
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// -------------------------------------------------------------------------
|
|
257
|
+
// Internal
|
|
258
|
+
// -------------------------------------------------------------------------
|
|
259
|
+
|
|
260
|
+
private buildQueryOptions(): ClaudeSDKQueryOptions {
|
|
261
|
+
const opts: ClaudeSDKQueryOptions = {
|
|
262
|
+
model: this.config.model,
|
|
263
|
+
maxTurns: this.config.maxTurns,
|
|
264
|
+
allowedTools: this.config.allowedTools,
|
|
265
|
+
cwd: this.config.cwd,
|
|
266
|
+
abortController: this._currentAbort!,
|
|
267
|
+
includePartialMessages: true,
|
|
268
|
+
persistSession: true,
|
|
269
|
+
...(this.config.claudeExecutablePath && {
|
|
270
|
+
pathToClaudeCodeExecutable: this.config.claudeExecutablePath,
|
|
271
|
+
}),
|
|
272
|
+
...(this.config.settingSources && {
|
|
273
|
+
settingSources: this.config.settingSources,
|
|
274
|
+
}),
|
|
275
|
+
};
|
|
276
|
+
|
|
277
|
+
if (this.config.maxBudgetUsd) {
|
|
278
|
+
opts.maxBudgetUsd = this.config.maxBudgetUsd;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// After the first query resolves a real session ID, all subsequent
|
|
282
|
+
// queries must resume that session to continue the conversation.
|
|
283
|
+
if (this._resolvedId) {
|
|
284
|
+
opts.resume = this._resolvedId;
|
|
285
|
+
return this.applyPermissionMode(opts);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// First query: use Claude Code's built-in prompt with our context appended
|
|
289
|
+
if (this.config.systemPrompt) {
|
|
290
|
+
opts.systemPrompt = {
|
|
291
|
+
type: "preset",
|
|
292
|
+
preset: "claude_code",
|
|
293
|
+
append: this.config.systemPrompt,
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
if (this.config.forkFromSession) {
|
|
298
|
+
opts.resume = this.config.forkFromSession;
|
|
299
|
+
opts.forkSession = true;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
if (this.config.resumeSessionId) {
|
|
303
|
+
opts.resume = this.config.resumeSessionId;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
return this.applyPermissionMode(opts);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
private applyPermissionMode(opts: ClaudeSDKQueryOptions): ClaudeSDKQueryOptions {
|
|
310
|
+
if (this.config.permissionMode === "bypassPermissions") {
|
|
311
|
+
opts.permissionMode = "bypassPermissions";
|
|
312
|
+
opts.allowDangerouslySkipPermissions = true;
|
|
313
|
+
} else if (this.config.permissionMode === "plan") {
|
|
314
|
+
opts.permissionMode = "plan";
|
|
315
|
+
}
|
|
316
|
+
return opts;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// ---------------------------------------------------------------------------
|
|
321
|
+
// Message mapping
|
|
322
|
+
// ---------------------------------------------------------------------------
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* Map an SDK message to one or more AIMessages.
|
|
326
|
+
*
|
|
327
|
+
* An SDK assistant message can contain both text and tool_use content blocks
|
|
328
|
+
* in a single response. We emit each block as a separate AIMessage so no
|
|
329
|
+
* content is dropped.
|
|
330
|
+
*/
|
|
331
|
+
function mapSDKMessage(msg: Record<string, unknown>): AIMessage[] {
|
|
332
|
+
const type = msg.type as string;
|
|
333
|
+
|
|
334
|
+
switch (type) {
|
|
335
|
+
case "assistant": {
|
|
336
|
+
const message = msg.message as Record<string, unknown> | undefined;
|
|
337
|
+
if (!message) return [{ type: "unknown", raw: msg }];
|
|
338
|
+
const content = message.content as Array<Record<string, unknown>>;
|
|
339
|
+
if (!content) return [{ type: "unknown", raw: msg }];
|
|
340
|
+
|
|
341
|
+
const messages: AIMessage[] = [];
|
|
342
|
+
const textParts: string[] = [];
|
|
343
|
+
|
|
344
|
+
for (const block of content) {
|
|
345
|
+
if (block.type === "text" && typeof block.text === "string") {
|
|
346
|
+
textParts.push(block.text);
|
|
347
|
+
} else if (block.type === "tool_use") {
|
|
348
|
+
// Flush accumulated text before the tool_use block
|
|
349
|
+
if (textParts.length > 0) {
|
|
350
|
+
messages.push({ type: "text", text: textParts.join("") });
|
|
351
|
+
textParts.length = 0;
|
|
352
|
+
}
|
|
353
|
+
messages.push({
|
|
354
|
+
type: "tool_use",
|
|
355
|
+
toolName: block.name as string,
|
|
356
|
+
toolInput: block.input as Record<string, unknown>,
|
|
357
|
+
toolUseId: block.id as string,
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// Flush any remaining text after the last block
|
|
363
|
+
if (textParts.length > 0) {
|
|
364
|
+
messages.push({ type: "text", text: textParts.join("") });
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
return messages.length > 0 ? messages : [{ type: "unknown", raw: msg }];
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
case "stream_event": {
|
|
371
|
+
const event = msg.event as Record<string, unknown> | undefined;
|
|
372
|
+
if (!event) return [{ type: "unknown", raw: msg }];
|
|
373
|
+
const eventType = event.type as string;
|
|
374
|
+
|
|
375
|
+
if (eventType === "content_block_delta") {
|
|
376
|
+
const delta = event.delta as Record<string, unknown>;
|
|
377
|
+
if (delta?.type === "text_delta" && typeof delta.text === "string") {
|
|
378
|
+
return [{ type: "text_delta", delta: delta.text }];
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
return [{ type: "unknown", raw: msg }];
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
case "user": {
|
|
385
|
+
// SDK wraps tool results in SDKUserMessage (type: "user")
|
|
386
|
+
if (msg.tool_use_result != null) {
|
|
387
|
+
return [{
|
|
388
|
+
type: "tool_result",
|
|
389
|
+
result: typeof msg.tool_use_result === "string"
|
|
390
|
+
? msg.tool_use_result
|
|
391
|
+
: JSON.stringify(msg.tool_use_result),
|
|
392
|
+
}];
|
|
393
|
+
}
|
|
394
|
+
return [{ type: "unknown", raw: msg }];
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
case "control_request": {
|
|
398
|
+
const request = msg.request as Record<string, unknown> | undefined;
|
|
399
|
+
if (request?.subtype === "can_use_tool") {
|
|
400
|
+
return [{
|
|
401
|
+
type: "permission_request",
|
|
402
|
+
requestId: msg.request_id as string,
|
|
403
|
+
toolName: request.tool_name as string,
|
|
404
|
+
toolInput: (request.input as Record<string, unknown>) ?? {},
|
|
405
|
+
title: request.title as string | undefined,
|
|
406
|
+
displayName: request.display_name as string | undefined,
|
|
407
|
+
description: request.description as string | undefined,
|
|
408
|
+
toolUseId: request.tool_use_id as string,
|
|
409
|
+
}];
|
|
410
|
+
}
|
|
411
|
+
return [{ type: "unknown", raw: msg }];
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
case "result": {
|
|
415
|
+
const sessionId = (msg.session_id as string) ?? "";
|
|
416
|
+
const subtype = msg.subtype as string;
|
|
417
|
+
return [{
|
|
418
|
+
type: "result",
|
|
419
|
+
sessionId,
|
|
420
|
+
success: subtype === "success",
|
|
421
|
+
result: (msg.result as string) ?? undefined,
|
|
422
|
+
costUsd: msg.total_cost_usd as number | undefined,
|
|
423
|
+
turns: msg.num_turns as number | undefined,
|
|
424
|
+
}];
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
default:
|
|
428
|
+
return [{ type: "unknown", raw: msg }];
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
// ---------------------------------------------------------------------------
|
|
433
|
+
// Factory registration
|
|
434
|
+
// ---------------------------------------------------------------------------
|
|
435
|
+
|
|
436
|
+
import { registerProviderFactory } from "../provider.ts";
|
|
437
|
+
|
|
438
|
+
registerProviderFactory(
|
|
439
|
+
PROVIDER_NAME,
|
|
440
|
+
async (config) => new ClaudeAgentSDKProvider(config as ClaudeAgentSDKConfig)
|
|
441
|
+
);
|