@agent-api/app-engine 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.
Files changed (62) hide show
  1. package/README.md +46 -0
  2. package/dist/agent/runner.d.ts +117 -0
  3. package/dist/agent/runner.js +486 -0
  4. package/dist/agent.d.ts +2 -0
  5. package/dist/agent.js +2 -0
  6. package/dist/chat-options.d.ts +37 -0
  7. package/dist/chat-options.js +42 -0
  8. package/dist/config.d.ts +66 -0
  9. package/dist/config.js +201 -0
  10. package/dist/conversation/index.d.ts +17 -0
  11. package/dist/conversation/index.js +54 -0
  12. package/dist/engine/agent-engine.d.ts +38 -0
  13. package/dist/engine/agent-engine.js +146 -0
  14. package/dist/engine/index.d.ts +50 -0
  15. package/dist/engine/index.js +26 -0
  16. package/dist/engine/services.d.ts +20 -0
  17. package/dist/engine/services.js +1 -0
  18. package/dist/index.d.ts +1 -0
  19. package/dist/index.js +1 -0
  20. package/dist/profile.d.ts +57 -0
  21. package/dist/profile.js +211 -0
  22. package/dist/runtime/index.d.ts +23 -0
  23. package/dist/runtime/index.js +177 -0
  24. package/dist/update.d.ts +16 -0
  25. package/dist/update.js +74 -0
  26. package/dist/workbench/auth-controller.d.ts +43 -0
  27. package/dist/workbench/auth-controller.js +84 -0
  28. package/dist/workbench/auth-gate-controller.d.ts +62 -0
  29. package/dist/workbench/auth-gate-controller.js +231 -0
  30. package/dist/workbench/command-controller.d.ts +29 -0
  31. package/dist/workbench/command-controller.js +426 -0
  32. package/dist/workbench/conversation-controller.d.ts +32 -0
  33. package/dist/workbench/conversation-controller.js +53 -0
  34. package/dist/workbench/engine.d.ts +66 -0
  35. package/dist/workbench/engine.js +291 -0
  36. package/dist/workbench/input-controller.d.ts +44 -0
  37. package/dist/workbench/input-controller.js +71 -0
  38. package/dist/workbench/isolator-installer.d.ts +29 -0
  39. package/dist/workbench/isolator-installer.js +208 -0
  40. package/dist/workbench/lifecycle-controller.d.ts +30 -0
  41. package/dist/workbench/lifecycle-controller.js +75 -0
  42. package/dist/workbench/local-controller.d.ts +21 -0
  43. package/dist/workbench/local-controller.js +94 -0
  44. package/dist/workbench/render-model.d.ts +46 -0
  45. package/dist/workbench/render-model.js +61 -0
  46. package/dist/workbench/runtime-controller.d.ts +12 -0
  47. package/dist/workbench/runtime-controller.js +57 -0
  48. package/dist/workbench/session.d.ts +29 -0
  49. package/dist/workbench/session.js +42 -0
  50. package/dist/workbench/settings-controller.d.ts +80 -0
  51. package/dist/workbench/settings-controller.js +309 -0
  52. package/dist/workbench/shell-isolation.d.ts +20 -0
  53. package/dist/workbench/shell-isolation.js +13 -0
  54. package/dist/workbench/state.d.ts +187 -0
  55. package/dist/workbench/state.js +392 -0
  56. package/dist/workbench/turn-controller.d.ts +25 -0
  57. package/dist/workbench/turn-controller.js +164 -0
  58. package/dist/workbench/view-model.d.ts +34 -0
  59. package/dist/workbench/view-model.js +121 -0
  60. package/dist/workdir/index.d.ts +22 -0
  61. package/dist/workdir/index.js +46 -0
  62. package/package.json +50 -0
package/README.md ADDED
@@ -0,0 +1,46 @@
1
+ # Agent API App Engine
2
+
3
+ Renderer-neutral application engine for Agent API apps.
4
+
5
+ `@agent-api/app-engine` contains the reusable core behind `@agent-api/cli`: auth profile handling, conversation state, workdir context, local tool orchestration, isolator configuration, and the workbench state machine. It does not depend on Ink, React, or any terminal renderer.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ npm install @agent-api/app-engine
11
+ ```
12
+
13
+ ## Usage
14
+
15
+ ```ts
16
+ import {
17
+ configureAgentAppRuntime,
18
+ createAgentEngine,
19
+ createWorkbenchAuthController,
20
+ } from "@agent-api/app-engine";
21
+
22
+ configureAgentAppRuntime({
23
+ appName: "my-agent-app",
24
+ appAuthor: "My Company",
25
+ appVersion: "1.0.0",
26
+ });
27
+
28
+ const engine = createAgentEngine();
29
+ const auth = createWorkbenchAuthController();
30
+ ```
31
+
32
+ Host applications should call `configureAgentAppRuntime()` during startup so config, profiles, and runtime files live under the host app's own platform config directory.
33
+
34
+ ## Boundaries
35
+
36
+ - This package owns core application state and side effects.
37
+ - Renderers own input widgets, layout, keyboard handling, and screen drawing.
38
+ - The CLI/TUI package should import core behavior from this package rather than from private source paths.
39
+
40
+ ## Local Development
41
+
42
+ ```bash
43
+ npm install
44
+ npm run build -w @agent-api/app-engine
45
+ npm run smoke -w @agent-api/app-engine
46
+ ```
@@ -0,0 +1,117 @@
1
+ import { type AgentResponse, type PresetToolCatalogClient, type ResponseStreamEvent, type Tool } from "@agent-api/sdk";
2
+ import type { ShellIsolationPreferences } from "../workbench/shell-isolation.js";
3
+ export interface AgentRunOptions {
4
+ profile?: string;
5
+ promptParts: string[];
6
+ file?: string;
7
+ stdin?: boolean;
8
+ preset?: string;
9
+ presetExplicit?: boolean;
10
+ model?: string;
11
+ modelExplicit?: boolean;
12
+ stream?: boolean;
13
+ conversation?: string;
14
+ continueConversation?: boolean;
15
+ restartConversation?: boolean;
16
+ previousResponseId?: string;
17
+ workdir?: string;
18
+ includeLocalContext?: boolean;
19
+ contextQuery?: string;
20
+ maxContextFiles?: number;
21
+ maxContextBytes?: number;
22
+ accessMode?: WorkdirAccessMode;
23
+ shellIsolation?: ShellIsolationPreferences;
24
+ abortSignal?: AbortSignal;
25
+ }
26
+ export type WorkdirAccessMode = "off" | "approval" | "full";
27
+ export interface AgentTurnResult {
28
+ text: string;
29
+ responseID?: string;
30
+ }
31
+ export interface LocalToolApprovalRequest {
32
+ name: string;
33
+ action?: string;
34
+ arguments: Record<string, unknown>;
35
+ preview?: unknown;
36
+ callID: string;
37
+ responseID: string;
38
+ }
39
+ export type AgentTurnEvent = {
40
+ type: "text.delta";
41
+ delta: string;
42
+ } | {
43
+ type: "response.started";
44
+ responseID?: string;
45
+ } | {
46
+ type: "response.completed";
47
+ responseID?: string;
48
+ } | {
49
+ type: "response.failed";
50
+ message: string;
51
+ } | {
52
+ type: "reasoning.started";
53
+ } | {
54
+ type: "reasoning.stopped";
55
+ thought?: string;
56
+ } | {
57
+ type: "reasoning.search_queries";
58
+ queries: string[];
59
+ } | {
60
+ type: "reasoning.search_results";
61
+ count: number;
62
+ } | {
63
+ type: "reasoning.fetch_url_queries";
64
+ urls: string[];
65
+ } | {
66
+ type: "reasoning.fetch_url_results";
67
+ count: number;
68
+ } | {
69
+ type: "tool.completed";
70
+ name: string;
71
+ status?: string;
72
+ } | {
73
+ type: "local_tool.completed";
74
+ name: string;
75
+ action?: string;
76
+ requiresApproval?: boolean;
77
+ } | ({
78
+ type: "local_tool.approval_requested";
79
+ } & LocalToolApprovalRequest) | {
80
+ type: "model.requested";
81
+ model?: string;
82
+ provider?: string;
83
+ } | {
84
+ type: "model.completed";
85
+ model?: string;
86
+ provider?: string;
87
+ } | {
88
+ type: "model.failed";
89
+ model?: string;
90
+ provider?: string;
91
+ } | {
92
+ type: "step.completed";
93
+ stepType?: string;
94
+ } | {
95
+ type: "step.failed";
96
+ stepType?: string;
97
+ } | {
98
+ type: "raw";
99
+ eventType: string;
100
+ };
101
+ export interface ResolveAgentRequestToolsOptions {
102
+ baseURL?: string;
103
+ cacheTTLMS?: number;
104
+ }
105
+ export interface PresetSummary {
106
+ preset: string;
107
+ description?: string;
108
+ }
109
+ export declare function runAgent(options: AgentRunOptions): Promise<void>;
110
+ export declare function runAgentTurn(options: AgentRunOptions, onEvent?: (event: AgentTurnEvent) => void): Promise<AgentTurnResult>;
111
+ export declare function resumeAgentAfterLocalApproval(options: AgentRunOptions, approval: LocalToolApprovalRequest, output: string | Record<string, unknown>, onEvent?: (event: AgentTurnEvent) => void): Promise<AgentTurnResult>;
112
+ export declare function listAvailablePresets(profileName?: string): Promise<PresetSummary[]>;
113
+ export declare function isAvailablePreset(profileName: string | undefined, preset: string): Promise<boolean>;
114
+ export declare function agentResponseFailureMessage(response: AgentResponse): string;
115
+ export declare function agentTurnEventFromStreamEvent(event: ResponseStreamEvent): AgentTurnEvent | null;
116
+ export declare function resolveAgentRequestTools(client: PresetToolCatalogClient, preset?: string, tools?: readonly Tool[], options?: ResolveAgentRequestToolsOptions): Promise<Tool[] | undefined>;
117
+ export declare function clearPresetToolCatalogCache(baseURL?: string): void;
@@ -0,0 +1,486 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { functionCallOutputInput, pendingFunctionCalls, resolvePresetTools, resolvePresetToolsFromCatalog, } from "@agent-api/sdk";
3
+ import { createLocalShellToolRegistry, createLocalWorkdirToolRegistry, localShellToolInstructions, localWorkdirToolInstructions, } from "@agent-api/sdk/local";
4
+ import { resolvePreviousResponseID, updateConversation } from "../conversation/index.js";
5
+ import { resolveRuntimeProfile } from "../profile.js";
6
+ import { buildWorkdirContextBlock, openWorkdir } from "../workdir/index.js";
7
+ import { localShellIsolationOptions } from "../workbench/shell-isolation.js";
8
+ const defaultCatalogCacheTTLMS = 10 * 60_000;
9
+ const presetCatalogCache = new Map();
10
+ const toolCatalogCache = new Map();
11
+ export async function runAgent(options) {
12
+ const result = await runAgentTurn(options, (event) => {
13
+ if (event.type === "text.delta")
14
+ process.stdout.write(event.delta);
15
+ });
16
+ if (result.text && options.stream === false) {
17
+ console.log(result.text);
18
+ }
19
+ else if (options.stream !== false) {
20
+ process.stdout.write("\n");
21
+ }
22
+ }
23
+ export async function runAgentTurn(options, onEvent) {
24
+ const runtimeProfile = await resolveRuntimeProfile(options.profile);
25
+ const localWorkdir = await prepareLocalWorkdirTools(options);
26
+ const input = await buildInput(options);
27
+ const previousResponseId = await resolvePreviousResponseID(options);
28
+ const tools = await resolveAgentRequestTools(runtimeProfile.client, options.preset, localWorkdir?.registry.definitions(), { baseURL: runtimeProfile.profile.baseURL });
29
+ const params = {
30
+ input,
31
+ instructions: localWorkdir?.instructions,
32
+ tools,
33
+ preset: options.preset,
34
+ model: options.model,
35
+ previous_response_id: previousResponseId,
36
+ stream: options.stream ?? true,
37
+ };
38
+ if (localWorkdir) {
39
+ return await runAgentTurnWithLocalTools(runtimeProfile.client.agent, runtimeProfile.client.responses, params, options, localWorkdir.registry, onEvent);
40
+ }
41
+ if (params.stream) {
42
+ const stream = await runtimeProfile.client.agent.create({ ...params, stream: true }, requestAbortOptions(options.abortSignal));
43
+ let finalResponseID = "";
44
+ let text = "";
45
+ for await (const event of stream) {
46
+ throwIfAborted(options.abortSignal);
47
+ emitAgentTurnEvent(event, onEvent);
48
+ if (event.type === "response.output_text.delta" && event.delta) {
49
+ text += event.delta;
50
+ }
51
+ if (event.response?.id) {
52
+ finalResponseID = event.response.id;
53
+ }
54
+ if (event.type === "response.failed") {
55
+ const message = event.error?.message || "agent run failed";
56
+ throw new Error(message);
57
+ }
58
+ }
59
+ if (finalResponseID) {
60
+ await updateConversation(options, finalResponseID);
61
+ }
62
+ return { text, responseID: finalResponseID || undefined };
63
+ }
64
+ const response = await runtimeProfile.client.agent.create({ ...params, stream: false }, requestAbortOptions(options.abortSignal));
65
+ await updateConversation(options, response.id);
66
+ return { text: response.output_text || "", responseID: response.id };
67
+ }
68
+ export async function resumeAgentAfterLocalApproval(options, approval, output, onEvent) {
69
+ const runtimeProfile = await resolveRuntimeProfile(options.profile);
70
+ const localWorkdir = await prepareLocalWorkdirTools(options);
71
+ if (!localWorkdir) {
72
+ throw new Error("local tools are not available for approval continuation");
73
+ }
74
+ const tools = await resolveAgentRequestTools(runtimeProfile.client, options.preset, localWorkdir.registry.definitions(), { baseURL: runtimeProfile.profile.baseURL });
75
+ return await runAgentTurnWithLocalTools(runtimeProfile.client.agent, runtimeProfile.client.responses, {
76
+ input: [functionCallOutputInput(approval.callID, output)],
77
+ instructions: localWorkdir.instructions,
78
+ tools,
79
+ preset: options.preset,
80
+ model: options.model,
81
+ previous_response_id: approval.responseID,
82
+ stream: options.stream ?? true,
83
+ }, options, localWorkdir.registry, onEvent);
84
+ }
85
+ export async function listAvailablePresets(profileName) {
86
+ const runtimeProfile = await resolveRuntimeProfile(profileName);
87
+ const presets = await cachedPresetCatalog(runtimeProfile.profile.baseURL, defaultCatalogCacheTTLMS, () => runtimeProfile.client.presets.list());
88
+ return presets.data
89
+ .map((preset) => {
90
+ const item = preset;
91
+ const name = typeof item.preset === "string"
92
+ ? item.preset
93
+ : typeof item.name === "string"
94
+ ? item.name
95
+ : "";
96
+ return {
97
+ preset: name,
98
+ description: typeof item.description === "string" ? item.description : undefined,
99
+ };
100
+ })
101
+ .filter((preset) => preset.preset.length > 0)
102
+ .sort((a, b) => a.preset.localeCompare(b.preset));
103
+ }
104
+ export async function isAvailablePreset(profileName, preset) {
105
+ const available = await listAvailablePresets(profileName);
106
+ return available.some((item) => item.preset === preset);
107
+ }
108
+ async function runAgentTurnWithLocalTools(agent, responses, initialParams, options, registry, onEvent) {
109
+ let input = initialParams.input;
110
+ let previousResponseID = initialParams.previous_response_id;
111
+ let finalResponse;
112
+ const maxLocalToolRounds = 8;
113
+ for (let round = 0; round <= maxLocalToolRounds; round += 1) {
114
+ throwIfAborted(options.abortSignal);
115
+ const response = await createAgentResponseWithOptionalStream(agent, {
116
+ input,
117
+ instructions: initialParams.instructions,
118
+ tools: initialParams.tools,
119
+ preset: initialParams.preset,
120
+ model: initialParams.model,
121
+ previous_response_id: previousResponseID,
122
+ stream: initialParams.stream,
123
+ }, responses, options.abortSignal, onEvent);
124
+ throwIfAborted(options.abortSignal);
125
+ finalResponse = response;
126
+ if (initialParams.stream === false) {
127
+ onEvent?.({ type: "response.started", responseID: response.id });
128
+ }
129
+ if (response.status === "failed") {
130
+ const message = agentResponseFailureMessage(response);
131
+ onEvent?.({ type: "response.failed", message });
132
+ throw new Error(message);
133
+ }
134
+ const pending = pendingFunctionCalls(response);
135
+ if (pending.length === 0) {
136
+ if (initialParams.stream === false) {
137
+ onEvent?.({ type: "response.completed", responseID: response.id });
138
+ }
139
+ await updateConversation(options, response.id);
140
+ return { text: response.output_text || "", responseID: response.id };
141
+ }
142
+ const localResult = await executeLocalFunctionCalls(response, registry, options.abortSignal, onEvent);
143
+ if (localResult.approvalRequested) {
144
+ const message = localApprovalMessage(localResult.approvalRequested);
145
+ if (initialParams.stream !== false) {
146
+ onEvent?.({ type: "text.delta", delta: message });
147
+ }
148
+ if (initialParams.stream === false) {
149
+ onEvent?.({ type: "response.completed", responseID: response.id });
150
+ }
151
+ await updateConversation(options, response.id);
152
+ return {
153
+ text: message,
154
+ responseID: response.id,
155
+ };
156
+ }
157
+ const outputs = localResult.outputs;
158
+ input = outputs;
159
+ previousResponseID = response.id;
160
+ }
161
+ throw new Error(`local function loop exceeded ${maxLocalToolRounds} rounds after ${finalResponse?.id || "initial response"}`);
162
+ }
163
+ async function createAgentResponseWithOptionalStream(agent, params, responses, abortSignal, onEvent) {
164
+ throwIfAborted(abortSignal);
165
+ if (!params.stream) {
166
+ const response = await agent.create({ ...params, stream: false }, requestAbortOptions(abortSignal));
167
+ throwIfAborted(abortSignal);
168
+ return response;
169
+ }
170
+ const stream = await agent.create({ ...params, stream: true }, requestAbortOptions(abortSignal));
171
+ let finalResponse;
172
+ let responseID = "";
173
+ let sawTextDelta = false;
174
+ for await (const event of stream) {
175
+ throwIfAborted(abortSignal);
176
+ emitAgentTurnEvent(event, onEvent);
177
+ if (event.type === "response.output_text.delta") {
178
+ sawTextDelta = true;
179
+ }
180
+ if (event.response?.id) {
181
+ responseID = event.response.id;
182
+ }
183
+ if (event.type === "response.completed" && event.response) {
184
+ finalResponse = withOutputText(event.response);
185
+ }
186
+ if (event.type === "response.failed") {
187
+ throw new Error(event.error?.message || "agent run failed");
188
+ }
189
+ }
190
+ throwIfAborted(abortSignal);
191
+ if (!finalResponse && responseID) {
192
+ finalResponse = await responses.retrieve(responseID, requestAbortOptions(abortSignal));
193
+ }
194
+ if (!finalResponse) {
195
+ throw new Error("agent stream completed without a final response");
196
+ }
197
+ if (!sawTextDelta && finalResponse.output_text) {
198
+ onEvent?.({ type: "text.delta", delta: finalResponse.output_text });
199
+ }
200
+ return finalResponse;
201
+ }
202
+ export function agentResponseFailureMessage(response) {
203
+ const details = [];
204
+ if (response.id)
205
+ details.push(response.id);
206
+ if (response.model)
207
+ details.push(`model=${response.model}`);
208
+ const label = details.length > 0 ? `Agent response ${details.join(" ")}` : "Agent response";
209
+ const message = response.error?.message || "agent run failed";
210
+ const code = response.error?.code || response.error?.type;
211
+ return code ? `${label} failed: ${message} (${code})` : `${label} failed: ${message}`;
212
+ }
213
+ function withOutputText(response) {
214
+ if (response.output_text !== undefined) {
215
+ return response;
216
+ }
217
+ const outputText = response.output
218
+ .filter((item) => item.type === "message")
219
+ .flatMap((item) => item.content)
220
+ .filter((part) => part.type === "output_text" && typeof part.text === "string")
221
+ .map((part) => part.text)
222
+ .join("");
223
+ return { ...response, output_text: outputText };
224
+ }
225
+ export function agentTurnEventFromStreamEvent(event) {
226
+ const responseID = event.response?.id;
227
+ switch (event.type) {
228
+ case "response.created":
229
+ case "response.in_progress":
230
+ return { type: "response.started", responseID };
231
+ case "response.output_text.delta":
232
+ return event.delta ? { type: "text.delta", delta: event.delta } : null;
233
+ case "response.completed":
234
+ return { type: "response.completed", responseID };
235
+ case "response.failed":
236
+ return { type: "response.failed", message: event.error?.message || "agent run failed" };
237
+ case "response.reasoning.started":
238
+ return { type: "reasoning.started" };
239
+ case "response.reasoning.stopped":
240
+ return { type: "reasoning.stopped", thought: event.thought };
241
+ case "response.reasoning.search_queries":
242
+ return { type: "reasoning.search_queries", queries: event.queries ?? [] };
243
+ case "response.reasoning.search_results":
244
+ return { type: "reasoning.search_results", count: event.results?.length ?? 0 };
245
+ case "response.reasoning.fetch_url_queries":
246
+ return { type: "reasoning.fetch_url_queries", urls: event.urls ?? [] };
247
+ case "response.reasoning.fetch_url_results":
248
+ return { type: "reasoning.fetch_url_results", count: event.contents?.length ?? 0 };
249
+ case "response.tool.invocation.completed":
250
+ return {
251
+ type: "tool.completed",
252
+ name: event.tool_result?.tool_name || "tool",
253
+ status: event.tool_result?.status,
254
+ };
255
+ case "response.model.requested":
256
+ return {
257
+ type: "model.requested",
258
+ model: event.model_call?.model,
259
+ provider: event.model_call?.provider,
260
+ };
261
+ case "response.model.completed":
262
+ return {
263
+ type: "model.completed",
264
+ model: event.model_call?.model,
265
+ provider: event.model_call?.provider,
266
+ };
267
+ case "response.model.failed":
268
+ return {
269
+ type: "model.failed",
270
+ model: event.model_call?.model,
271
+ provider: event.model_call?.provider,
272
+ };
273
+ case "response.step.completed":
274
+ return { type: "step.completed", stepType: event.step?.step_type };
275
+ case "response.step.failed":
276
+ return { type: "step.failed", stepType: event.step?.step_type };
277
+ default:
278
+ return { type: "raw", eventType: event.type };
279
+ }
280
+ }
281
+ function emitAgentTurnEvent(event, onEvent) {
282
+ if (!onEvent)
283
+ return;
284
+ const mapped = agentTurnEventFromStreamEvent(event);
285
+ if (mapped)
286
+ onEvent(mapped);
287
+ }
288
+ export async function resolveAgentRequestTools(client, preset, tools, options = {}) {
289
+ if (!tools || tools.length === 0) {
290
+ return undefined;
291
+ }
292
+ if (!preset) {
293
+ return [...tools];
294
+ }
295
+ if (options.baseURL) {
296
+ const ttl = options.cacheTTLMS ?? defaultCatalogCacheTTLMS;
297
+ const [presets, toolCatalog] = await Promise.all([
298
+ cachedPresetCatalog(options.baseURL, ttl, () => client.presets.list()),
299
+ cachedToolCatalog(options.baseURL, ttl, () => client.tools.list()),
300
+ ]);
301
+ const resolved = resolvePresetToolsFromCatalog({
302
+ preset,
303
+ tools,
304
+ presets: presets.data,
305
+ toolCatalog: toolCatalog.data,
306
+ });
307
+ return resolved.tools;
308
+ }
309
+ const resolved = await resolvePresetTools(client, { preset, tools });
310
+ return resolved.tools;
311
+ }
312
+ export function clearPresetToolCatalogCache(baseURL) {
313
+ if (!baseURL) {
314
+ presetCatalogCache.clear();
315
+ toolCatalogCache.clear();
316
+ return;
317
+ }
318
+ const key = catalogCacheKey(baseURL);
319
+ presetCatalogCache.delete(key);
320
+ toolCatalogCache.delete(key);
321
+ }
322
+ async function cachedPresetCatalog(baseURL, ttl, load) {
323
+ return cachedCatalog(presetCatalogCache, baseURL, ttl, load);
324
+ }
325
+ async function cachedToolCatalog(baseURL, ttl, load) {
326
+ return cachedCatalog(toolCatalogCache, baseURL, ttl, load);
327
+ }
328
+ async function cachedCatalog(cache, baseURL, ttl, load) {
329
+ const key = catalogCacheKey(baseURL);
330
+ const now = Date.now();
331
+ const existing = cache.get(key);
332
+ if (existing && existing.expiresAt > now) {
333
+ return existing.promise;
334
+ }
335
+ const promise = load().catch((error) => {
336
+ if (cache.get(key)?.promise === promise) {
337
+ cache.delete(key);
338
+ }
339
+ throw error;
340
+ });
341
+ cache.set(key, { expiresAt: now + Math.max(0, ttl), promise });
342
+ return promise;
343
+ }
344
+ function catalogCacheKey(baseURL) {
345
+ return baseURL.trim().replace(/\/+$/, "");
346
+ }
347
+ async function buildInput(options) {
348
+ const chunks = [];
349
+ if (options.promptParts.length > 0) {
350
+ chunks.push(options.promptParts.join(" "));
351
+ }
352
+ if (options.file) {
353
+ chunks.push(await readFile(options.file, "utf8"));
354
+ }
355
+ if (options.stdin || chunks.length === 0) {
356
+ const piped = await readStdinIfAvailable();
357
+ if (piped.trim())
358
+ chunks.push(piped);
359
+ }
360
+ if (chunks.length === 0) {
361
+ throw new Error("Prompt is required. Pass text, --file, or pipe stdin.");
362
+ }
363
+ if (shouldUseLocalTools(options)) {
364
+ chunks.push("Local operations are available through the `local_workdir` and `local_shell` function tools. Use `local_workdir` for local file inspection and edits. Use `local_shell` for commands, tests, builds, package managers, and git. Do not encode local edits or commands in the final answer when a tool call is needed.");
365
+ chunks.push(await buildWorkdirContextBlock({
366
+ path: options.workdir || process.cwd(),
367
+ query: options.contextQuery,
368
+ maxFiles: options.maxContextFiles,
369
+ maxBytes: options.maxContextBytes,
370
+ }));
371
+ }
372
+ return chunks.join("\n\n");
373
+ }
374
+ async function prepareLocalWorkdirTools(options) {
375
+ if (!shouldUseLocalTools(options)) {
376
+ return null;
377
+ }
378
+ const service = await openWorkdir({ path: options.workdir || process.cwd() });
379
+ const registry = createLocalWorkdirToolRegistry(service.workdir, {
380
+ accessMode: localToolAccessMode(options),
381
+ });
382
+ const shellRegistry = createLocalShellToolRegistry({
383
+ accessMode: localToolAccessMode(options),
384
+ workdir: service.workdir,
385
+ ...localShellIsolationOptions(options.shellIsolation),
386
+ });
387
+ return {
388
+ registry: combineLocalToolRegistries(registry, shellRegistry),
389
+ instructions: [
390
+ localWorkdirToolInstructions(),
391
+ localShellToolInstructions({
392
+ accessMode: localToolAccessMode(options),
393
+ cwd: service.workdir.root,
394
+ ...localShellIsolationOptions(options.shellIsolation),
395
+ }),
396
+ "Use local_workdir for selected local workdir operations. Prefer summarize/list/search/grep before read/read_lines. Prefer preview_edits/apply_edits for source edits. Use local_shell for command/process tasks. In approval mode, local actions return requires_approval and must be explained to the user instead of retried blindly.",
397
+ ].join("\n\n"),
398
+ };
399
+ }
400
+ function shouldUseLocalTools(options) {
401
+ if (options.accessMode === "off")
402
+ return false;
403
+ return Boolean(options.includeLocalContext || options.workdir || options.accessMode === "approval" || options.accessMode === "full");
404
+ }
405
+ function localToolAccessMode(options) {
406
+ return options.accessMode === "full" ? "full" : "approval";
407
+ }
408
+ function combineLocalToolRegistries(workdir, shell) {
409
+ return {
410
+ definitions: () => [
411
+ ...workdir.definitions(),
412
+ ...shell.definitions(),
413
+ ],
414
+ execute: async (name, args, abortSignal) => {
415
+ if (name === workdir.toolName)
416
+ return await workdir.execute(name, args);
417
+ if (name === shell.toolName)
418
+ return await shell.execute(name, args, { signal: abortSignal });
419
+ throw new Error(`no local handler registered for function ${name}`);
420
+ },
421
+ has: (name) => name === workdir.toolName || name === shell.toolName,
422
+ };
423
+ }
424
+ async function executeLocalFunctionCalls(response, registry, abortSignal, onEvent) {
425
+ const outputs = [];
426
+ for (const call of pendingFunctionCalls(response)) {
427
+ throwIfAborted(abortSignal);
428
+ if (!registry.has(call.name)) {
429
+ throw new Error(`no local handler registered for function ${call.name}`);
430
+ }
431
+ const args = call.arguments ? JSON.parse(call.arguments) : {};
432
+ const result = await registry.execute(call.name, args, abortSignal);
433
+ throwIfAborted(abortSignal);
434
+ const action = typeof result.action === "string"
435
+ ? result.action
436
+ : typeof args.action === "string"
437
+ ? args.action
438
+ : undefined;
439
+ if (result.requires_approval === true) {
440
+ const approvalRequested = {
441
+ name: call.name,
442
+ action,
443
+ arguments: args,
444
+ preview: result.preview,
445
+ callID: call.call_id,
446
+ responseID: response.id,
447
+ };
448
+ onEvent?.({
449
+ type: "local_tool.approval_requested",
450
+ ...approvalRequested,
451
+ });
452
+ return { outputs, approvalRequested };
453
+ }
454
+ outputs.push(functionCallOutputInput(call.call_id, result));
455
+ onEvent?.({
456
+ type: "local_tool.completed",
457
+ name: call.name,
458
+ action,
459
+ requiresApproval: false,
460
+ });
461
+ }
462
+ return { outputs };
463
+ }
464
+ function throwIfAborted(signal) {
465
+ if (!signal?.aborted)
466
+ return;
467
+ throw new Error("Agent turn aborted.");
468
+ }
469
+ function requestAbortOptions(signal) {
470
+ return signal ? { signal } : undefined;
471
+ }
472
+ function localApprovalMessage(approval) {
473
+ return [
474
+ `Local action requires approval: ${approval.name}${approval.action ? `.${approval.action}` : ""}.`,
475
+ "Review it in the workbench, then use /apply to execute it or /reject to discard it.",
476
+ ].join("\n");
477
+ }
478
+ async function readStdinIfAvailable() {
479
+ if (process.stdin.isTTY)
480
+ return "";
481
+ const chunks = [];
482
+ for await (const chunk of process.stdin) {
483
+ chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
484
+ }
485
+ return Buffer.concat(chunks).toString("utf8");
486
+ }
@@ -0,0 +1,2 @@
1
+ export { type AgentRunOptions, type AgentTurnEvent, type AgentTurnResult, type LocalToolApprovalRequest, type WorkdirAccessMode, agentResponseFailureMessage, agentTurnEventFromStreamEvent, clearPresetToolCatalogCache, isAvailablePreset, listAvailablePresets, resolveAgentRequestTools, resumeAgentAfterLocalApproval, runAgent, runAgentTurn, } from "./agent/runner.js";
2
+ export { conversationSummary, deleteConversation, getConversation, listConversations, } from "./conversation/index.js";
package/dist/agent.js ADDED
@@ -0,0 +1,2 @@
1
+ export { agentResponseFailureMessage, agentTurnEventFromStreamEvent, clearPresetToolCatalogCache, isAvailablePreset, listAvailablePresets, resolveAgentRequestTools, resumeAgentAfterLocalApproval, runAgent, runAgentTurn, } from "./agent/runner.js";
2
+ export { conversationSummary, deleteConversation, getConversation, listConversations, } from "./conversation/index.js";
@@ -0,0 +1,37 @@
1
+ import type { WorkdirAccessMode } from "./agent.js";
2
+ export type ChatOptions = {
3
+ profile?: string;
4
+ conversation?: string;
5
+ preset?: string;
6
+ model?: string;
7
+ file?: string;
8
+ stdin?: boolean;
9
+ workdir?: string;
10
+ localContext?: boolean;
11
+ contextQuery?: string;
12
+ maxContextFiles?: string;
13
+ maxContextBytes?: string;
14
+ access?: string;
15
+ restart?: boolean;
16
+ stream?: boolean;
17
+ };
18
+ export declare function normalizeChatOptions(promptParts: string[], options: ChatOptions): {
19
+ profile: string | undefined;
20
+ promptParts: string[];
21
+ file: string | undefined;
22
+ stdin: boolean | undefined;
23
+ preset: string | undefined;
24
+ presetExplicit: boolean;
25
+ model: string | undefined;
26
+ modelExplicit: boolean;
27
+ stream: boolean;
28
+ conversation: string;
29
+ continueConversation: boolean;
30
+ restartConversation: boolean | undefined;
31
+ workdir: string | undefined;
32
+ includeLocalContext: boolean | undefined;
33
+ contextQuery: string | undefined;
34
+ maxContextFiles: number | undefined;
35
+ maxContextBytes: number | undefined;
36
+ accessMode: WorkdirAccessMode | undefined;
37
+ };