@windowkit/agent-chat 0.0.0-stage → 0.3.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/LICENSE +21 -0
- package/README.md +147 -2
- package/dist/core/driver.d.ts +183 -0
- package/dist/core/driver.d.ts.map +1 -0
- package/dist/core/driver.js +12 -0
- package/dist/core/driver.js.map +1 -0
- package/dist/core/index.d.ts +8 -0
- package/dist/core/index.d.ts.map +1 -0
- package/dist/core/index.js +11 -0
- package/dist/core/index.js.map +1 -0
- package/dist/core/markdown.d.ts +7 -0
- package/dist/core/markdown.d.ts.map +1 -0
- package/dist/core/markdown.js +53 -0
- package/dist/core/markdown.js.map +1 -0
- package/dist/core/policy.d.ts +18 -0
- package/dist/core/policy.d.ts.map +1 -0
- package/dist/core/policy.js +67 -0
- package/dist/core/policy.js.map +1 -0
- package/dist/core/reducer.d.ts +61 -0
- package/dist/core/reducer.d.ts.map +1 -0
- package/dist/core/reducer.js +391 -0
- package/dist/core/reducer.js.map +1 -0
- package/dist/core/session.d.ts +105 -0
- package/dist/core/session.d.ts.map +1 -0
- package/dist/core/session.js +648 -0
- package/dist/core/session.js.map +1 -0
- package/dist/core/tools.d.ts +120 -0
- package/dist/core/tools.d.ts.map +1 -0
- package/dist/core/tools.js +120 -0
- package/dist/core/tools.js.map +1 -0
- package/dist/core/types.d.ts +295 -0
- package/dist/core/types.d.ts.map +1 -0
- package/dist/core/types.js +21 -0
- package/dist/core/types.js.map +1 -0
- package/dist/drivers/claude/index.d.ts +20 -0
- package/dist/drivers/claude/index.d.ts.map +1 -0
- package/dist/drivers/claude/index.js +727 -0
- package/dist/drivers/claude/index.js.map +1 -0
- package/dist/drivers/claude/input.d.ts +7 -0
- package/dist/drivers/claude/input.d.ts.map +1 -0
- package/dist/drivers/claude/input.js +46 -0
- package/dist/drivers/claude/input.js.map +1 -0
- package/dist/drivers/claude/peers.d.ts +16 -0
- package/dist/drivers/claude/peers.d.ts.map +1 -0
- package/dist/drivers/claude/peers.js +137 -0
- package/dist/drivers/claude/peers.js.map +1 -0
- package/dist/drivers/claude/schema.d.ts +5 -0
- package/dist/drivers/claude/schema.d.ts.map +1 -0
- package/dist/drivers/claude/schema.js +77 -0
- package/dist/drivers/claude/schema.js.map +1 -0
- package/dist/drivers/claude/sdk.d.ts +86 -0
- package/dist/drivers/claude/sdk.d.ts.map +1 -0
- package/dist/drivers/claude/sdk.js +47 -0
- package/dist/drivers/claude/sdk.js.map +1 -0
- package/dist/drivers/claude/tools.d.ts +18 -0
- package/dist/drivers/claude/tools.d.ts.map +1 -0
- package/dist/drivers/claude/tools.js +119 -0
- package/dist/drivers/claude/tools.js.map +1 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +12 -0
- package/dist/index.js.map +1 -0
- package/dist/testing/fake.d.ts +63 -0
- package/dist/testing/fake.d.ts.map +1 -0
- package/dist/testing/fake.js +270 -0
- package/dist/testing/fake.js.map +1 -0
- package/dist/testing/index.d.ts +2 -0
- package/dist/testing/index.d.ts.map +1 -0
- package/dist/testing/index.js +4 -0
- package/dist/testing/index.js.map +1 -0
- package/dist/view/AgentChat.d.ts +53 -0
- package/dist/view/AgentChat.d.ts.map +1 -0
- package/dist/view/AgentChat.js +87 -0
- package/dist/view/AgentChat.js.map +1 -0
- package/dist/view/CodeBlock.d.ts +9 -0
- package/dist/view/CodeBlock.d.ts.map +1 -0
- package/dist/view/CodeBlock.js +67 -0
- package/dist/view/CodeBlock.js.map +1 -0
- package/dist/view/Composer.d.ts +24 -0
- package/dist/view/Composer.d.ts.map +1 -0
- package/dist/view/Composer.js +144 -0
- package/dist/view/Composer.js.map +1 -0
- package/dist/view/Header.d.ts +3 -0
- package/dist/view/Header.d.ts.map +1 -0
- package/dist/view/Header.js +30 -0
- package/dist/view/Header.js.map +1 -0
- package/dist/view/MessageList.d.ts +9 -0
- package/dist/view/MessageList.d.ts.map +1 -0
- package/dist/view/MessageList.js +111 -0
- package/dist/view/MessageList.js.map +1 -0
- package/dist/view/QuestionCard.d.ts +6 -0
- package/dist/view/QuestionCard.d.ts.map +1 -0
- package/dist/view/QuestionCard.js +39 -0
- package/dist/view/QuestionCard.js.map +1 -0
- package/dist/view/TextBlock.d.ts +9 -0
- package/dist/view/TextBlock.d.ts.map +1 -0
- package/dist/view/TextBlock.js +25 -0
- package/dist/view/TextBlock.js.map +1 -0
- package/dist/view/ToolCard.d.ts +7 -0
- package/dist/view/ToolCard.d.ts.map +1 -0
- package/dist/view/ToolCard.js +155 -0
- package/dist/view/ToolCard.js.map +1 -0
- package/dist/view/Transcript.d.ts +5 -0
- package/dist/view/Transcript.d.ts.map +1 -0
- package/dist/view/Transcript.js +56 -0
- package/dist/view/Transcript.js.map +1 -0
- package/dist/view/context.d.ts +21 -0
- package/dist/view/context.d.ts.map +1 -0
- package/dist/view/context.js +37 -0
- package/dist/view/context.js.map +1 -0
- package/dist/view/fences.d.ts +13 -0
- package/dist/view/fences.d.ts.map +1 -0
- package/dist/view/fences.js +59 -0
- package/dist/view/fences.js.map +1 -0
- package/dist/view/files.d.ts +11 -0
- package/dist/view/files.d.ts.map +1 -0
- package/dist/view/files.js +109 -0
- package/dist/view/files.js.map +1 -0
- package/dist/view/look.d.ts +25 -0
- package/dist/view/look.d.ts.map +1 -0
- package/dist/view/look.js +47 -0
- package/dist/view/look.js.map +1 -0
- package/dist/view/types.d.ts +50 -0
- package/dist/view/types.d.ts.map +1 -0
- package/dist/view/types.js +14 -0
- package/dist/view/types.js.map +1 -0
- package/dist/view/use-agent.d.ts +11 -0
- package/dist/view/use-agent.d.ts.map +1 -0
- package/dist/view/use-agent.js +33 -0
- package/dist/view/use-agent.js.map +1 -0
- package/package.json +92 -4
- package/src/core/driver.ts +191 -0
- package/src/core/index.ts +54 -0
- package/src/core/markdown.ts +56 -0
- package/src/core/policy.ts +77 -0
- package/src/core/reducer.ts +490 -0
- package/src/core/session.ts +824 -0
- package/src/core/tools.ts +233 -0
- package/src/core/types.ts +321 -0
- package/src/drivers/claude/index.ts +825 -0
- package/src/drivers/claude/input.ts +47 -0
- package/src/drivers/claude/peers.ts +153 -0
- package/src/drivers/claude/schema.ts +99 -0
- package/src/drivers/claude/sdk.ts +138 -0
- package/src/drivers/claude/tools.ts +141 -0
- package/src/index.ts +51 -0
- package/src/testing/fake.ts +374 -0
- package/src/testing/index.ts +11 -0
- package/src/view/AgentChat.tsx +204 -0
- package/src/view/CodeBlock.tsx +115 -0
- package/src/view/Composer.tsx +230 -0
- package/src/view/Header.tsx +73 -0
- package/src/view/MessageList.tsx +290 -0
- package/src/view/QuestionCard.tsx +107 -0
- package/src/view/TextBlock.tsx +61 -0
- package/src/view/ToolCard.tsx +283 -0
- package/src/view/Transcript.tsx +91 -0
- package/src/view/context.ts +70 -0
- package/src/view/dom-globals.d.ts +10 -0
- package/src/view/fences.ts +73 -0
- package/src/view/files.ts +111 -0
- package/src/view/look.ts +70 -0
- package/src/view/types.ts +71 -0
- package/src/view/use-agent.ts +40 -0
|
@@ -0,0 +1,824 @@
|
|
|
1
|
+
// A session: the transcript, the harness behind it, and every question the
|
|
2
|
+
// harness asks the client — answered by the app's policy, or by the user.
|
|
3
|
+
//
|
|
4
|
+
// It is a store in the `useSyncExternalStore` sense: `snapshot` is one
|
|
5
|
+
// immutable object, replaced on every change, and `subscribe` hears each
|
|
6
|
+
// replacement. Nothing in it is React's, so a terminal or DOM front end can
|
|
7
|
+
// drive the same object.
|
|
8
|
+
|
|
9
|
+
import {
|
|
10
|
+
AgentUnavailableError,
|
|
11
|
+
type AgentDriver,
|
|
12
|
+
type ApprovalResult,
|
|
13
|
+
type DriverConfig,
|
|
14
|
+
type DriverEvent,
|
|
15
|
+
type DriverHost,
|
|
16
|
+
type DriverSession,
|
|
17
|
+
type GateDecision,
|
|
18
|
+
type OutgoingMessage,
|
|
19
|
+
} from './driver.js';
|
|
20
|
+
import { transcriptMarkdown } from './markdown.js';
|
|
21
|
+
import { policyDecision } from './policy.js';
|
|
22
|
+
import {
|
|
23
|
+
reduce,
|
|
24
|
+
restore,
|
|
25
|
+
findLivePart,
|
|
26
|
+
type TranscriptAction,
|
|
27
|
+
} from './reducer.js';
|
|
28
|
+
import {
|
|
29
|
+
errorResult,
|
|
30
|
+
normalizeResult,
|
|
31
|
+
validateInput,
|
|
32
|
+
type AnyToolDef,
|
|
33
|
+
type ToolResult,
|
|
34
|
+
} from './tools.js';
|
|
35
|
+
import {
|
|
36
|
+
NO_CAPABILITIES,
|
|
37
|
+
type AgentMessage,
|
|
38
|
+
type AgentStatus,
|
|
39
|
+
type ApprovalChoice,
|
|
40
|
+
type ApprovalOption,
|
|
41
|
+
type Capabilities,
|
|
42
|
+
type ComposedMessage,
|
|
43
|
+
type CoreToolKey,
|
|
44
|
+
type Part,
|
|
45
|
+
type PermissionPolicy,
|
|
46
|
+
type Question,
|
|
47
|
+
type SessionInfo,
|
|
48
|
+
type ToolCall,
|
|
49
|
+
type ToolDecision,
|
|
50
|
+
} from './types.js';
|
|
51
|
+
|
|
52
|
+
/** A `coreTools` value: on, off, always asking, or replaced by a tool. */
|
|
53
|
+
export type CoreToolSetting = boolean | 'ask' | AnyToolDef;
|
|
54
|
+
|
|
55
|
+
export interface AgentOptions {
|
|
56
|
+
/** Which harness. Default `'claude'`: the Claude Agent SDK. */
|
|
57
|
+
agent?: 'claude' | AgentDriver;
|
|
58
|
+
/** Where it works. Default: the process's working directory. */
|
|
59
|
+
cwd?: string;
|
|
60
|
+
/** More directories it may read and write. */
|
|
61
|
+
folders?: readonly string[];
|
|
62
|
+
/** Appended to the harness's own system prompt. An array is joined with
|
|
63
|
+
* blank lines — `describeRenderers()` returns one. */
|
|
64
|
+
instructions?: string | readonly string[];
|
|
65
|
+
/** Replaces the harness's system prompt. `instructions` still follow it. */
|
|
66
|
+
systemPrompt?: string;
|
|
67
|
+
/**
|
|
68
|
+
* The harness's built-in tools, by what they do — `shell`, `read`, `edit`,
|
|
69
|
+
* `web`, `agents`, `todos` — or by a harness's own name (`NotebookEdit`).
|
|
70
|
+
* `false` removes one, `'ask'` makes it always ask, and a tool of the app's
|
|
71
|
+
* replaces it, which the agent then reaches by the built-in's name.
|
|
72
|
+
*/
|
|
73
|
+
coreTools?: Readonly<Partial<Record<CoreToolKey, CoreToolSetting>>> &
|
|
74
|
+
Readonly<Record<string, CoreToolSetting | undefined>>;
|
|
75
|
+
/** The app's own tools, by name. */
|
|
76
|
+
tools?: Readonly<Record<string, AnyToolDef>>;
|
|
77
|
+
/** What runs without asking. Default `'ask'`. */
|
|
78
|
+
permissions?: PermissionPolicy;
|
|
79
|
+
/** Before every tool call the session sees: the app's say, ahead of the
|
|
80
|
+
* policy. `undefined` leaves it to the policy. */
|
|
81
|
+
onToolCall?: (call: ToolCall) => ToolDecision | Promise<ToolDecision>;
|
|
82
|
+
model?: string;
|
|
83
|
+
mode?: string;
|
|
84
|
+
/** A harness session to resume, by its id. */
|
|
85
|
+
resume?: string;
|
|
86
|
+
/**
|
|
87
|
+
* The transcript to start from: an earlier session's `snapshot.messages`,
|
|
88
|
+
* saved by the app. It is shown as it was, with anything left open
|
|
89
|
+
* settled as a stopped turn. The harness remembers the conversation only
|
|
90
|
+
* through `resume`, so pass its `info.id` beside it.
|
|
91
|
+
*/
|
|
92
|
+
messages?: readonly AgentMessage[];
|
|
93
|
+
/**
|
|
94
|
+
* `'cli'` (default): the harness behaves as it does on the command line,
|
|
95
|
+
* with the user's settings and project instructions (`CLAUDE.md`).
|
|
96
|
+
* `'isolated'`: none of them.
|
|
97
|
+
*/
|
|
98
|
+
settings?: 'cli' | 'isolated';
|
|
99
|
+
/** Passed to the driver as they are, after everything above. */
|
|
100
|
+
driverOptions?: Readonly<Record<string, unknown>>;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export interface SessionSnapshot {
|
|
104
|
+
readonly status: AgentStatus;
|
|
105
|
+
/** The transcript. */
|
|
106
|
+
readonly messages: readonly AgentMessage[];
|
|
107
|
+
readonly info: SessionInfo;
|
|
108
|
+
readonly capabilities: Capabilities;
|
|
109
|
+
/** Why the status is `'error'` or `'unavailable'`. */
|
|
110
|
+
readonly error?: string;
|
|
111
|
+
readonly paused: boolean;
|
|
112
|
+
/** Text taken back out of the queue by `stop()`, for the composer. */
|
|
113
|
+
readonly restored?: string;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export interface AgentSession {
|
|
117
|
+
readonly snapshot: SessionSnapshot;
|
|
118
|
+
/** Bound, so `useSyncExternalStore(session.subscribe, …)` works as is. */
|
|
119
|
+
readonly subscribe: (listener: () => void) => () => void;
|
|
120
|
+
/** The app's tools, replacements included, by name: what the view asks
|
|
121
|
+
* for a tool's `render`. */
|
|
122
|
+
readonly tools: ReadonlyMap<string, AnyToolDef>;
|
|
123
|
+
/** Start the harness now, rather than at the first `send`. */
|
|
124
|
+
start(): Promise<void>;
|
|
125
|
+
/**
|
|
126
|
+
* Send a message: the user's, or, with `from`, another session's. Sent
|
|
127
|
+
* mid-turn it steers where the harness can, and otherwise waits its turn.
|
|
128
|
+
*/
|
|
129
|
+
send(input: string | ComposedMessage): void;
|
|
130
|
+
/** Stop the turn. Queued messages come back as `snapshot.restored`. */
|
|
131
|
+
stop(): Promise<void>;
|
|
132
|
+
/** Hold every tool call at the gate. Needs `capabilities.gate` to be
|
|
133
|
+
* `'every-call'`; elsewhere it holds only the calls the agent asks about. */
|
|
134
|
+
pause(): void;
|
|
135
|
+
resume(): void;
|
|
136
|
+
/** Answer a tool call waiting for approval. */
|
|
137
|
+
respond(toolId: string, choice: ApprovalChoice, note?: string): void;
|
|
138
|
+
/** Answer a question; `null` dismisses it. */
|
|
139
|
+
answer(questionId: string, answers: Record<string, string> | null): void;
|
|
140
|
+
setModel(id: string): Promise<void>;
|
|
141
|
+
setMode(id: string): Promise<void>;
|
|
142
|
+
/** A line in the transcript from the app: an action's result, a failure. */
|
|
143
|
+
notify(level: 'info' | 'warning' | 'error', text: string): void;
|
|
144
|
+
/** The transcript as markdown, for Copy. */
|
|
145
|
+
toMarkdown(): string;
|
|
146
|
+
/** Mark `restored` as taken. */
|
|
147
|
+
takeRestored(): string | undefined;
|
|
148
|
+
close(): Promise<void>;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const APPROVAL_OPTIONS: readonly ApprovalOption[] = Object.freeze([
|
|
152
|
+
{ id: 'allow-once', label: 'Allow', kind: 'allow' },
|
|
153
|
+
{ id: 'allow-session', label: 'Allow for this session', kind: 'allow' },
|
|
154
|
+
{ id: 'deny', label: 'Deny', kind: 'deny' },
|
|
155
|
+
]);
|
|
156
|
+
|
|
157
|
+
const BUSY: ReadonlySet<AgentStatus> = new Set([
|
|
158
|
+
'starting',
|
|
159
|
+
'running',
|
|
160
|
+
'waiting',
|
|
161
|
+
'paused',
|
|
162
|
+
]);
|
|
163
|
+
|
|
164
|
+
export function createAgentSession(options: AgentOptions = {}): AgentSession {
|
|
165
|
+
return new Session(options);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** Lines that tell the agent what the UI can draw — see `describe` on a
|
|
169
|
+
* fence or a component. */
|
|
170
|
+
export function describeRenderers(
|
|
171
|
+
renderers: {
|
|
172
|
+
fences?: Readonly<Record<string, unknown>>;
|
|
173
|
+
components?: Readonly<Record<string, unknown>>;
|
|
174
|
+
} = {},
|
|
175
|
+
): string[] {
|
|
176
|
+
const lines: string[] = [];
|
|
177
|
+
for (const [lang, entry] of Object.entries(renderers.fences ?? {})) {
|
|
178
|
+
const describe = describeOf(entry);
|
|
179
|
+
if (describe) {
|
|
180
|
+
lines.push(
|
|
181
|
+
`The UI draws a fenced code block tagged \`${lang}\` as: ${describe}`,
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
for (const [name, entry] of Object.entries(renderers.components ?? {})) {
|
|
186
|
+
const describe = describeOf(entry);
|
|
187
|
+
if (describe) {
|
|
188
|
+
lines.push(
|
|
189
|
+
`The UI draws a <${name}/> tag, alone on its line, as: ${describe}. ` +
|
|
190
|
+
'Its attributes are strings or JSON.',
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
return lines.length
|
|
195
|
+
? ['You are shown in a chat UI that renders markdown.', ...lines]
|
|
196
|
+
: [];
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function describeOf(entry: unknown): string | undefined {
|
|
200
|
+
return entry && typeof entry === 'object' && 'describe' in entry
|
|
201
|
+
? String((entry as { describe: unknown }).describe)
|
|
202
|
+
: undefined;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
class Session implements AgentSession {
|
|
206
|
+
readonly #options: AgentOptions;
|
|
207
|
+
readonly #config: DriverConfig;
|
|
208
|
+
#snapshot: SessionSnapshot;
|
|
209
|
+
readonly #listeners = new Set<() => void>();
|
|
210
|
+
#driver: DriverSession | null = null;
|
|
211
|
+
#starting: Promise<DriverSession | null> | null = null;
|
|
212
|
+
#turn: AbortController | null = null;
|
|
213
|
+
readonly #queue: OutgoingMessage[] = [];
|
|
214
|
+
#ids = 0;
|
|
215
|
+
|
|
216
|
+
readonly #decisions = new Map<string, GateDecision>();
|
|
217
|
+
readonly #sessionAllowed = new Set<string>();
|
|
218
|
+
readonly #approvals = new Map<string, (r: ApprovalResult) => void>();
|
|
219
|
+
readonly #questions = new Map<
|
|
220
|
+
string,
|
|
221
|
+
(a: Record<string, string> | null) => void
|
|
222
|
+
>();
|
|
223
|
+
#paused = false;
|
|
224
|
+
readonly #resumers = new Set<() => void>();
|
|
225
|
+
|
|
226
|
+
constructor(options: AgentOptions) {
|
|
227
|
+
this.#options = options;
|
|
228
|
+
this.#config = resolveConfig(options);
|
|
229
|
+
const messages = restore(options.messages ?? []);
|
|
230
|
+
// the user's next message continues the saved ones' numbering, so its
|
|
231
|
+
// id is not one already in the transcript
|
|
232
|
+
for (const m of messages) {
|
|
233
|
+
const n = /^u(\d+)$/.exec(m.id);
|
|
234
|
+
if (n) this.#ids = Math.max(this.#ids, Number(n[1]));
|
|
235
|
+
}
|
|
236
|
+
const driverName =
|
|
237
|
+
typeof options.agent === 'object' ? options.agent.name : 'claude';
|
|
238
|
+
this.#snapshot = Object.freeze({
|
|
239
|
+
status: 'idle',
|
|
240
|
+
messages,
|
|
241
|
+
info: {
|
|
242
|
+
agent: driverName,
|
|
243
|
+
cwd: this.#config.cwd,
|
|
244
|
+
models: [],
|
|
245
|
+
modes: [],
|
|
246
|
+
commands: [],
|
|
247
|
+
},
|
|
248
|
+
capabilities: NO_CAPABILITIES,
|
|
249
|
+
paused: false,
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
get snapshot(): SessionSnapshot {
|
|
254
|
+
return this.#snapshot;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
readonly subscribe = (listener: () => void): (() => void) => {
|
|
258
|
+
this.#listeners.add(listener);
|
|
259
|
+
return () => {
|
|
260
|
+
this.#listeners.delete(listener);
|
|
261
|
+
};
|
|
262
|
+
};
|
|
263
|
+
|
|
264
|
+
get tools(): ReadonlyMap<string, AnyToolDef> {
|
|
265
|
+
return this.#config.tools;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
#set(patch: Partial<SessionSnapshot>): void {
|
|
269
|
+
this.#snapshot = Object.freeze({ ...this.#snapshot, ...patch });
|
|
270
|
+
for (const l of [...this.#listeners]) l();
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
#dispatch(action: TranscriptAction): void {
|
|
274
|
+
const messages = reduce(this.#snapshot.messages, action);
|
|
275
|
+
if (messages !== this.#snapshot.messages) this.#set({ messages });
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// --- the harness -----------------------------------------------------------
|
|
279
|
+
|
|
280
|
+
start(): Promise<void> {
|
|
281
|
+
return this.#start().then(() => undefined);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
#start(): Promise<DriverSession | null> {
|
|
285
|
+
if (this.#driver) return Promise.resolve(this.#driver);
|
|
286
|
+
if (this.#starting) return this.#starting;
|
|
287
|
+
this.#set({ status: 'starting', error: undefined });
|
|
288
|
+
this.#starting = (async () => {
|
|
289
|
+
try {
|
|
290
|
+
const driver = await loadDriver(this.#options.agent);
|
|
291
|
+
const session = await driver.start(this.#host());
|
|
292
|
+
this.#driver = session;
|
|
293
|
+
this.#set({
|
|
294
|
+
capabilities: session.capabilities,
|
|
295
|
+
status: this.#turn ? 'running' : 'idle',
|
|
296
|
+
});
|
|
297
|
+
return session;
|
|
298
|
+
} catch (err) {
|
|
299
|
+
const unavailable = err instanceof AgentUnavailableError;
|
|
300
|
+
this.#set({
|
|
301
|
+
status: unavailable ? 'unavailable' : 'error',
|
|
302
|
+
error: err instanceof Error ? err.message : String(err),
|
|
303
|
+
});
|
|
304
|
+
this.#turn = null;
|
|
305
|
+
return null;
|
|
306
|
+
} finally {
|
|
307
|
+
this.#starting = null;
|
|
308
|
+
}
|
|
309
|
+
})();
|
|
310
|
+
return this.#starting;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
#host(): DriverHost {
|
|
314
|
+
return {
|
|
315
|
+
config: this.#config,
|
|
316
|
+
emit: (event) => this.#onEvent(event),
|
|
317
|
+
gate: (call) => this.#gate(call, true),
|
|
318
|
+
approve: (call, signal) => this.#approve(call, signal),
|
|
319
|
+
ask: (id, questions, signal) => this.#ask(id, questions, signal),
|
|
320
|
+
runTool: (name, input, ctx) => this.#runTool(name, input, ctx),
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
#onEvent(event: DriverEvent): void {
|
|
325
|
+
switch (event.type) {
|
|
326
|
+
case 'info':
|
|
327
|
+
this.#set({ info: { ...this.#snapshot.info, ...event.info } });
|
|
328
|
+
return;
|
|
329
|
+
case 'usage':
|
|
330
|
+
this.#set({
|
|
331
|
+
info: {
|
|
332
|
+
...this.#snapshot.info,
|
|
333
|
+
usage: { ...this.#snapshot.info.usage, ...event.usage },
|
|
334
|
+
},
|
|
335
|
+
});
|
|
336
|
+
return;
|
|
337
|
+
case 'message':
|
|
338
|
+
// the harness took it and is acting on it: a turn, if none runs
|
|
339
|
+
this.#dispatch(event);
|
|
340
|
+
if (!this.#turn) {
|
|
341
|
+
this.#turn = new AbortController();
|
|
342
|
+
this.#set({ status: this.#paused ? 'paused' : 'running' });
|
|
343
|
+
}
|
|
344
|
+
return;
|
|
345
|
+
case 'turn-end':
|
|
346
|
+
this.#dispatch(event);
|
|
347
|
+
if (event.reason === 'error' && event.error) {
|
|
348
|
+
this.#dispatch({ type: 'notice', level: 'error', text: event.error });
|
|
349
|
+
}
|
|
350
|
+
this.#endTurn();
|
|
351
|
+
return;
|
|
352
|
+
default:
|
|
353
|
+
this.#dispatch(event);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
#endTurn(): void {
|
|
358
|
+
this.#turn = null;
|
|
359
|
+
this.#decisions.clear();
|
|
360
|
+
const next = this.#queue.shift();
|
|
361
|
+
if (next) {
|
|
362
|
+
this.#dispatch({ type: 'unqueue', id: next.id });
|
|
363
|
+
void this.#deliver(next);
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
if (BUSY.has(this.#snapshot.status)) this.#set({ status: 'idle' });
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// --- the user's side -------------------------------------------------------
|
|
370
|
+
|
|
371
|
+
send(input: string | ComposedMessage): void {
|
|
372
|
+
const composed: ComposedMessage =
|
|
373
|
+
typeof input === 'string' ? { text: input } : input;
|
|
374
|
+
const text = composed.text;
|
|
375
|
+
const attachments = composed.attachments ?? [];
|
|
376
|
+
const from = composed.from;
|
|
377
|
+
if (!text.trim() && !attachments.length) return;
|
|
378
|
+
if (this.#snapshot.status === 'closed') return;
|
|
379
|
+
|
|
380
|
+
const id = `u${++this.#ids}`;
|
|
381
|
+
const parts: Part[] = [];
|
|
382
|
+
if (text)
|
|
383
|
+
parts.push({ type: 'text', id: `${id}:t`, text, streaming: false });
|
|
384
|
+
attachments.forEach((a, i) =>
|
|
385
|
+
parts.push({
|
|
386
|
+
type: 'file',
|
|
387
|
+
id: `${id}:f${i}`,
|
|
388
|
+
label: a.label,
|
|
389
|
+
mime: a.mime ?? (a.kind === 'image' ? 'image/png' : 'text/plain'),
|
|
390
|
+
...(a.path ? { path: a.path } : {}),
|
|
391
|
+
...(a.data !== undefined ? { data: a.data } : {}),
|
|
392
|
+
}),
|
|
393
|
+
);
|
|
394
|
+
|
|
395
|
+
const busy = !!this.#turn;
|
|
396
|
+
const steer = busy && this.#snapshot.capabilities.steer;
|
|
397
|
+
const queued = busy && !steer;
|
|
398
|
+
this.#dispatch({
|
|
399
|
+
type: 'user',
|
|
400
|
+
message: {
|
|
401
|
+
id,
|
|
402
|
+
role: 'user',
|
|
403
|
+
parts,
|
|
404
|
+
createdAt: Date.now(),
|
|
405
|
+
...(from ? { from } : {}),
|
|
406
|
+
...(queued ? { queued: true } : {}),
|
|
407
|
+
},
|
|
408
|
+
});
|
|
409
|
+
const out: OutgoingMessage = {
|
|
410
|
+
id,
|
|
411
|
+
text,
|
|
412
|
+
attachments,
|
|
413
|
+
steer,
|
|
414
|
+
...(from ? { from } : {}),
|
|
415
|
+
};
|
|
416
|
+
if (queued) this.#queue.push(out);
|
|
417
|
+
else void this.#deliver(out);
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
async #deliver(message: OutgoingMessage): Promise<void> {
|
|
421
|
+
if (!message.steer) this.#turn = new AbortController();
|
|
422
|
+
if (!this.#paused) this.#set({ status: 'running', restored: undefined });
|
|
423
|
+
const driver = await this.#start();
|
|
424
|
+
if (!driver) return;
|
|
425
|
+
if (!this.#turn && !message.steer) return; // stopped while starting
|
|
426
|
+
if (!BUSY.has(this.#snapshot.status)) this.#set({ status: 'running' });
|
|
427
|
+
try {
|
|
428
|
+
driver.send(message);
|
|
429
|
+
} catch (err) {
|
|
430
|
+
this.#onEvent({
|
|
431
|
+
type: 'turn-end',
|
|
432
|
+
reason: 'error',
|
|
433
|
+
error: err instanceof Error ? err.message : String(err),
|
|
434
|
+
});
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
async stop(): Promise<void> {
|
|
439
|
+
if (!this.#turn && !this.#queue.length) return;
|
|
440
|
+
// what the user typed goes back to the composer; what another session
|
|
441
|
+
// sent is not theirs to edit, and goes with the turn
|
|
442
|
+
const restored = this.#queue
|
|
443
|
+
.filter((m) => !m.from)
|
|
444
|
+
.map((m) => m.text)
|
|
445
|
+
.join('\n\n');
|
|
446
|
+
const dropped = new Set(this.#queue.map((m) => m.id));
|
|
447
|
+
this.#queue.length = 0;
|
|
448
|
+
this.#turn?.abort();
|
|
449
|
+
for (const [id, resolve] of this.#approvals) {
|
|
450
|
+
this.#approvals.delete(id);
|
|
451
|
+
resolve({
|
|
452
|
+
behavior: 'deny',
|
|
453
|
+
message: 'Stopped by the user.',
|
|
454
|
+
interrupt: true,
|
|
455
|
+
});
|
|
456
|
+
}
|
|
457
|
+
for (const [id, resolve] of this.#questions) {
|
|
458
|
+
this.#questions.delete(id);
|
|
459
|
+
resolve(null);
|
|
460
|
+
}
|
|
461
|
+
this.#wake();
|
|
462
|
+
if (dropped.size) {
|
|
463
|
+
this.#set({
|
|
464
|
+
messages: this.#snapshot.messages.filter((m) => !dropped.has(m.id)),
|
|
465
|
+
restored: restored || undefined,
|
|
466
|
+
});
|
|
467
|
+
}
|
|
468
|
+
const driver = this.#driver;
|
|
469
|
+
this.#dispatch({ type: 'stopped' });
|
|
470
|
+
this.#turn = null;
|
|
471
|
+
this.#decisions.clear();
|
|
472
|
+
this.#set({ status: 'idle' });
|
|
473
|
+
try {
|
|
474
|
+
await driver?.interrupt();
|
|
475
|
+
} catch {
|
|
476
|
+
// the harness is already gone; the transcript says stopped either way
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
takeRestored(): string | undefined {
|
|
481
|
+
const restored = this.#snapshot.restored;
|
|
482
|
+
if (restored !== undefined) this.#set({ restored: undefined });
|
|
483
|
+
return restored;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
pause(): void {
|
|
487
|
+
if (this.#paused) return;
|
|
488
|
+
this.#paused = true;
|
|
489
|
+
this.#set({
|
|
490
|
+
paused: true,
|
|
491
|
+
...(this.#turn ? { status: 'paused' as const } : {}),
|
|
492
|
+
});
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
resume(): void {
|
|
496
|
+
if (!this.#paused) return;
|
|
497
|
+
this.#paused = false;
|
|
498
|
+
this.#set({
|
|
499
|
+
paused: false,
|
|
500
|
+
...(this.#snapshot.status === 'paused'
|
|
501
|
+
? {
|
|
502
|
+
status:
|
|
503
|
+
this.#approvals.size || this.#questions.size
|
|
504
|
+
? 'waiting'
|
|
505
|
+
: 'running',
|
|
506
|
+
}
|
|
507
|
+
: {}),
|
|
508
|
+
} as Partial<SessionSnapshot>);
|
|
509
|
+
this.#wake();
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
#wake(): void {
|
|
513
|
+
const resumers = [...this.#resumers];
|
|
514
|
+
this.#resumers.clear();
|
|
515
|
+
for (const r of resumers) r();
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
#unpaused(signal?: AbortSignal): Promise<void> {
|
|
519
|
+
if (!this.#paused || signal?.aborted || !this.#turn)
|
|
520
|
+
return Promise.resolve();
|
|
521
|
+
return new Promise((resolve) => {
|
|
522
|
+
const done = () => {
|
|
523
|
+
this.#resumers.delete(done);
|
|
524
|
+
signal?.removeEventListener('abort', done);
|
|
525
|
+
resolve();
|
|
526
|
+
};
|
|
527
|
+
this.#resumers.add(done);
|
|
528
|
+
signal?.addEventListener('abort', done, { once: true });
|
|
529
|
+
});
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
respond(toolId: string, choice: ApprovalChoice, note?: string): void {
|
|
533
|
+
const resolve = this.#approvals.get(toolId);
|
|
534
|
+
if (!resolve) return;
|
|
535
|
+
this.#approvals.delete(toolId);
|
|
536
|
+
if (choice === 'deny') {
|
|
537
|
+
resolve({
|
|
538
|
+
behavior: 'deny',
|
|
539
|
+
message: note?.trim()
|
|
540
|
+
? `Denied by the user: ${note.trim()}`
|
|
541
|
+
: 'Denied by the user.',
|
|
542
|
+
});
|
|
543
|
+
return;
|
|
544
|
+
}
|
|
545
|
+
if (choice === 'allow-session') {
|
|
546
|
+
const part = findLivePart(this.#snapshot.messages, toolId);
|
|
547
|
+
if (part?.type === 'tool') this.#sessionAllowed.add(part.label);
|
|
548
|
+
}
|
|
549
|
+
resolve({ behavior: 'allow' });
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
answer(questionId: string, answers: Record<string, string> | null): void {
|
|
553
|
+
const resolve = this.#questions.get(questionId);
|
|
554
|
+
if (!resolve) return;
|
|
555
|
+
this.#questions.delete(questionId);
|
|
556
|
+
resolve(answers);
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
async setModel(id: string): Promise<void> {
|
|
560
|
+
const driver = this.#driver ?? (await this.#start());
|
|
561
|
+
if (!driver?.setModel) return;
|
|
562
|
+
await driver.setModel(id);
|
|
563
|
+
this.#set({ info: { ...this.#snapshot.info, model: id } });
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
async setMode(id: string): Promise<void> {
|
|
567
|
+
const driver = this.#driver ?? (await this.#start());
|
|
568
|
+
if (!driver?.setMode) return;
|
|
569
|
+
await driver.setMode(id);
|
|
570
|
+
this.#set({ info: { ...this.#snapshot.info, mode: id } });
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
notify(level: 'info' | 'warning' | 'error', text: string): void {
|
|
574
|
+
// a notice goes in the turn under way, or in a message of its own
|
|
575
|
+
if (!this.#turn) {
|
|
576
|
+
this.#dispatch({
|
|
577
|
+
type: 'user',
|
|
578
|
+
message: {
|
|
579
|
+
id: `n${++this.#ids}`,
|
|
580
|
+
role: 'assistant',
|
|
581
|
+
parts: [{ type: 'notice', id: `n${this.#ids}:0`, level, text }],
|
|
582
|
+
createdAt: Date.now(),
|
|
583
|
+
done: true,
|
|
584
|
+
},
|
|
585
|
+
});
|
|
586
|
+
return;
|
|
587
|
+
}
|
|
588
|
+
this.#dispatch({ type: 'notice', level, text });
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
toMarkdown(): string {
|
|
592
|
+
return transcriptMarkdown(this.#snapshot.messages);
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
async close(): Promise<void> {
|
|
596
|
+
if (this.#snapshot.status === 'closed') return;
|
|
597
|
+
await this.stop();
|
|
598
|
+
const driver = this.#driver;
|
|
599
|
+
this.#driver = null;
|
|
600
|
+
this.#set({ status: 'closed' });
|
|
601
|
+
await driver?.close();
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
// --- the harness's questions -------------------------------------------------
|
|
605
|
+
|
|
606
|
+
async #gate(call: ToolCall, remember: boolean): Promise<GateDecision> {
|
|
607
|
+
if (this.#paused && this.#turn) return { behavior: 'ask' };
|
|
608
|
+
const known = this.#decisions.get(call.id);
|
|
609
|
+
if (known) return known;
|
|
610
|
+
if (this.#sessionAllowed.has(call.label)) return { behavior: 'allow' };
|
|
611
|
+
|
|
612
|
+
let decision: GateDecision;
|
|
613
|
+
let fromApp: ToolDecision;
|
|
614
|
+
try {
|
|
615
|
+
fromApp = await this.#options.onToolCall?.(call);
|
|
616
|
+
} catch (err) {
|
|
617
|
+
fromApp = {
|
|
618
|
+
deny: `The app's check failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
619
|
+
};
|
|
620
|
+
}
|
|
621
|
+
if (fromApp === undefined) decision = policyDecision(call, this.#config);
|
|
622
|
+
else if (fromApp === 'allow') decision = { behavior: 'allow' };
|
|
623
|
+
else if (fromApp === 'allow-session') {
|
|
624
|
+
this.#sessionAllowed.add(call.label);
|
|
625
|
+
decision = { behavior: 'allow' };
|
|
626
|
+
} else if (fromApp === 'deny') {
|
|
627
|
+
decision = { behavior: 'deny', message: 'Denied by the app.' };
|
|
628
|
+
} else if (fromApp === 'ask') decision = { behavior: 'ask' };
|
|
629
|
+
else if ('deny' in fromApp)
|
|
630
|
+
decision = { behavior: 'deny', message: fromApp.deny };
|
|
631
|
+
else decision = { behavior: 'allow', input: fromApp.input };
|
|
632
|
+
|
|
633
|
+
if (remember) this.#decisions.set(call.id, decision);
|
|
634
|
+
if (decision.behavior === 'deny') {
|
|
635
|
+
this.#dispatch({ type: 'approved', id: call.id, allowed: false });
|
|
636
|
+
}
|
|
637
|
+
return decision;
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
async #approve(
|
|
641
|
+
call: ToolCall,
|
|
642
|
+
signal?: AbortSignal,
|
|
643
|
+
): Promise<ApprovalResult> {
|
|
644
|
+
await this.#unpaused(signal);
|
|
645
|
+
if (signal?.aborted || !this.#turn) {
|
|
646
|
+
return {
|
|
647
|
+
behavior: 'deny',
|
|
648
|
+
message: 'Stopped by the user.',
|
|
649
|
+
interrupt: true,
|
|
650
|
+
};
|
|
651
|
+
}
|
|
652
|
+
const decision = await this.#gate(call, true);
|
|
653
|
+
if (decision.behavior === 'allow') {
|
|
654
|
+
return { behavior: 'allow', input: decision.input };
|
|
655
|
+
}
|
|
656
|
+
if (decision.behavior === 'deny') {
|
|
657
|
+
return { behavior: 'deny', message: decision.message };
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
if (!findLivePart(this.#snapshot.messages, call.id)) {
|
|
661
|
+
this.#dispatch({
|
|
662
|
+
type: 'tool-start',
|
|
663
|
+
id: call.id,
|
|
664
|
+
name: call.name,
|
|
665
|
+
label: call.label,
|
|
666
|
+
kind: call.kind,
|
|
667
|
+
title: call.title,
|
|
668
|
+
input: call.input,
|
|
669
|
+
app: call.app,
|
|
670
|
+
});
|
|
671
|
+
}
|
|
672
|
+
this.#dispatch({
|
|
673
|
+
type: 'approval',
|
|
674
|
+
id: call.id,
|
|
675
|
+
options: APPROVAL_OPTIONS,
|
|
676
|
+
});
|
|
677
|
+
this.#set({ status: 'waiting' });
|
|
678
|
+
const result = await new Promise<ApprovalResult>((resolve) => {
|
|
679
|
+
this.#approvals.set(call.id, resolve);
|
|
680
|
+
signal?.addEventListener(
|
|
681
|
+
'abort',
|
|
682
|
+
() => {
|
|
683
|
+
if (!this.#approvals.delete(call.id)) return;
|
|
684
|
+
resolve({ behavior: 'deny', message: 'Stopped.', interrupt: true });
|
|
685
|
+
},
|
|
686
|
+
{ once: true },
|
|
687
|
+
);
|
|
688
|
+
});
|
|
689
|
+
this.#decisions.set(
|
|
690
|
+
call.id,
|
|
691
|
+
result.behavior === 'allow'
|
|
692
|
+
? { behavior: 'allow', input: result.input }
|
|
693
|
+
: { behavior: 'deny', message: result.message },
|
|
694
|
+
);
|
|
695
|
+
this.#dispatch({
|
|
696
|
+
type: 'approved',
|
|
697
|
+
id: call.id,
|
|
698
|
+
allowed: result.behavior === 'allow',
|
|
699
|
+
});
|
|
700
|
+
this.#settleWaiting();
|
|
701
|
+
return result;
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
async #ask(
|
|
705
|
+
id: string,
|
|
706
|
+
questions: readonly Question[],
|
|
707
|
+
signal?: AbortSignal,
|
|
708
|
+
): Promise<Record<string, string> | null> {
|
|
709
|
+
this.#dispatch({ type: 'question', id, questions });
|
|
710
|
+
this.#set({ status: 'waiting' });
|
|
711
|
+
const answers = await new Promise<Record<string, string> | null>(
|
|
712
|
+
(resolve) => {
|
|
713
|
+
this.#questions.set(id, resolve);
|
|
714
|
+
signal?.addEventListener(
|
|
715
|
+
'abort',
|
|
716
|
+
() => {
|
|
717
|
+
if (this.#questions.delete(id)) resolve(null);
|
|
718
|
+
},
|
|
719
|
+
{ once: true },
|
|
720
|
+
);
|
|
721
|
+
},
|
|
722
|
+
);
|
|
723
|
+
this.#dispatch({ type: 'answered', id, answers });
|
|
724
|
+
this.#settleWaiting();
|
|
725
|
+
return answers;
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
#settleWaiting(): void {
|
|
729
|
+
if (this.#approvals.size || this.#questions.size) return;
|
|
730
|
+
if (this.#snapshot.status === 'waiting') {
|
|
731
|
+
this.#set({
|
|
732
|
+
status: this.#turn ? (this.#paused ? 'paused' : 'running') : 'idle',
|
|
733
|
+
});
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
async #runTool(
|
|
738
|
+
name: string,
|
|
739
|
+
input: unknown,
|
|
740
|
+
ctx: { callId?: string; signal?: AbortSignal },
|
|
741
|
+
): Promise<ToolResult> {
|
|
742
|
+
const tool = this.#config.tools.get(name);
|
|
743
|
+
if (!tool) return errorResult(new Error(`No tool named ${name}.`));
|
|
744
|
+
const checked = await validateInput(tool.parameters, input);
|
|
745
|
+
if (!checked.ok) return errorResult(new Error(checked.message));
|
|
746
|
+
const callId = ctx.callId ?? `${name}:${++this.#ids}`;
|
|
747
|
+
const signal =
|
|
748
|
+
ctx.signal ?? this.#turn?.signal ?? new AbortController().signal;
|
|
749
|
+
if (ctx.callId) {
|
|
750
|
+
this.#dispatch({ type: 'tool-update', id: callId, state: 'running' });
|
|
751
|
+
}
|
|
752
|
+
try {
|
|
753
|
+
const returned = await tool.run(checked.value, {
|
|
754
|
+
signal,
|
|
755
|
+
callId,
|
|
756
|
+
progress: (text) => {
|
|
757
|
+
if (ctx.callId) {
|
|
758
|
+
this.#dispatch({ type: 'tool-update', id: callId, progress: text });
|
|
759
|
+
}
|
|
760
|
+
},
|
|
761
|
+
});
|
|
762
|
+
const { result, output } = normalizeResult(returned);
|
|
763
|
+
if (ctx.callId) {
|
|
764
|
+
this.#dispatch({ type: 'tool-update', id: callId, output });
|
|
765
|
+
}
|
|
766
|
+
return result;
|
|
767
|
+
} catch (err) {
|
|
768
|
+
if (ctx.callId) {
|
|
769
|
+
this.#dispatch({
|
|
770
|
+
type: 'tool-update',
|
|
771
|
+
id: callId,
|
|
772
|
+
output: [],
|
|
773
|
+
});
|
|
774
|
+
}
|
|
775
|
+
return errorResult(err);
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
function resolveConfig(options: AgentOptions): DriverConfig {
|
|
781
|
+
const tools = new Map<string, AnyToolDef>(
|
|
782
|
+
Object.entries(options.tools ?? {}),
|
|
783
|
+
);
|
|
784
|
+
const removed: string[] = [];
|
|
785
|
+
const asking: string[] = [];
|
|
786
|
+
const replaced: Partial<Record<CoreToolKey, string>> = {};
|
|
787
|
+
for (const [key, setting] of Object.entries(options.coreTools ?? {})) {
|
|
788
|
+
if (setting === undefined || setting === true) continue;
|
|
789
|
+
if (setting === false) removed.push(key);
|
|
790
|
+
else if (setting === 'ask') asking.push(key);
|
|
791
|
+
else {
|
|
792
|
+
tools.set(key, setting);
|
|
793
|
+
replaced[key as CoreToolKey] = key;
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
const instructions = Array.isArray(options.instructions)
|
|
797
|
+
? options.instructions.filter(Boolean).join('\n\n')
|
|
798
|
+
: ((options.instructions as string | undefined) ?? '');
|
|
799
|
+
return {
|
|
800
|
+
cwd: options.cwd ?? process.cwd(),
|
|
801
|
+
folders: options.folders ?? [],
|
|
802
|
+
instructions,
|
|
803
|
+
...(options.systemPrompt !== undefined
|
|
804
|
+
? { systemPrompt: options.systemPrompt }
|
|
805
|
+
: {}),
|
|
806
|
+
removed,
|
|
807
|
+
asking,
|
|
808
|
+
replaced,
|
|
809
|
+
tools,
|
|
810
|
+
permissions: options.permissions ?? 'ask',
|
|
811
|
+
...(options.model ? { model: options.model } : {}),
|
|
812
|
+
...(options.mode ? { mode: options.mode } : {}),
|
|
813
|
+
...(options.resume ? { resume: options.resume } : {}),
|
|
814
|
+
settings: options.settings ?? 'cli',
|
|
815
|
+
driverOptions: options.driverOptions ?? {},
|
|
816
|
+
};
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
async function loadDriver(agent: AgentOptions['agent']): Promise<AgentDriver> {
|
|
820
|
+
if (agent && typeof agent === 'object') return agent;
|
|
821
|
+
// A dynamic import, so a bundle that never names Claude carries none of it.
|
|
822
|
+
const { claudeDriver } = await import('../drivers/claude/index.js');
|
|
823
|
+
return claudeDriver();
|
|
824
|
+
}
|